diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index d587a88402..afc96a68db 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -32,8 +32,10 @@ import { configApiRef, createApiFactory, errorApiRef, - githubAuthApiRef, + githubAuthApiRef, gitlabAuthApiRef, } from '@backstage/core-plugin-api'; +import { cicdStatisticsApiRef } from '@backstage/plugin-cicd-statistics'; +import {CicdStatisticsApiGitlab} from "@backstage/plugin-cicd-statistics-module-gitlab"; export const apis: AnyApiFactory[] = [ createApiFactory({ @@ -42,6 +44,7 @@ export const apis: AnyApiFactory[] = [ factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), }), + ScmAuth.createDefaultApiFactory(), createApiFactory({ @@ -64,4 +67,12 @@ export const apis: AnyApiFactory[] = [ }), createApiFactory(costInsightsApiRef, new ExampleCostInsightsClient()), + createApiFactory({ + api: cicdStatisticsApiRef, + deps: {gitlabAuthApi: gitlabAuthApiRef}, + factory({ gitlabAuthApi }) { + return new CicdStatisticsApiGitlab(gitlabAuthApi); + }, + }), +// createApiFactory(cicdStatisticsApiRef, new CicdStatisticsApiGitlab()), ]; diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 26068c6c8f..89897905b8 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -136,6 +136,7 @@ import { EntityNewRelicDashboardCard, } from '@backstage/plugin-newrelic-dashboard'; import { EntityGoCdContent, isGoCdAvailable } from '@backstage/plugin-gocd'; +import {EntityCicdStatisticsContent} from "@backstage/plugin-cicd-statistics"; import React, { ReactNode, useMemo, useState } from 'react'; @@ -444,6 +445,10 @@ const websiteEntityPage = ( {cicdContent} + + + + diff --git a/plugins/cicd-statistics-module-gitlab/.eslintrc.js b/plugins/cicd-statistics-module-gitlab/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/cicd-statistics-module-gitlab/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/cicd-statistics-module-gitlab/README.md b/plugins/cicd-statistics-module-gitlab/README.md new file mode 100644 index 0000000000..85c297932e --- /dev/null +++ b/plugins/cicd-statistics-module-gitlab/README.md @@ -0,0 +1,9 @@ +# cicd-statistics-module-gitlab + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/cicd-statistics-module-gitlab](http://localhost:3000/cicd-statistics-module-gitlab). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. diff --git a/plugins/cicd-statistics-module-gitlab/package.json b/plugins/cicd-statistics-module-gitlab/package.json new file mode 100644 index 0000000000..df9ccef6b8 --- /dev/null +++ b/plugins/cicd-statistics-module-gitlab/package.json @@ -0,0 +1,48 @@ +{ + "name": "@backstage/plugin-cicd-statistics-module-gitlab", + "description": "CI/CD Statistics plugin module; Gitlab CICD", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-plugin" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/cicd-statistics-module-gitlab" + }, + "keywords": [ + "backstage", + "cicd statestics", + "gitlab" + ], + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/catalog-model": "^0.9.10", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-cicd-statistics": "^0.1.0", + "@gitbeaker/browser": "34.2.0", + "@gitbeaker/core": "^35.4.0", + "lodash": "^4.17.21", + "luxon": "^2.0.2", + "p-limit": "^4.0.0" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/cicd-statistics-module-gitlab/src/api/gitlab.ts b/plugins/cicd-statistics-module-gitlab/src/api/gitlab.ts new file mode 100644 index 0000000000..a28ce612d5 --- /dev/null +++ b/plugins/cicd-statistics-module-gitlab/src/api/gitlab.ts @@ -0,0 +1,189 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + CicdStatisticsApi, + CicdState, + CicdConfiguration, + Build, + FilterStatusType, + FetchBuildsOptions, + Stage, +} from '@backstage/plugin-cicd-statistics'; +import { Gitlab } from '@gitbeaker/browser'; +import { OAuthApi } from '@backstage/core-plugin-api'; +import limiterFactory from 'p-limit'; + +import { Types } from '@gitbeaker/core'; +import { Entity, getEntitySourceLocation } from '@backstage/catalog-model'; + +const statusMap: Record = { + manual: 'unknown', + created: 'enqueued', + waiting_for_resource: 'stalled', + preparing: 'unknown', + pending: 'scheduled', + running: 'running', + success: 'succeeded', + failed: 'failed', + canceled: 'aborted', + skipped: 'aborted', + scheduled: 'scheduled', +}; + +function jobtToBuild(jobs: Array): Build[] { + return jobs.map(j => { + return { + id: j.id.toString(), + status: statusMap[j.status], + branchType: 'master', + duration: 0, // will get filled in later in a seperate API call + requestedAt: new Date(j.created_at), + stages: [], + }; + }); +} + +function jobsToStages(jobs: Array): Stage[] { + return jobs.map(j => { + const status = statusMap[j.status] ? statusMap[j.status] : 'unknown'; + return { + name: j.name, + status: status, + duration: j.duration ? ((j.duration * 1000) as number) : 0, + }; + }); +} + +type GitlabClient = { + api: InstanceType; + owner: string; +}; + +export class CicdStatisticsApiGitlab implements CicdStatisticsApi { + readonly #gitLabAuthApi: OAuthApi; + + constructor(gitLabAuthApi: OAuthApi) { + this.#gitLabAuthApi = gitLabAuthApi; + } + + public async createGitlabApi( + entity: Entity, + scopes: string[], + ): Promise { + const entityInfo = getEntitySourceLocation(entity); + const url = new URL(entityInfo.target); + const owner = url.pathname.split('/-/blob/')[0]; + const oauthToken = await this.#gitLabAuthApi.getAccessToken(scopes); + return { + api: new Gitlab({ + host: `https://${url.host}`, + oauthToken, + }), + owner: owner.substring(1), + }; + } + + private static async updateBuildWithStages( + gitbeaker: InstanceType, + owner: string, + build: Build, + ): Promise { + const jobs = await gitbeaker.Jobs.showPipelineJobs( + owner, + parseInt(build.id, 10), + ); + const stages = jobsToStages(jobs); + return stages; + } + + private static async getDurationOfBuild( + gitbeaker: InstanceType, + owner: string, + build: Build, + ): Promise { + const pipeline = (await gitbeaker.Pipelines.show( + owner, + parseInt(build.id, 10), + )) as Types.PipelineExtendedSchema; + return parseInt(pipeline.duration as string, 10) * 1000; + } + + private static async getDefaultBranch( + gitbeaker: InstanceType, + owner: string, + ): Promise { + const branches = await gitbeaker.Branches.all(owner); + return branches.find(b => b.default)?.name; + } + + public async fetchBuilds(options: FetchBuildsOptions): Promise { + const { + entity, + updateProgress, + timeFrom, + timeTo, + filterStatus = ['all'], + filterType = 'all', + } = options; + const { api, owner } = await this.createGitlabApi(entity as Entity, [ + 'read_api', + ]); + updateProgress(0, 0, 0); + + const branch = + filterType === 'master' + ? await CicdStatisticsApiGitlab.getDefaultBranch(api, owner) + : undefined; + const pipelines = await api.Pipelines.all(owner, { + perPage: 25, + updated_after: timeFrom.toISOString(), + updated_before: timeTo.toISOString(), + ref: branch, + }); + + const limiter = limiterFactory(10); + const builds = jobtToBuild(pipelines).map(async build => ({ + ...build, + duration: await limiter(() => + CicdStatisticsApiGitlab.getDurationOfBuild(api, owner, build), + ), + stages: await limiter(() => + CicdStatisticsApiGitlab.updateBuildWithStages(api, owner, build), + ), + })); + const promisedBuilds = (await Promise.all(builds)).filter(b => + filterStatus.includes(b.status), + ) as unknown as Build[]; + + return { builds: promisedBuilds }; + } + + public async getConfiguration(): Promise> { + return { + availableStatuses: [ + 'succeeded', + 'failed', + 'enqueued', + 'running', + 'aborted', + 'stalled', + 'expired', + 'unknown', + ] as const, + }; + } +} diff --git a/plugins/cicd-statistics-module-gitlab/src/api/index.ts b/plugins/cicd-statistics-module-gitlab/src/api/index.ts new file mode 100644 index 0000000000..686c8b5f63 --- /dev/null +++ b/plugins/cicd-statistics-module-gitlab/src/api/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from "./gitlab"; diff --git a/plugins/cicd-statistics-module-gitlab/src/index.ts b/plugins/cicd-statistics-module-gitlab/src/index.ts new file mode 100644 index 0000000000..409c45eecd --- /dev/null +++ b/plugins/cicd-statistics-module-gitlab/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { CicdStatisticsApiGitlab } from './api'; diff --git a/plugins/cicd-statistics-module-gitlab/src/setupTests.ts b/plugins/cicd-statistics-module-gitlab/src/setupTests.ts new file mode 100644 index 0000000000..9bb3e72355 --- /dev/null +++ b/plugins/cicd-statistics-module-gitlab/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/cicd-statistics/src/apis/types.ts b/plugins/cicd-statistics/src/apis/types.ts index 32d9d3f2df..975f69525c 100644 --- a/plugins/cicd-statistics/src/apis/types.ts +++ b/plugins/cicd-statistics/src/apis/types.ts @@ -117,7 +117,7 @@ export interface Build { duration: number; /** Top-level build stages */ - stages: Array; + stages?: Array; } /** diff --git a/plugins/cicd-statistics/src/components/button-switch.tsx b/plugins/cicd-statistics/src/components/button-switch.tsx index 65f55eee41..89a5f7735a 100644 --- a/plugins/cicd-statistics/src/components/button-switch.tsx +++ b/plugins/cicd-statistics/src/components/button-switch.tsx @@ -78,6 +78,9 @@ export function ButtonSwitch(props: ButtonSwitchProps) { ); const value = switchValue(values[index]); + if(!value){ + console.log(values[index]); + } if (props.multi) { props.onChange( props.selection.includes(value as T) diff --git a/plugins/cicd-statistics/src/entity-page.tsx b/plugins/cicd-statistics/src/entity-page.tsx index 3c51dabf2a..8c3209c854 100644 --- a/plugins/cicd-statistics/src/entity-page.tsx +++ b/plugins/cicd-statistics/src/entity-page.tsx @@ -166,6 +166,8 @@ function CicdCharts(props: CicdChartsProps) { errorApi.post(chartableStagesState.error); }, [errorApi, chartableStagesState.error]); + console.log(chartableStagesState); + return ( diff --git a/plugins/cicd-statistics/src/utils/stage-names.ts b/plugins/cicd-statistics/src/utils/stage-names.ts index 69c8cdcf25..8853336fef 100644 --- a/plugins/cicd-statistics/src/utils/stage-names.ts +++ b/plugins/cicd-statistics/src/utils/stage-names.ts @@ -68,6 +68,8 @@ export async function cleanupBuildTree( return map(builds, { chunk: 'idle' }, build => ({ ...build, - stages: build.stages.map(stage => recurseStage(stage, [])), + stages: build.stages + ? build.stages.map(stage => recurseStage(stage, [])) + : [], })); }