Introduce scorecards

Signed-off-by: irma12 <irma@roadie.io>
This commit is contained in:
irma12
2021-11-17 14:28:54 +01:00
parent 33c7d6522a
commit dc62dcab12
25 changed files with 714 additions and 5 deletions
+11
View File
@@ -0,0 +1,11 @@
---
'example-app': patch
'example-backend': patch
'@backstage/plugin-scorecards': patch
'@backstage/plugin-tech-insights-backend': patch
'@backstage/plugin-tech-insights-common': patch
---
Removed unecessary check in @backstage/plugin-tech-insights-backend
Added new property 'result' in every CheckResult in @backstage/plugin-tech-insights-common
+1
View File
@@ -36,6 +36,7 @@
"@backstage/plugin-pagerduty": "0.3.17",
"@backstage/plugin-rollbar": "^0.3.18",
"@backstage/plugin-scaffolder": "^0.11.8",
"@backstage/plugin-scorecards": "^0.1.0",
"@backstage/plugin-search": "^0.4.15",
"@backstage/plugin-sentry": "^0.3.26",
"@backstage/plugin-shortcuts": "^0.1.12",
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+52
View File
@@ -0,0 +1,52 @@
# Scorecards
This plugin represents scorecards which are rendered as a part of the default Backstage Tech Insights feature.
It provides UI for the scorecards as well as a check overview.
## Installation
### Install the plugin
```bash
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-scorecards
```
### Adding the plugin to your `packages/app`
```tsx
// In packages/app/src/App.tsx
import { EntityScorecardContent } from '@backstage/plugin-scorecards';
<Route path="/scorecards" element={<EntityScorecardContent />} />;
```
3. Add Scorecards overview page to the EntityPage:
```tsx
// packages/app/src/components/catalog/EntityPage.tsx
import { EntityScorecardContent } from '@backstage/plugin-scorecards';
const serviceEntityPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/ci-cd" title="CI/CD">
{cicdContent}
</EntityLayout.Route>
...
<EntityLayout.Route path="/scorecards" title="Scorecards">
<EntityScorecardContent />
</EntityLayout.Route>
...
</EntityLayoutWrapper>
);
```
## Links
- [The Backstage homepage](https://backstage.io)
+27
View File
@@ -0,0 +1,27 @@
## API Report File for "@backstage/plugin-scorecards"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="react" />
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { RouteRef } from '@backstage/core-plugin-api';
// Warning: (ae-missing-release-tag) "EntityScorecardContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const EntityScorecardContent: () => JSX.Element;
// Warning: (ae-missing-release-tag) "scorecardsPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const scorecardsPlugin: BackstagePlugin<
{
root: RouteRef<undefined>;
},
{}
>;
// (No @packageDocumentation comment for this package)
```
+27
View File
@@ -0,0 +1,27 @@
/*
* Copyright 2021 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 { createDevApp } from '@backstage/dev-utils';
import { scorecardsPlugin, EntityScorecardContent } from '../src/plugin';
createDevApp()
.registerPlugin(scorecardsPlugin)
.addPage({
element: <EntityScorecardContent />,
title: 'Root Page',
path: '/scorecards',
})
.render();
+52
View File
@@ -0,0 +1,52 @@
{
"name": "@backstage/plugin-scorecards",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"scripts": {
"build": "backstage-cli plugin:build",
"start": "backstage-cli plugin:serve",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"diff": "backstage-cli plugin:diff",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/core-components": "^0.7.2",
"@backstage/core-plugin-api": "^0.1.12",
"@backstage/theme": "^0.2.12",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-use": "^17.2.4",
"react-router-dom": "6.0.0-beta.0",
"@backstage/plugin-catalog-react": "^0.6.2",
"@backstage/plugin-tech-insights-common": "^0.1.0"
},
"devDependencies": {
"@backstage/cli": "^0.8.1",
"@backstage/core-app-api": "^0.1.19",
"@backstage/dev-utils": "^0.2.12",
"@backstage/test-utils": "^0.1.20",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^13.1.8",
"@types/jest": "*",
"@types/node": "*",
"msw": "^0.35.0",
"cross-fetch": "^3.0.6"
},
"files": [
"dist"
]
}
@@ -0,0 +1,44 @@
/*
* Copyright 2021 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 { createApiRef } from '@backstage/core-plugin-api';
import { CheckResult } from '@backstage/plugin-tech-insights-common';
import { Check } from './types';
import { CheckResultRenderer } from '../components/CheckResultRenderer';
export const scorecardsApiRef = createApiRef<ScorecardsApi>({
id: 'plugin.scorecards.service',
description: 'Used by the scorecards plugin to make requests',
});
export interface ScorecardsApi {
getScorecardsDefinition: (
type: string,
value: CheckResult[],
) => CheckResultRenderer | undefined;
getAllChecks(): Promise<Check[]>;
runChecks({
namespace,
kind,
name,
checks,
}: {
namespace: string;
kind: string;
name: string;
checks?: Check[];
}): Promise<CheckResult[]>;
}
@@ -0,0 +1,91 @@
/*
* Copyright 2021 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 { ScorecardsApi } from './ScorecardsApi';
import { CheckResult } from '@backstage/plugin-tech-insights-common';
import { Check } from './types';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import {
CheckResultRenderer,
defaultCheckResultRenderers,
} from '../components/CheckResultRenderer';
export type Options = {
discoveryApi: DiscoveryApi;
proxyPath?: string;
};
export class ScorecardsClient implements ScorecardsApi {
private readonly discoveryApi: DiscoveryApi;
private baseUrl: string = '';
constructor(options: Options) {
this.discoveryApi = options.discoveryApi;
}
getScorecardsDefinition(
type: string,
value: CheckResult[],
): CheckResultRenderer | undefined {
const resultRenderers = defaultCheckResultRenderers(value);
return resultRenderers.find(d => d.type === type);
}
getBaseUrl: () => Promise<string> = async () => {
if (!this.baseUrl) {
this.baseUrl = await this.discoveryApi.getBaseUrl('tech-insights');
}
return this.baseUrl;
};
async getAllChecks(): Promise<Check[]> {
const url = await this.getBaseUrl();
const allChecks = await fetch(`${url}/checks`);
const payload = await allChecks.json();
if (!allChecks.ok) {
throw new Error(payload.errors[0]);
}
return payload;
}
async runChecks({
namespace,
kind,
name,
checks,
}: {
namespace: string;
kind: string;
name: string;
checks: Check[];
}): Promise<CheckResult[]> {
const url = await this.getBaseUrl();
const allChecks = checks ? checks : await this.getAllChecks();
const checkIds = allChecks.map((check: Check) => check.id);
const response = await fetch(
`${url}/checks/run/${namespace}/${kind}/${name}`,
{
method: 'POST',
body: JSON.stringify({ checks: checkIds }),
headers: {
'Content-Type': 'application/json',
},
},
);
const result = await response.json();
return result;
}
}
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2021 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 './ScorecardsApi';
export * from './ScorecardsClient';
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2021 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 type Check = {
id: string;
type: string;
name: string;
description: string;
factIds: string[];
};
@@ -0,0 +1,62 @@
/*
* Copyright 2021 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 { makeStyles, List, ListItem, ListItemText } from '@material-ui/core';
import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline';
import { CheckResult } from '@backstage/plugin-tech-insights-common';
const useStyles = makeStyles(() => ({
listItemText: {
paddingRight: '1rem',
flex: '0 1 auto',
},
icon: {
marginLeft: 'auto',
},
}));
type Prop = {
checkResult: CheckResult[];
};
const renderResult = (classes: any, result: Boolean) => {
return result ? (
<CheckCircleOutline className={classes.icon} color="primary" />
) : (
<ErrorOutlineIcon className={classes.icon} color="error" />
);
};
export const BooleanCheck = ({ checkResult }: Prop) => {
const classes = useStyles();
return (
<List>
{checkResult!.map(check => (
<ListItem>
<ListItemText
primary={check.check.name}
secondary={check.check.description}
className={classes.listItemText}
/>
{renderResult(classes, check.result)}
</ListItem>
))}
</List>
);
};
@@ -0,0 +1,17 @@
/*
* Copyright 2021 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 { BooleanCheck } from './BooleanCheck';
@@ -0,0 +1,40 @@
/*
* Copyright 2020 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 { CheckResult } from '@backstage/plugin-tech-insights-common';
import React from 'react';
import { BooleanCheck } from './BooleanCheck';
export type CheckResultRenderer = {
type: string;
title: string;
description: string;
component: React.ReactElement;
};
export function defaultCheckResultRenderers(
value: CheckResult[],
): CheckResultRenderer[] {
return [
{
type: 'json-rules-engine',
title: 'Boolean scorecard',
description:
'This card represents an overview of default boolean checks:',
component: <BooleanCheck checkResult={value} />,
},
];
}
@@ -0,0 +1,66 @@
/*
* Copyright 2021 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 { makeStyles, Grid, Typography } from '@material-ui/core';
import { useApi } from '@backstage/core-plugin-api';
import { Content, Page, InfoCard } from '@backstage/core-components';
import { CheckResult } from '@backstage/plugin-tech-insights-common';
import { scorecardsApiRef } from '../../api/ScorecardsApi';
const useStyles = makeStyles(() => ({
contentScorecards: {
paddingLeft: 0,
paddingRight: 0,
},
subheader: {
fontWeight: 'bold',
},
}));
type Checks = {
checks: CheckResult[];
};
export const ChecksOverview = ({ checks }: Checks) => {
const classes = useStyles();
const api = useApi(scorecardsApiRef);
const checkRenderType = api.getScorecardsDefinition(
checks[0].check.type,
checks,
);
if (checkRenderType) {
return (
<Page themeId="home">
<Content className={classes.contentScorecards}>
<Grid item xs={12}>
<InfoCard title={checkRenderType.title}>
<Typography className={classes.subheader} variant="body1">
{checkRenderType.description}
</Typography>
<Grid item xs={11}>
{checkRenderType.component}
</Grid>
</InfoCard>
</Grid>
</Content>
</Page>
);
}
return <></>;
};
@@ -0,0 +1,41 @@
/*
* Copyright 2021 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 { useParams } from 'react-router-dom';
import { useAsync } from 'react-use';
import { Progress } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { ChecksOverview } from './ChecksOverview';
import Alert from '@material-ui/lab/Alert';
import { scorecardsApiRef } from '../../api/ScorecardsApi';
export const ScorecardsOverview = () => {
const api = useApi(scorecardsApiRef);
const { namespace, kind, name } = useParams();
const { value, loading, error } = useAsync(
async () => await api.runChecks({ namespace, kind, name }),
);
if (loading) {
return <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
return <ChecksOverview checks={value || []} />;
};
@@ -0,0 +1,17 @@
/*
* Copyright 2021 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 { ScorecardsOverview } from './ScorecardsOverview';
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright 2021 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 { scorecardsPlugin, EntityScorecardContent } from './plugin';
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2021 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 { scorecardsPlugin } from './plugin';
describe('scorecards', () => {
it('should export plugin', () => {
expect(scorecardsPlugin).toBeDefined();
});
});
+47
View File
@@ -0,0 +1,47 @@
/*
* Copyright 2021 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 {
createPlugin,
createRoutableExtension,
createApiFactory,
discoveryApiRef,
} from '@backstage/core-plugin-api';
import { rootRouteRef } from './routes';
import { scorecardsApiRef } from './api/ScorecardsApi';
import { ScorecardsClient } from './api';
export const scorecardsPlugin = createPlugin({
id: 'scorecards',
apis: [
createApiFactory({
api: scorecardsApiRef,
deps: { discoveryApi: discoveryApiRef },
factory: ({ discoveryApi }) => new ScorecardsClient({ discoveryApi }),
}),
],
routes: {
root: rootRouteRef,
},
});
export const EntityScorecardContent = scorecardsPlugin.provide(
createRoutableExtension({
name: 'EntityScorecardContent',
component: () =>
import('./components/ScorecardsOverview').then(m => m.ScorecardsOverview),
mountPoint: rootRouteRef,
}),
);
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright 2021 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 { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
title: 'scorecards',
});
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2021 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';
@@ -92,11 +92,6 @@ export async function createRouter<
router.post('/checks/run/:namespace/:kind/:name', async (req, res) => {
const { namespace, kind, name } = req.params;
try {
if (!('checks' in req.body)) {
return res.status(422).send({
message: 'Failed to get checks from request.',
});
}
const { checks }: { checks: string[] } = req.body;
const entityTriplet = stringifyEntityRef({ namespace, kind, name });
const checkResult = await factChecker.runChecks(entityTriplet, checks);
@@ -25,6 +25,7 @@ export interface CheckResponse {
export type CheckResult = {
facts: FactResponse;
check: CheckResponse;
result: any;
};
// @public
@@ -112,6 +112,7 @@ export type FactResponse = {
export type CheckResult = {
facts: FactResponse;
check: CheckResponse;
result: any;
};
/**