Merge pull request #21289 from philips-forks/feat-codescene-entitylevel
feat: codescene plugin at entity level
This commit is contained in:
@@ -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>;
|
||||
```
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
+66
@@ -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';
|
||||
+31
-35
@@ -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 };
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user