From e477ec4df2c016dde6361ffed0d024b986e4bff1 Mon Sep 17 00:00:00 2001 From: blbose Date: Tue, 14 Nov 2023 21:33:08 +0530 Subject: [PATCH] feat: codescene plugin at entity level Signed-off-by: blbose --- .changeset/beige-books-kiss.md | 5 + plugins/codescene/README.md | 31 ++++ plugins/codescene/package.json | 4 + .../CodeSceneEntityKPICard.test.tsx | 135 ++++++++++++++ .../CodeSceneEntityKPICard.tsx | 80 +++++++++ .../CodeSceneEntityKPICard/index.ts | 16 ++ .../CodeSceneEntityPage.test.tsx | 135 ++++++++++++++ .../CodeSceneEntityPage.tsx | 167 ++++++++++++++++++ .../components/CodeSceneEntityPage/index.ts | 16 ++ plugins/codescene/src/index.ts | 8 +- plugins/codescene/src/plugin.ts | 22 +++ plugins/codescene/src/utils/commonUtil.ts | 29 +++ yarn.lock | 2 + 13 files changed, 649 insertions(+), 1 deletion(-) create mode 100644 .changeset/beige-books-kiss.md create mode 100644 plugins/codescene/src/components/CodeSceneEntityKPICard/CodeSceneEntityKPICard.test.tsx create mode 100644 plugins/codescene/src/components/CodeSceneEntityKPICard/CodeSceneEntityKPICard.tsx create mode 100644 plugins/codescene/src/components/CodeSceneEntityKPICard/index.ts create mode 100644 plugins/codescene/src/components/CodeSceneEntityPage/CodeSceneEntityPage.test.tsx create mode 100644 plugins/codescene/src/components/CodeSceneEntityPage/CodeSceneEntityPage.tsx create mode 100644 plugins/codescene/src/components/CodeSceneEntityPage/index.ts create mode 100644 plugins/codescene/src/utils/commonUtil.ts diff --git a/.changeset/beige-books-kiss.md b/.changeset/beige-books-kiss.md new file mode 100644 index 0000000000..7c465874ea --- /dev/null +++ b/.changeset/beige-books-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-codescene': patch +--- + +Extending code scene plugin in Entity page. diff --git a/plugins/codescene/README.md b/plugins/codescene/README.md index 1a713c1589..5f0df2c5c7 100644 --- a/plugins/codescene/README.md +++ b/plugins/codescene/README.md @@ -60,3 +60,34 @@ proxy: codescene: baseUrl: https://codescene.my-company.net # replace with your own URL ``` + +5. Adding the codescene plugin to Entity page: + +```yaml +# Add `codescene` annotations to the `catalog-info.yaml` of an entity. +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage + annotations: + codescene.io/project-id: +``` + +```tsx +// In packages/app/src/components/catalog/EntityPage.tsx + +import { + CodeSceneEntityPage, + isCodeSceneAvailable, +} from '@backstage/plugin-codescene'; + +/* other EntityLayout.Route items... */ + + + +; +``` diff --git a/plugins/codescene/package.json b/plugins/codescene/package.json index cf18ebdaf5..005133af23 100644 --- a/plugins/codescene/package.json +++ b/plugins/codescene/package.json @@ -29,13 +29,17 @@ "postpack": "backstage-cli package postpack" }, "dependencies": { + "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", + "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/theme": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/lab": "4.0.0-alpha.61", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "luxon": "^3.4.4", "rc-progress": "3.5.1", "react-use": "^17.2.4" }, diff --git a/plugins/codescene/src/components/CodeSceneEntityKPICard/CodeSceneEntityKPICard.test.tsx b/plugins/codescene/src/components/CodeSceneEntityKPICard/CodeSceneEntityKPICard.test.tsx new file mode 100644 index 0000000000..f433f33ce5 --- /dev/null +++ b/plugins/codescene/src/components/CodeSceneEntityKPICard/CodeSceneEntityKPICard.test.tsx @@ -0,0 +1,135 @@ +/* + * Copyright 2023 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 React from 'react'; +import { CodeSceneEntityKPICard } from './CodeSceneEntityKPICard'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { + setupRequestMockHandlers, + renderInTestApp, + TestApiRegistry, +} from '@backstage/test-utils'; +import { CodeSceneApi, codesceneApiRef } from '../../api/api'; +import { ApiProvider } from '@backstage/core-app-api'; +import { Analysis } from '../../api/types'; +import { ConfigReader } from '@backstage/config'; +import { configApiRef } from '@backstage/core-plugin-api'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; + +const entity = { + metadata: { + namespace: 'default', + annotations: { + 'codescene.io/project-id': '12345', + }, + name: 'codescene-test-project', + title: 'codescene-test-project', + description: 'A demo codescene-test repo', + links: [ + { + url: 'https://some-demo.com', + title: 'Demo', + icon: 'dashboard', + }, + ], + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + spec: { + type: 'website', + owner: 'swcoe', + lifecycle: 'experimental', + }, + relations: [ + { + type: 'ownedBy', + targetRef: 'group:default/swcoe', + target: { + kind: 'group', + namespace: 'default', + name: 'swcoe', + }, + }, + ], +}; + +describe('CodeSceneEntityKPICard', () => { + const server = setupServer(); + // Enable same handlers for network requests + setupRequestMockHandlers(server); + let apis: TestApiRegistry; + + // setup mock response + beforeEach(() => { + server.use( + rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))), + ); + + const config = new ConfigReader({ + codescene: { + baseUrl: 'www.fake-url.com', + }, + }); + const analysis: Analysis = { + id: 1, + name: 'codescene-test-project', + project_id: 12345, + readable_analysis_time: '2022-03-22', + summary: { + unique_issue_ids: 0, + issues_filtered_as_outliers: 0, + entities: 0, + commits_with_issue_ids: 0, + authors_count: 0, + active_authors_count: 0, + issues_with_cycle_time: 0, + commits: 0, + issue_ids_matched_to_issues: 0, + issues_classed_as_defects: 0, + issues_with_cost: 0, + }, + file_summary: [], + high_level_metrics: { + current_score: 0, + month_score: 0, + year_score: 0, + active_developers: 0, + lines_of_code: 0, + system_mastery: 0, + }, + }; + apis = TestApiRegistry.from( + [ + codesceneApiRef, + { + fetchLatestAnalysis: jest.fn(() => analysis), + } as unknown as CodeSceneApi, + ], + [configApiRef, config], + ); + }); + + it('should render', async () => { + const rendered = await renderInTestApp( + + + + + , + ); + expect(rendered.getByText(/Mar 22, 2022/i)).toBeInTheDocument(); + }); +}); diff --git a/plugins/codescene/src/components/CodeSceneEntityKPICard/CodeSceneEntityKPICard.tsx b/plugins/codescene/src/components/CodeSceneEntityKPICard/CodeSceneEntityKPICard.tsx new file mode 100644 index 0000000000..0f348eaf3f --- /dev/null +++ b/plugins/codescene/src/components/CodeSceneEntityKPICard/CodeSceneEntityKPICard.tsx @@ -0,0 +1,80 @@ +/* + * Copyright 2023 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 { + Content, + EmptyState, + Progress, + ContentHeader, +} from '@backstage/core-components'; +import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import Alert from '@material-ui/lab/Alert'; +import React from 'react'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import useAsync from 'react-use/lib/useAsync'; +import { codesceneApiRef } from '../../api/api'; +import { Analysis } from '../../api/types'; +import { CodeHealthKpisCard } from '../CodeHealthKpisCard/CodeHealthKpisCard'; +import { + getProjectAnnotation, + CODESCENE_PROJECT_ANNOTATION, + isCodeSceneAvailable, +} from '../../utils/commonUtil'; +import { DateTime } from 'luxon'; +import { MissingAnnotationEmptyState } from '@backstage/plugin-catalog-react'; + +export const CodeSceneEntityKPICard = () => { + const { entity } = useEntity(); + const { projectId } = getProjectAnnotation(entity); + const codesceneApi = useApi(codesceneApiRef); + const config = useApi(configApiRef); + const codesceneHost = config.getString('codescene.baseUrl'); + + const { + value: analysis, + loading, + error, + } = useAsync(async (): Promise => { + return await codesceneApi.fetchLatestAnalysis(projectId); + }, []); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } else if (!analysis) { + return ( + + ); + } + + return isCodeSceneAvailable(entity) ? ( + + + Last analyzed on{' '} + {DateTime.fromISO(analysis.readable_analysis_time).toLocaleString( + DateTime.DATETIME_MED, + )} + + + + ) : ( + + ); +}; diff --git a/plugins/codescene/src/components/CodeSceneEntityKPICard/index.ts b/plugins/codescene/src/components/CodeSceneEntityKPICard/index.ts new file mode 100644 index 0000000000..934705e321 --- /dev/null +++ b/plugins/codescene/src/components/CodeSceneEntityKPICard/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 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 { CodeSceneEntityKPICard } from './CodeSceneEntityKPICard'; diff --git a/plugins/codescene/src/components/CodeSceneEntityPage/CodeSceneEntityPage.test.tsx b/plugins/codescene/src/components/CodeSceneEntityPage/CodeSceneEntityPage.test.tsx new file mode 100644 index 0000000000..96fd16aa62 --- /dev/null +++ b/plugins/codescene/src/components/CodeSceneEntityPage/CodeSceneEntityPage.test.tsx @@ -0,0 +1,135 @@ +/* + * Copyright 2023 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 React from 'react'; +import { CodeSceneEntityPage } from './CodeSceneEntityPage'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { + setupRequestMockHandlers, + renderInTestApp, + TestApiRegistry, +} from '@backstage/test-utils'; +import { CodeSceneApi, codesceneApiRef } from '../../api/api'; +import { ApiProvider } from '@backstage/core-app-api'; +import { Analysis } from '../../api/types'; +import { ConfigReader } from '@backstage/config'; +import { configApiRef } from '@backstage/core-plugin-api'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; + +const entity = { + metadata: { + namespace: 'default', + annotations: { + 'codescene.io/project-id': '12345', + }, + name: 'codescene-test-project', + title: 'codescene-test-project', + description: 'A demo codescene-test repo', + links: [ + { + url: 'https://some-demo.com', + title: 'Demo', + icon: 'dashboard', + }, + ], + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + spec: { + type: 'website', + owner: 'swcoe', + lifecycle: 'experimental', + }, + relations: [ + { + type: 'ownedBy', + targetRef: 'group:default/swcoe', + target: { + kind: 'group', + namespace: 'default', + name: 'swcoe', + }, + }, + ], +}; + +describe('CodeSceneEntityPage', () => { + const server = setupServer(); + // Enable sane handlers for network requests + setupRequestMockHandlers(server); + let apis: TestApiRegistry; + + // setup mock response + beforeEach(() => { + server.use( + rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))), + ); + + const config = new ConfigReader({ + codescene: { + baseUrl: 'www.fake-url.com', + }, + }); + const analysis: Analysis = { + id: 1, + name: 'codescene-test-project', + project_id: 12345, + readable_analysis_time: '2022-03-22', + summary: { + unique_issue_ids: 0, + issues_filtered_as_outliers: 0, + entities: 0, + commits_with_issue_ids: 0, + authors_count: 0, + active_authors_count: 0, + issues_with_cycle_time: 0, + commits: 0, + issue_ids_matched_to_issues: 0, + issues_classed_as_defects: 0, + issues_with_cost: 0, + }, + file_summary: [], + high_level_metrics: { + current_score: 0, + month_score: 0, + year_score: 0, + active_developers: 0, + lines_of_code: 0, + system_mastery: 0, + }, + }; + apis = TestApiRegistry.from( + [ + codesceneApiRef, + { + fetchLatestAnalysis: jest.fn(() => analysis), + } as unknown as CodeSceneApi, + ], + [configApiRef, config], + ); + }); + + it('should render', async () => { + const rendered = await renderInTestApp( + + + + + , + ); + expect(rendered.getByText(/codescene-test-project/i)).toBeInTheDocument(); + }); +}); diff --git a/plugins/codescene/src/components/CodeSceneEntityPage/CodeSceneEntityPage.tsx b/plugins/codescene/src/components/CodeSceneEntityPage/CodeSceneEntityPage.tsx new file mode 100644 index 0000000000..19d2636fe9 --- /dev/null +++ b/plugins/codescene/src/components/CodeSceneEntityPage/CodeSceneEntityPage.tsx @@ -0,0 +1,167 @@ +/* + * Copyright 2023 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 { + Content, + EmptyState, + GaugeCard, + Header, + InfoCard, + Page, + Progress, + SupportButton, + Table, + TableColumn, +} from '@backstage/core-components'; +import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { Grid, Typography } from '@material-ui/core'; +import Alert from '@material-ui/lab/Alert'; +import React from 'react'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import useAsync from 'react-use/lib/useAsync'; +import { codesceneApiRef } from '../../api/api'; +import { Analysis } from '../../api/types'; +import { CodeHealthKpisCard } from '../CodeHealthKpisCard/CodeHealthKpisCard'; +import { MissingAnnotationEmptyState } from '@backstage/plugin-catalog-react'; +import { + getProjectAnnotation, + CODESCENE_PROJECT_ANNOTATION, + isCodeSceneAvailable, +} from '../../utils/commonUtil'; + +export const CodeSceneEntityPage = () => { + const { entity } = useEntity(); + const { projectId } = getProjectAnnotation(entity); + const codesceneApi = useApi(codesceneApiRef); + const config = useApi(configApiRef); + const { + value: analysis, + loading, + error, + } = useAsync(async (): Promise => { + return await codesceneApi.fetchLatestAnalysis(projectId); + }, []); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } else if (!analysis) { + return ( + + ); + } + + const columns: TableColumn[] = [ + { + title: 'Language', + field: 'language', + highlight: true, + }, + { + title: 'Files', + field: 'number_of_files', + type: 'numeric', + }, + { + title: 'Code', + field: 'code', + type: 'numeric', + }, + { + title: 'Comment', + field: 'comment', + type: 'numeric', + }, + ]; + + const fileSummaryTable = ( + b.code - a.code)} + columns={columns} + title="File Summary" + /> + ); + + const codesceneHost = config.getString('codescene.baseUrl'); + const analysisPath = `${codesceneHost}/${projectId}/analyses/${analysis.id}`; + const analysisLink = { title: 'Check the analysis', link: analysisPath }; + + const analysisBody = ( + <> + + Active authors: {analysis.summary.active_authors_count} + + + Total authors: {analysis.summary.authors_count} + + + Commits: {analysis.summary.commits} + + + ); + + return isCodeSceneAvailable(entity) ? ( + +
+ + See hidden risks and social patterns in your code. Prioritize and + reduce technical debt. + +
+ + + + + + + + + + + + + + + {analysisBody} + + + + + {fileSummaryTable} + + +
+ ) : ( + + ); +}; diff --git a/plugins/codescene/src/components/CodeSceneEntityPage/index.ts b/plugins/codescene/src/components/CodeSceneEntityPage/index.ts new file mode 100644 index 0000000000..3909fb4861 --- /dev/null +++ b/plugins/codescene/src/components/CodeSceneEntityPage/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 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 { CodeSceneEntityPage } from './CodeSceneEntityPage'; diff --git a/plugins/codescene/src/index.ts b/plugins/codescene/src/index.ts index 301b4377a7..414af61d04 100644 --- a/plugins/codescene/src/index.ts +++ b/plugins/codescene/src/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Backstage Authors + * Copyright 2023 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. @@ -17,5 +17,11 @@ export { codescenePlugin, CodeScenePage, CodeSceneProjectDetailsPage, + CodeSceneEntityPage, + CodeSceneEntityKPICard, } from './plugin'; export { CodeSceneIcon } from './CodeSceneIcon'; +export { + isCodeSceneAvailable, + CODESCENE_PROJECT_ANNOTATION, +} from './utils/commonUtil'; diff --git a/plugins/codescene/src/plugin.ts b/plugins/codescene/src/plugin.ts index b4e297345d..5c1a8e2e14 100644 --- a/plugins/codescene/src/plugin.ts +++ b/plugins/codescene/src/plugin.ts @@ -67,3 +67,25 @@ export const CodeSceneProjectDetailsPage = codescenePlugin.provide( mountPoint: projectDetailsRouteRef, }), ); + +export const CodeSceneEntityKPICard = codescenePlugin.provide( + createRoutableExtension({ + name: 'CodeSceneEntityKPICard', + component: () => + import('./components/CodeSceneEntityKPICard').then( + m => m.CodeSceneEntityKPICard, + ), + mountPoint: rootRouteRef, + }), +); + +export const CodeSceneEntityPage = codescenePlugin.provide( + createRoutableExtension({ + name: 'CodeSceneEntityPage', + component: () => + import('./components/CodeSceneEntityPage').then( + m => m.CodeSceneEntityPage, + ), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/codescene/src/utils/commonUtil.ts b/plugins/codescene/src/utils/commonUtil.ts new file mode 100644 index 0000000000..830cc797c9 --- /dev/null +++ b/plugins/codescene/src/utils/commonUtil.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2023 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 { Entity } from '@backstage/catalog-model'; + +export const CODESCENE_PROJECT_ANNOTATION = 'codescene.io/project-id'; + +export const getProjectAnnotation = (entity: Entity) => { + const annotation = + entity?.metadata.annotations?.[CODESCENE_PROJECT_ANNOTATION]; + const projectId = annotation?.split('/')[0]; + return { projectId: Number(projectId) }; +}; + +export const isCodeSceneAvailable = (entity: Entity) => + Boolean(getProjectAnnotation(entity).projectId); diff --git a/yarn.lock b/yarn.lock index 53c220af6d..7105733663 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6073,6 +6073,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-codescene@workspace:plugins/codescene" dependencies: + "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/core-app-api": "workspace:^" @@ -6080,6 +6081,7 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/plugin-catalog-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@material-ui/core": ^4.12.2 "@material-ui/lab": 4.0.0-alpha.61