Merge pull request #21289 from philips-forks/feat-codescene-entitylevel

feat: codescene plugin at entity level
This commit is contained in:
Philipp Hugenroth
2024-03-05 13:05:41 +01:00
committed by GitHub
14 changed files with 434 additions and 37 deletions
+39
View File
@@ -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: <codescene-project-id>
```
```tsx
// In packages/app/src/components/catalog/EntityPage.tsx
import {
CodeSceneEntityPage,
CodeSceneEntityFileSummary,
isCodeSceneAvailable,
} from '@backstage/plugin-codescene';
/* other EntityLayout.Route items... */
<EntityLayout.Route
path="/codescene"
title="codescene"
if={isCodeSceneAvailable}
>
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<CodeSceneEntityKPICard />
</Grid>
<Grid item md={6}>
<CodeSceneEntityFileSummary />
</Grid>
</Grid>
</EntityLayout.Route>;
```
+13
View File
@@ -6,10 +6,20 @@
/// <reference types="react" />
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)
```
+4
View File
@@ -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"
},
@@ -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 <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
} else if (!analysis) {
return (
<EmptyState
missing="content"
title="CodeScene analysis"
description={`No analysis found for project with id ${projectId}`}
/>
);
}
return isCodeSceneAvailable(entity) ? (
<InfoCard>
<Content>
<Grid container spacing={2} direction="column">
<Grid item>
<CodeSceneFileSummary {...analysis} />
</Grid>
</Grid>
</Content>
</InfoCard>
) : (
<MissingAnnotationEmptyState annotation={CODESCENE_PROJECT_ANNOTATION} />
);
};
@@ -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';
@@ -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<Analysis> => {
return await codesceneApi.fetchLatestAnalysis(projectId);
}, []);
if (loading) {
return <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
} else if (!analysis) {
return (
<EmptyState
missing="content"
title="CodeScene analysis"
description={`No analysis found for project with id ${projectId}`}
/>
);
}
const analysisPath = `${codesceneHost}/${projectId}/analyses/${analysis.id}`;
const analysisLink = { title: 'Check the analysis', link: analysisPath };
const analysisBody = (
<>
<Typography variant="body1">
<b>Active authors</b>: {analysis.summary.active_authors_count}
</Typography>
<Typography variant="body1">
<b>Total authors</b>: {analysis.summary.authors_count}
</Typography>
<Typography variant="body1">
<b>Commits</b>: {analysis.summary.commits}
</Typography>
</>
);
return isCodeSceneAvailable(entity) ? (
<>
<Grid
container
spacing={1}
direction="row"
justifyContent="center"
alignItems="stretch"
>
<Grid item xs={8}>
<Grid style={{ height: '100%' }}>
<CodeHealthKpisCard
codesceneHost={codesceneHost}
analysis={analysis}
/>
</Grid>
</Grid>
<Grid item xs={4}>
<Grid container spacing={1}>
<Grid item xs={8}>
<GaugeCard
title="System Mastery"
progress={analysis.high_level_metrics.system_mastery / 100}
/>
</Grid>
<Grid item xs={8} style={{ height: '100%' }}>
<GaugeCard
title="Current Score"
progress={analysis.high_level_metrics.current_score / 10}
/>
</Grid>
</Grid>
</Grid>
<Grid item xs={12}>
<InfoCard title="Analysis" deepLink={analysisLink}>
{analysisBody}
</InfoCard>
</Grid>
</Grid>
<div style={{ textAlign: 'center' }}>
<Typography variant="caption">{`Last analyzed on ${DateTime.fromISO(
analysis.readable_analysis_time,
).toLocaleString(DateTime.DATETIME_MED)}`}</Typography>
</div>
</>
) : (
<MissingAnnotationEmptyState annotation={CODESCENE_PROJECT_ANNOTATION} />
);
};
@@ -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';
@@ -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<Analysis> => {
return await codesceneApi.fetchLatestAnalysis(projectId);
}, []);
if (loading) {
return <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
} else if (!analysis) {
return (
<EmptyState
missing="content"
title="CodeScene analysis"
description={`No analysis found for project with id ${params.projectId}`}
/>
);
}
export const CodeSceneFileSummary = (props: Analysis) => {
const columns: TableColumn[] = [
{
title: 'Language',
@@ -85,14 +57,36 @@ export const CodeSceneProjectDetailsPage = () => {
},
];
const fileSummaryTable = (
return (
<Table
options={{ paging: false, padding: 'dense' }}
data={analysis.file_summary.sort((a, b) => 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 <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
} else if (!analysis) {
return (
<EmptyState
missing="content"
title="CodeScene analysis"
description={`No analysis found for project with id ${params.projectId}`}
/>
);
}
const codesceneHost = config.getString('codescene.baseUrl');
const analysisPath = `${codesceneHost}/${projectId}/analyses/${analysis.id}`;
@@ -150,7 +144,9 @@ export const CodeSceneProjectDetailsPage = () => {
</Grid>
</Grid>
<Grid container spacing={2} direction="column">
<Grid item>{fileSummaryTable}</Grid>
<Grid item>
<CodeSceneFileSummary {...analysis} />
</Grid>
</Grid>
</Content>
</Page>
@@ -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<Analysis> => {
return await codesceneApi.fetchLatestAnalysis(projectId);
}, []);
return { analysis, loading, error };
};
+7 -1
View File
@@ -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';
+31
View File
@@ -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,
),
},
}),
);
+38
View File
@@ -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);