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..6770e34ddd 100644 --- a/plugins/codescene/README.md +++ b/plugins/codescene/README.md @@ -60,3 +60,42 @@ 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, + CodeSceneEntityFileSummary, + isCodeSceneAvailable, +} from '@backstage/plugin-codescene'; + +/* other EntityLayout.Route items... */ + + + + + + + + + + +; +``` diff --git a/plugins/codescene/api-report.md b/plugins/codescene/api-report.md index 99eb2b6256..33b994ea6e 100644 --- a/plugins/codescene/api-report.md +++ b/plugins/codescene/api-report.md @@ -6,10 +6,20 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { Entity } from '@backstage/catalog-model'; import { IconComponent } from '@backstage/core-plugin-api'; import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; +// @public (undocumented) +export const CODESCENE_PROJECT_ANNOTATION = 'codescene.io/project-id'; + +// @public (undocumented) +export const CodeSceneEntityFileSummary: () => JSX_2.Element; + +// @public (undocumented) +export const CodeSceneEntityKPICard: () => JSX_2.Element; + // @public (undocumented) export const CodeSceneIcon: IconComponent; @@ -30,5 +40,8 @@ export const codescenePlugin: BackstagePlugin< // @public (undocumented) export const CodeSceneProjectDetailsPage: () => JSX_2.Element; +// @public (undocumented) +export const isCodeSceneAvailable: (entity: Entity) => boolean; + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/codescene/package.json b/plugins/codescene/package.json index fcb1ddecc3..7102635fdd 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/CodeSceneEntityFileSummary/CodeSceneEntityFileSummary.tsx b/plugins/codescene/src/components/CodeSceneEntityFileSummary/CodeSceneEntityFileSummary.tsx new file mode 100644 index 0000000000..db5cef8c44 --- /dev/null +++ b/plugins/codescene/src/components/CodeSceneEntityFileSummary/CodeSceneEntityFileSummary.tsx @@ -0,0 +1,66 @@ +/* + * 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 { Content, EmptyState, InfoCard } from '@backstage/core-components'; +import { Grid } from '@material-ui/core'; +import Alert from '@material-ui/lab/Alert'; +import React from 'react'; +import { + useEntity, + MissingAnnotationEmptyState, +} from '@backstage/plugin-catalog-react'; +import { + getProjectAnnotation, + CODESCENE_PROJECT_ANNOTATION, + isCodeSceneAvailable, +} from '../../utils/commonUtil'; +import { useAnalyses } from '../../hooks/useAnalyses'; +import { useApp } from '@backstage/core-plugin-api'; +import { CodeSceneFileSummary } from '../CodeSceneProjectDetailsPage/CodeSceneProjectDetailsPage'; + +export const CodeSceneEntityFileSummary = () => { + const { entity } = useEntity(); + const { projectId } = getProjectAnnotation(entity); + const { Progress } = useApp().getComponents(); + const { analysis, loading, error } = useAnalyses(projectId); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } else if (!analysis) { + return ( + + ); + } + + return isCodeSceneAvailable(entity) ? ( + + + + + + + + + + ) : ( + + ); +}; diff --git a/plugins/codescene/src/components/CodeSceneEntityFileSummary/index.ts b/plugins/codescene/src/components/CodeSceneEntityFileSummary/index.ts new file mode 100644 index 0000000000..7495fe26ef --- /dev/null +++ b/plugins/codescene/src/components/CodeSceneEntityFileSummary/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 { CodeSceneEntityFileSummary } from './CodeSceneEntityFileSummary'; diff --git a/plugins/codescene/src/components/CodeSceneEntityKPICard/CodeSceneEntityKPICard.tsx b/plugins/codescene/src/components/CodeSceneEntityKPICard/CodeSceneEntityKPICard.tsx new file mode 100644 index 0000000000..1757ad19cf --- /dev/null +++ b/plugins/codescene/src/components/CodeSceneEntityKPICard/CodeSceneEntityKPICard.tsx @@ -0,0 +1,129 @@ +/* + * 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 { EmptyState, GaugeCard, InfoCard } from '@backstage/core-components'; +import { configApiRef, useApi, useApp } 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'; +import { Grid, Typography } from '@material-ui/core'; + +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 { Progress } = useApp().getComponents(); + + 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 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) ? ( + <> + + + + + + + + + + + + + + + + + + + {analysisBody} + + + +
+ {`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/CodeSceneProjectDetailsPage/CodeSceneProjectDetailsPage.tsx b/plugins/codescene/src/components/CodeSceneProjectDetailsPage/CodeSceneProjectDetailsPage.tsx index 1197edb47e..dbacd919b4 100644 --- a/plugins/codescene/src/components/CodeSceneProjectDetailsPage/CodeSceneProjectDetailsPage.tsx +++ b/plugins/codescene/src/components/CodeSceneProjectDetailsPage/CodeSceneProjectDetailsPage.tsx @@ -20,48 +20,20 @@ import { Header, InfoCard, Page, - Progress, SupportButton, Table, TableColumn, } from '@backstage/core-components'; -import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { configApiRef, useApi, useApp } from '@backstage/core-plugin-api'; import { Grid, Typography } from '@material-ui/core'; import Alert from '@material-ui/lab/Alert'; import React from 'react'; import { useParams } from 'react-router-dom'; -import useAsync from 'react-use/lib/useAsync'; -import { codesceneApiRef } from '../../api/api'; -import { Analysis } from '../../api/types'; import { CodeHealthKpisCard } from '../CodeHealthKpisCard/CodeHealthKpisCard'; +import { useAnalyses } from '../../hooks/useAnalyses'; +import { Analysis } from '../../api/types'; -export const CodeSceneProjectDetailsPage = () => { - const params = useParams(); - const projectId = Number(params.projectId); - 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 ( - - ); - } - +export const CodeSceneFileSummary = (props: Analysis) => { const columns: TableColumn[] = [ { title: 'Language', @@ -85,14 +57,36 @@ export const CodeSceneProjectDetailsPage = () => { }, ]; - const fileSummaryTable = ( + return ( b.code - a.code)} + data={props.file_summary.sort((a, b) => b.code - a.code)} columns={columns} title="File Summary" /> ); +}; + +export const CodeSceneProjectDetailsPage = () => { + const params = useParams(); + const projectId = Number(params.projectId); + const config = useApi(configApiRef); + const { analysis, loading, error } = useAnalyses(projectId); + const { Progress } = useApp().getComponents(); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } else if (!analysis) { + return ( + + ); + } const codesceneHost = config.getString('codescene.baseUrl'); const analysisPath = `${codesceneHost}/${projectId}/analyses/${analysis.id}`; @@ -150,7 +144,9 @@ export const CodeSceneProjectDetailsPage = () => { - {fileSummaryTable} + + + diff --git a/plugins/codescene/src/hooks/useAnalyses.ts b/plugins/codescene/src/hooks/useAnalyses.ts new file mode 100644 index 0000000000..9326b27641 --- /dev/null +++ b/plugins/codescene/src/hooks/useAnalyses.ts @@ -0,0 +1,34 @@ +/* + * 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 { Analysis } from '../api/types'; +import { codesceneApiRef } from '../api/api'; +import { useApi } from '@backstage/core-plugin-api'; +import useAsync from 'react-use/lib/useAsync'; + +export const useAnalyses = (projectId: number) => { + const codesceneApi = useApi(codesceneApiRef); + + const { + value: analysis, + loading, + error, + } = useAsync(async (): Promise => { + return await codesceneApi.fetchLatestAnalysis(projectId); + }, []); + + return { analysis, loading, error }; +}; diff --git a/plugins/codescene/src/index.ts b/plugins/codescene/src/index.ts index 301b4377a7..463086dd5e 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, + CodeSceneEntityFileSummary, + 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..49014568a5 100644 --- a/plugins/codescene/src/plugin.ts +++ b/plugins/codescene/src/plugin.ts @@ -18,6 +18,7 @@ import { createRoutableExtension, createApiFactory, discoveryApiRef, + createComponentExtension, } from '@backstage/core-plugin-api'; import { codesceneApiRef, CodeSceneClient } from './api/api'; import { rootRouteRef, projectDetailsRouteRef } from './routes'; @@ -67,3 +68,33 @@ export const CodeSceneProjectDetailsPage = codescenePlugin.provide( mountPoint: projectDetailsRouteRef, }), ); + +/** + * @public + */ +export const CodeSceneEntityKPICard = codescenePlugin.provide( + createComponentExtension({ + name: 'CodeSceneEntityKPICard', + component: { + lazy: () => + import('./components/CodeSceneEntityKPICard').then( + m => m.CodeSceneEntityKPICard, + ), + }, + }), +); + +/** + * @public + */ +export const CodeSceneEntityFileSummary = codescenePlugin.provide( + createComponentExtension({ + name: 'CodeSceneEntityFileSummary', + component: { + lazy: () => + import('./components/CodeSceneEntityFileSummary').then( + m => m.CodeSceneEntityFileSummary, + ), + }, + }), +); diff --git a/plugins/codescene/src/utils/commonUtil.ts b/plugins/codescene/src/utils/commonUtil.ts new file mode 100644 index 0000000000..435cb377e6 --- /dev/null +++ b/plugins/codescene/src/utils/commonUtil.ts @@ -0,0 +1,38 @@ +/* + * 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'; + +/** + * @public + */ +export const CODESCENE_PROJECT_ANNOTATION = 'codescene.io/project-id'; + +/** + * @public + */ +export const getProjectAnnotation = (entity: Entity) => { + const annotation = + entity?.metadata.annotations?.[CODESCENE_PROJECT_ANNOTATION]; + const projectId = annotation?.split('/')[0]; + return { projectId: Number(projectId) }; +}; + +/** + * @public + */ +export const isCodeSceneAvailable = (entity: Entity) => + Boolean(getProjectAnnotation(entity).projectId); diff --git a/yarn.lock b/yarn.lock index 3c08b1e93e..9571717343 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6306,6 +6306,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:^" @@ -6313,13 +6314,16 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/plugin-catalog-react": "workspace:^" "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" "@material-ui/core": ^4.12.2 "@material-ui/lab": 4.0.0-alpha.61 "@testing-library/dom": ^9.0.0 "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^14.0.0 "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + luxon: ^3.4.4 msw: ^1.0.0 rc-progress: 3.5.1 react-use: ^17.2.4 @@ -34316,7 +34320,7 @@ __metadata: languageName: node linkType: hard -"luxon@npm:^3.0.0, luxon@npm:^3.3.0, luxon@npm:^3.4.3, luxon@npm:~3.4.0": +"luxon@npm:^3.0.0, luxon@npm:^3.3.0, luxon@npm:^3.4.3, luxon@npm:^3.4.4, luxon@npm:~3.4.0": version: 3.4.4 resolution: "luxon@npm:3.4.4" checksum: 36c1f99c4796ee4bfddf7dc94fa87815add43ebc44c8934c924946260a58512f0fd2743a629302885df7f35ccbd2d13f178c15df046d0e3b6eb71db178f1c60c