Create code-coverage plugin
In order to use this plugin, you must set the
`backstage.io/code-coverage` annotation on your entity.
```yaml
backstage.io/code-coverage: enabled
```
There's a feature to only include files that are in SCM in the coverage
report, this is helpful to not count generated files for example. To
enable this set the `backstage.io/code-coverage` annotation to
`scm-only`.
```yaml
backstage.io/code-coverage: scm-only
```
The backend plugin provides API endpoints for submitting code-coverage
reports. Currently jacoco and cobertura are supported. These reports
are normalized to a json format that is stored in the database.
```json
// curl -X POST -H "Content-Type:text/xml" -d @cobertura.xml "localhost:7000/api/code-coverage/Component/default/entity-name?coverageType=cobertura"
{
"links": [
{
"href": "http://localhost:7000/api/code-coverage/Component/default/entity-name",
"rel": "coverage"
}
]
}
```
It also provides some additional API endpoints:
* Viewing the latest report
* Viewing a more condensed history of code coverage values
* Retrieving file contents from source-control, used by the UI
Provides a graph of code coverage change over time, as well as a file
view where you can see the highlighted lines.
Co-authored-by: nissayeva <natashaaay@gmail.com>
Signed-off-by: alde <r.dybeck@gmail.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
# code-coverage
|
||||
|
||||
This is the frontend part of the code-coverage plugin. It takes care of processing various coverage formats and standardizing them into a single json format, used by the frontend.
|
||||
|
||||
## Configuring your entity
|
||||
|
||||
In order to use this plugin, you must set the `backstage.io/code-coverage` annotation.
|
||||
|
||||
```yaml
|
||||
backstage.io/code-coverage: enabled
|
||||
```
|
||||
|
||||
There's a feature to only include files that are in VCS in the coverage report, this is helpful to not count generated files for example. To enable this set the `backstage.io/code-coverage` annotation to `scm-only`.
|
||||
|
||||
```yaml
|
||||
backstage.io/code-coverage: scm-only
|
||||
```
|
||||
|
||||
Note: This requires the [`backstage.io/source-location` annotation](https://backstage.io/docs/features/software-catalog/well-known-annotations#backstageiosource-location) to be set.
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { codeCoveragePlugin, CodeCoveragePage } from '../src/plugin';
|
||||
|
||||
createDevApp()
|
||||
.registerPlugin(codeCoveragePlugin)
|
||||
.addPage({
|
||||
element: <CodeCoveragePage />,
|
||||
title: 'Root Page',
|
||||
})
|
||||
.render();
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "@backstage/plugin-code-coverage",
|
||||
"version": "0.1.1",
|
||||
"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/catalog-model": "^0.7.2",
|
||||
"@backstage/config": "^0.1.3",
|
||||
"@backstage/core": "^0.6.2",
|
||||
"@backstage/core-api": "^0.2.12",
|
||||
"@backstage/dev-utils": "^0.1.11",
|
||||
"@backstage/plugin-catalog-react": "^0.1.0",
|
||||
"@backstage/theme": "^0.2.3",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"@types/highlightjs": "^10.1.0",
|
||||
"highlight.js": "^10.6.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^15.3.3",
|
||||
"recharts": "^1.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.1",
|
||||
"@backstage/test-utils": "^0.1.7",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/recharts": "^1.8.15",
|
||||
"cross-fetch": "^3.0.6",
|
||||
"msw": "^0.21.2"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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';
|
||||
import { Config } from '@backstage/config';
|
||||
import { EntityName } from '@backstage/catalog-model';
|
||||
|
||||
export class FetchError extends Error {
|
||||
get name(): string {
|
||||
return this.constructor.name;
|
||||
}
|
||||
|
||||
static async forResponse(resp: Response): Promise<FetchError> {
|
||||
return new FetchError(
|
||||
`Request failed with status code ${
|
||||
resp.status
|
||||
}.\nReason: ${await resp.text()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type CodeCoverageApi = {
|
||||
url: string;
|
||||
getCoverageForEntity: (entity: EntityName) => Promise<any>;
|
||||
getFileContentFromEntity: (
|
||||
entity: EntityName,
|
||||
filePath: string,
|
||||
) => Promise<any>;
|
||||
getCoverageHistoryForEntity: (
|
||||
entity: EntityName,
|
||||
limit?: number,
|
||||
) => Promise<any>;
|
||||
};
|
||||
|
||||
export const codeCoverageApiRef = createApiRef<CodeCoverageApi>({
|
||||
id: 'plugin.code-coverage.service',
|
||||
description: 'Used by the code coverage plugin to make requests',
|
||||
});
|
||||
|
||||
export class CodeCoverageRestApi implements CodeCoverageApi {
|
||||
static fromConfig(config: Config) {
|
||||
return new CodeCoverageRestApi(config.getString('backend.baseUrl'));
|
||||
}
|
||||
|
||||
constructor(public url: string) {}
|
||||
|
||||
private async fetch<T = any | string>(
|
||||
input: string,
|
||||
init?: RequestInit,
|
||||
): Promise<T | string> {
|
||||
const resp = await fetch(`${this.url}${input}`, init);
|
||||
if (!resp.ok) throw await FetchError.forResponse(resp);
|
||||
if (resp.headers.get('content-type')?.includes('application/json')) {
|
||||
return await resp.json();
|
||||
}
|
||||
return await resp.text();
|
||||
}
|
||||
|
||||
async getCoverageForEntity({
|
||||
kind,
|
||||
namespace,
|
||||
name,
|
||||
}: EntityName): Promise<any> {
|
||||
return await this.fetch<any>(
|
||||
`/api/code-coverage/${kind}/${namespace}/${name}`,
|
||||
);
|
||||
}
|
||||
|
||||
async getFileContentFromEntity(
|
||||
{ kind, namespace, name }: EntityName,
|
||||
filePath: string,
|
||||
): Promise<any> {
|
||||
return await this.fetch<any>(
|
||||
`/api/code-coverage/${kind}/${namespace}/${name}/file-content?path=${filePath}`,
|
||||
);
|
||||
}
|
||||
|
||||
async getCoverageHistoryForEntity(
|
||||
{ kind, namespace, name }: EntityName,
|
||||
limit?: number,
|
||||
): Promise<any> {
|
||||
const hasValidLimit = limit && limit > 0;
|
||||
return await this.fetch<any>(
|
||||
`/api/code-coverage/${kind}/${namespace}/${name}/history${
|
||||
hasValidLimit ? `?limit=${limit}` : ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { Content, ContentHeader, Page } from '@backstage/core';
|
||||
import { CoverageHistoryChart } from '../CoverageHistoryChart';
|
||||
import { FileExplorer } from '../FileExplorer';
|
||||
|
||||
export const CodeCoveragePage = () => {
|
||||
return (
|
||||
<Page themeId="tool">
|
||||
<Content>
|
||||
<ContentHeader title="Code coverage" />
|
||||
<CoverageHistoryChart />
|
||||
<FileExplorer />
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { CodeCoveragePage } from './CodeCoveragePage';
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
MenuItem,
|
||||
Select,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import {
|
||||
LineChart,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Line,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
import { useAsync } from 'react-use';
|
||||
import { useApi } from '@backstage/core-api';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { codeCoverageApiRef } from '../../api';
|
||||
import { Progress } from '@backstage/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import TrendingDownIcon from '@material-ui/icons/TrendingDown';
|
||||
import TrendingUpIcon from '@material-ui/icons/TrendingUp';
|
||||
import TrendingFlatIcon from '@material-ui/icons/TrendingFlat';
|
||||
|
||||
type Coverage = 'line' | 'branch';
|
||||
|
||||
const getTrendIcon = (trend: number) => {
|
||||
switch (true) {
|
||||
case trend > 0:
|
||||
return <TrendingUpIcon style={{ color: 'green' }} />;
|
||||
case trend < 0:
|
||||
return <TrendingDownIcon style={{ color: 'red' }} />;
|
||||
case trend === 0:
|
||||
default:
|
||||
return <TrendingFlatIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
export const CoverageHistoryChart = () => {
|
||||
const { entity } = useEntity();
|
||||
const codeCoverageApi = useApi(codeCoverageApiRef);
|
||||
const {
|
||||
loading: loadingHistory,
|
||||
error: errorHistory,
|
||||
value: valueHistory,
|
||||
} = useAsync(
|
||||
async () =>
|
||||
await codeCoverageApi.getCoverageHistoryForEntity({
|
||||
kind: entity.kind,
|
||||
namespace: entity.metadata.namespace || 'default',
|
||||
name: entity.metadata.name,
|
||||
}),
|
||||
);
|
||||
|
||||
if (loadingHistory) {
|
||||
return <Progress />;
|
||||
} else if (errorHistory) {
|
||||
return <Alert severity="error">{errorHistory.message}</Alert>;
|
||||
}
|
||||
|
||||
if (!valueHistory.history.length) {
|
||||
return (
|
||||
<Box>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box mb={2} display="flex" justifyContent="space-between">
|
||||
<Typography variant="h5">History</Typography>
|
||||
</Box>
|
||||
No coverage history found
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const oldestCoverage = valueHistory.history[0];
|
||||
const [latestCoverage] = valueHistory.history.slice(-1);
|
||||
|
||||
const getTrendForCoverage = (type: Coverage) => {
|
||||
if (!oldestCoverage[type].percentage) {
|
||||
return 0;
|
||||
}
|
||||
return (
|
||||
((latestCoverage[type].percentage - oldestCoverage[type].percentage) /
|
||||
oldestCoverage[type].percentage) *
|
||||
100
|
||||
);
|
||||
};
|
||||
|
||||
const lineTrend = getTrendForCoverage('line');
|
||||
const branchTrend = getTrendForCoverage('branch');
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box mb={2} display="flex" justifyContent="space-between">
|
||||
<Typography variant="h5">History</Typography>
|
||||
<Select>
|
||||
<MenuItem>7</MenuItem>
|
||||
</Select>
|
||||
</Box>
|
||||
<Box px={6} display="flex">
|
||||
<Box display="flex" mr={4}>
|
||||
{getTrendIcon(lineTrend)}
|
||||
<Typography>
|
||||
Current line: {latestCoverage.line.percentage}%<br />(
|
||||
{Math.floor(lineTrend)}% change over{' '}
|
||||
{valueHistory.history.length} builds)
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display="flex">
|
||||
{getTrendIcon(branchTrend)}
|
||||
<Typography>
|
||||
Current branch: {latestCoverage.branch.percentage}%<br />(
|
||||
{Math.floor(branchTrend)}% change over{' '}
|
||||
{valueHistory.history.length} builds)
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart
|
||||
data={valueHistory.history}
|
||||
margin={{ right: 48, top: 32 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="timestamp" />
|
||||
<YAxis dataKey="line.percentage" />
|
||||
<YAxis dataKey="branch.percentage" />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="branch.percentage"
|
||||
stroke="#8884d8"
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="line.percentage"
|
||||
stroke="#82ca9d"
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { CoverageHistoryChart } from './CoverageHistoryChart';
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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, { FC } from 'react';
|
||||
|
||||
// styles
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
import { red, green } from '@material-ui/core/colors';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
lineNumberCell: {
|
||||
color: `${theme.palette.grey[500]}`,
|
||||
fontSize: '90%',
|
||||
borderRight: `1px solid ${theme.palette.grey[500]}`,
|
||||
paddingRight: '8px',
|
||||
textAlign: 'right',
|
||||
},
|
||||
hitCountCell: {
|
||||
width: '50px',
|
||||
borderRight: `1px solid ${theme.palette.grey[500]}`,
|
||||
textAlign: 'center',
|
||||
color: 'white', // need to enforce this color since it needs to stand out against colored background
|
||||
paddingLeft: `${theme.spacing(1)}`,
|
||||
paddingRight: `${theme.spacing(1)}`,
|
||||
},
|
||||
countRoundedRectangle: {
|
||||
borderRadius: '45px',
|
||||
fontSize: '90%',
|
||||
padding: '1px 3px 1px 3px',
|
||||
width: '50px',
|
||||
},
|
||||
hitCountRoundedRectangle: {
|
||||
backgroundColor: `${green[500]}`,
|
||||
},
|
||||
notHitCountRoundedRectangle: {
|
||||
backgroundColor: `${red[500]}`,
|
||||
},
|
||||
codeLine: {
|
||||
paddingLeft: `${theme.spacing(1)}`,
|
||||
whiteSpace: 'pre',
|
||||
fontSize: '90%',
|
||||
},
|
||||
hitCodeLine: {
|
||||
backgroundColor: `${green[100]}`,
|
||||
},
|
||||
notHitCodeLine: {
|
||||
backgroundColor: `${red[100]}`,
|
||||
},
|
||||
}));
|
||||
|
||||
type CodeRowProps = {
|
||||
lineNumber: number;
|
||||
lineContent: string;
|
||||
lineHits?: number | null;
|
||||
};
|
||||
const CodeRow: FC<CodeRowProps> = ({
|
||||
lineNumber,
|
||||
lineContent,
|
||||
lineHits = null,
|
||||
}: CodeRowProps) => {
|
||||
const classes = useStyles();
|
||||
const hitCountRoundedRectangleClass = [classes.countRoundedRectangle];
|
||||
const lineContentClass = [classes.codeLine];
|
||||
|
||||
let hitRoundedRectangle = null;
|
||||
if (lineHits !== null) {
|
||||
if (lineHits > 0) {
|
||||
hitCountRoundedRectangleClass.push(classes.hitCountRoundedRectangle);
|
||||
lineContentClass.push(classes.hitCodeLine);
|
||||
} else {
|
||||
hitCountRoundedRectangleClass.push(classes.notHitCountRoundedRectangle);
|
||||
lineContentClass.push(classes.notHitCodeLine);
|
||||
}
|
||||
hitRoundedRectangle = (
|
||||
<div className={hitCountRoundedRectangleClass.join(' ')}>{lineHits}</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td className={classes.lineNumberCell}>{lineNumber}</td>
|
||||
<td className={classes.hitCountCell}>{hitRoundedRectangle}</td>
|
||||
<td
|
||||
className={lineContentClass.join(' ')}
|
||||
dangerouslySetInnerHTML={{ __html: lineContent }}
|
||||
/>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeRow;
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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, { FC, useEffect } from 'react';
|
||||
import { useApi } from '@backstage/core-api';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { codeCoverageApiRef } from '../../api';
|
||||
import { Progress } from '@backstage/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { makeStyles, Paper } from '@material-ui/core';
|
||||
import { highlightLines } from './Highlighter';
|
||||
import { FileCoverage } from './FileExplorer';
|
||||
import CoverageRow from './CoverageRow';
|
||||
|
||||
type Props = {
|
||||
filename: string;
|
||||
coverage: FileCoverage;
|
||||
};
|
||||
|
||||
const getContent = async (value: any) => {
|
||||
let blob = value;
|
||||
if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
|
||||
blob = new Blob([value], { type: 'application/octet-stream' });
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
paper: {
|
||||
margin: 'auto',
|
||||
top: '2em',
|
||||
width: '80%',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
border: '2px solid #000',
|
||||
boxShadow: theme.shadows[5],
|
||||
padding: theme.spacing(2, 4, 3),
|
||||
overflow: 'scroll',
|
||||
},
|
||||
coverageFileViewTable: {
|
||||
borderSpacing: '0px',
|
||||
width: '80%',
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
type FormattedLinesProps = {
|
||||
highlightedLines: string[];
|
||||
lineHits: Record<number, number>;
|
||||
};
|
||||
const FormattedLines: FC<FormattedLinesProps> = ({
|
||||
highlightedLines,
|
||||
lineHits,
|
||||
}: FormattedLinesProps) => {
|
||||
return (
|
||||
<>
|
||||
{highlightedLines.map((lineContent, idx) => {
|
||||
const line = idx + 1;
|
||||
return (
|
||||
<CoverageRow
|
||||
key={line}
|
||||
lineNumber={line}
|
||||
lineContent={lineContent}
|
||||
lineHits={lineHits[line]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const FileContent = ({ filename, coverage }: Props) => {
|
||||
const { entity } = useEntity();
|
||||
const codeCoverageApi = useApi(codeCoverageApiRef);
|
||||
const { loading, error, value } = useAsync(
|
||||
async () =>
|
||||
await codeCoverageApi.getFileContentFromEntity(
|
||||
{
|
||||
kind: entity.kind,
|
||||
namespace: entity.metadata.namespace || 'default',
|
||||
name: entity.metadata.name,
|
||||
},
|
||||
filename,
|
||||
),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (value) getContent(value);
|
||||
}, [value]);
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
} else if (error) {
|
||||
return <Alert severity="error">{error.message}</Alert>;
|
||||
}
|
||||
|
||||
const [language] = filename.split('.').slice(-1);
|
||||
const highlightedLines = highlightLines(language, value.split('\n'));
|
||||
|
||||
// List of formatted nodes containing highlighted code
|
||||
// lineHits array where lineHits[i] is number of hits for line i + 1
|
||||
const lineHits = Object.entries(coverage.lineHits).reduce(
|
||||
(acc: Record<string, number>, next: [string, number]) => {
|
||||
acc[next[0]] = next[1];
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
return (
|
||||
<Paper variant="outlined" className={classes.paper}>
|
||||
<table className={classes.coverageFileViewTable}>
|
||||
<tbody>
|
||||
<FormattedLines
|
||||
highlightedLines={highlightedLines}
|
||||
lineHits={lineHits}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { useApi } from '@backstage/core-api';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { Progress, Table, TableColumn } from '@backstage/core';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Modal,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { codeCoverageApiRef } from '../../api';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import DescriptionIcon from '@material-ui/icons/Description';
|
||||
import { FileContent } from './FileContent';
|
||||
|
||||
export type FileCoverage = {
|
||||
filename: string;
|
||||
branchHits: { [branch: number]: number };
|
||||
lineHits: { [line: number]: number };
|
||||
};
|
||||
|
||||
type FileStructureObject = Record<string, any>;
|
||||
|
||||
type CoverageTableRow = {
|
||||
filename?: string;
|
||||
files: CoverageTableRow[];
|
||||
coverage: number;
|
||||
missing: number;
|
||||
tracked: number;
|
||||
path: string;
|
||||
tableData?: { id: number };
|
||||
};
|
||||
|
||||
const buildFileStructure = (row: CoverageTableRow) => {
|
||||
const dataGroupedByPath: FileStructureObject = row.files.reduce(
|
||||
(acc: FileStructureObject, cur: CoverageTableRow) => {
|
||||
let path = cur.filename;
|
||||
if (row.path) {
|
||||
path = path?.split(`${row.path}/`)[1];
|
||||
}
|
||||
const pathArray = path?.split('/');
|
||||
|
||||
if (!pathArray) {
|
||||
return acc;
|
||||
}
|
||||
if (!acc.hasOwnProperty(pathArray[0])) {
|
||||
acc[pathArray[0]] = [];
|
||||
}
|
||||
acc[pathArray[0]].push(cur);
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
row.files = Object.keys(dataGroupedByPath).map(pathGroup => {
|
||||
return buildFileStructure({
|
||||
path: pathGroup,
|
||||
files: dataGroupedByPath.hasOwnProperty('files')
|
||||
? dataGroupedByPath.files
|
||||
: dataGroupedByPath[pathGroup],
|
||||
coverage:
|
||||
dataGroupedByPath[pathGroup].reduce(
|
||||
(acc: number, cur: CoverageTableRow) => acc + cur.coverage,
|
||||
0,
|
||||
) / dataGroupedByPath[pathGroup].length,
|
||||
missing: dataGroupedByPath[pathGroup].reduce(
|
||||
(acc: number, cur: CoverageTableRow) => acc + cur.missing,
|
||||
0,
|
||||
),
|
||||
tracked: dataGroupedByPath[pathGroup].reduce(
|
||||
(acc: number, cur: CoverageTableRow) => acc + cur.tracked,
|
||||
0,
|
||||
),
|
||||
});
|
||||
});
|
||||
return row;
|
||||
};
|
||||
|
||||
const formatInitialData = (value: any) => {
|
||||
return buildFileStructure({
|
||||
path: '',
|
||||
coverage: value.aggregate.line.percentage,
|
||||
missing: value.aggregate.line.missed,
|
||||
tracked: value.aggregate.line.available,
|
||||
files: value.files.map((fc: FileCoverage) => {
|
||||
return {
|
||||
path: '',
|
||||
filename: fc.filename,
|
||||
coverage: Math.floor(
|
||||
(Object.values(fc.lineHits).filter((hits: number) => hits > 0)
|
||||
.length /
|
||||
Object.values(fc.lineHits).length) *
|
||||
100,
|
||||
),
|
||||
missing: Object.values(fc.lineHits).filter(hits => !hits).length,
|
||||
tracked: Object.values(fc.lineHits).length,
|
||||
};
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export const FileExplorer = () => {
|
||||
const { entity } = useEntity();
|
||||
const [curData, setCurData] = useState<CoverageTableRow | undefined>();
|
||||
const [tableData, setTableData] = useState<CoverageTableRow[] | undefined>();
|
||||
const [curPath, setCurPath] = useState('');
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [curFile, setCurFile] = useState('');
|
||||
const codeCoverageApi = useApi(codeCoverageApiRef);
|
||||
const { loading, error, value } = useAsync(
|
||||
async () =>
|
||||
await codeCoverageApi.getCoverageForEntity({
|
||||
kind: entity.kind,
|
||||
namespace: entity.metadata.namespace || 'default',
|
||||
name: entity.metadata.name,
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) return;
|
||||
const data = formatInitialData(value);
|
||||
setCurData(data);
|
||||
if (data.files) setTableData(data.files);
|
||||
}, [value]);
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
} else if (error) {
|
||||
return <Alert severity="error">{error.message}</Alert>;
|
||||
}
|
||||
|
||||
const moveDownIntoPath = (path: string) => {
|
||||
const nextPathData = tableData!.find(
|
||||
(d: CoverageTableRow) => d.path === path,
|
||||
);
|
||||
if (nextPathData && nextPathData.files) {
|
||||
setTableData(nextPathData.files);
|
||||
}
|
||||
};
|
||||
|
||||
const moveUpIntoPath = (path: string) => {
|
||||
const pathArray = path.split('/').filter(p => p.length);
|
||||
let data = curData?.files;
|
||||
pathArray.forEach(p => {
|
||||
data = data?.find(d => d.path === p)?.files;
|
||||
});
|
||||
setCurPath(path);
|
||||
setTableData(data);
|
||||
};
|
||||
|
||||
const columns: TableColumn<CoverageTableRow>[] = [
|
||||
{
|
||||
title: 'Path',
|
||||
type: 'string',
|
||||
field: 'path',
|
||||
render: (row: CoverageTableRow) => {
|
||||
if (row.files?.length) {
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={row.tableData!.id}
|
||||
style={{ color: 'lightblue', cursor: 'pointer' }}
|
||||
onKeyDown={() => {
|
||||
setCurPath(`${curPath}/${row.path}`);
|
||||
moveDownIntoPath(row.path);
|
||||
}}
|
||||
onClick={() => {
|
||||
setCurPath(`${curPath}/${row.path}`);
|
||||
moveDownIntoPath(row.path);
|
||||
}}
|
||||
>
|
||||
{row.path}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box display="flex" alignItems="center">
|
||||
{row.path}
|
||||
<Tooltip title="View file content">
|
||||
<DescriptionIcon
|
||||
fontSize="small"
|
||||
style={{ color: 'lightblue', cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setCurFile(`${curPath.slice(1)}/${row.path}`);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Coverage',
|
||||
type: 'numeric',
|
||||
field: 'coverage',
|
||||
render: (row: CoverageTableRow) => `${row.coverage}%`,
|
||||
},
|
||||
{
|
||||
title: 'Missing lines',
|
||||
type: 'numeric',
|
||||
field: 'missing',
|
||||
},
|
||||
{
|
||||
title: 'Tracked lines',
|
||||
type: 'numeric',
|
||||
field: 'tracked',
|
||||
},
|
||||
];
|
||||
|
||||
const pathArray = curPath.split('/');
|
||||
const lastPathElementIndex = pathArray.length - 1;
|
||||
const fileCoverage = value.files.find((f: FileCoverage) =>
|
||||
f.filename.endsWith(curFile),
|
||||
);
|
||||
|
||||
return (
|
||||
<Box mt={8}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box mb={2} display="flex" justifyContent="space-between">
|
||||
<Typography variant="h5">Explore Files</Typography>
|
||||
</Box>
|
||||
<Box mb={2} display="flex">
|
||||
{pathArray.map((pathElement, idx) => (
|
||||
<Fragment key={pathElement || 'root'}>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={idx}
|
||||
style={{
|
||||
color: `${idx !== lastPathElementIndex && 'lightblue'}`,
|
||||
cursor: `${idx !== lastPathElementIndex && 'pointer'}`,
|
||||
}}
|
||||
onKeyDown={() => moveUpIntoPath(pathElement)}
|
||||
onClick={() => moveUpIntoPath(pathElement)}
|
||||
>
|
||||
{pathElement || 'root'}
|
||||
</div>
|
||||
<div>{'\u00A0/\u00A0'}</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</Box>
|
||||
<Table
|
||||
emptyContent={<>No files found</>}
|
||||
data={tableData || []}
|
||||
columns={columns}
|
||||
/>
|
||||
<Modal
|
||||
open={modalOpen}
|
||||
onClick={event => event.stopPropagation()}
|
||||
onClose={() => setModalOpen(false)}
|
||||
style={{ overflow: 'scroll' }}
|
||||
>
|
||||
<FileContent filename={curFile} coverage={fileCoverage} />
|
||||
</Modal>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 'highlight.js/styles/atom-one-dark.css';
|
||||
import highlight from 'highlight.js';
|
||||
|
||||
/*
|
||||
* Given a file extension, repo name, and array of code lines, return a Promise resolving
|
||||
* to an array of formatted lines with html/css formatting.
|
||||
*
|
||||
* @param {String} fileExtension The extension of the source file
|
||||
* @param {String} repo The name of the code repository
|
||||
* @param {Array<String>} lines The source code lines
|
||||
*
|
||||
* @returns {Promise<Array<String>>} Promise of formatted lines
|
||||
*
|
||||
* @see http://highlightjs.readthedocs.io/en/latest/api.html#highlight-name-value-ignore-illegals-continuation
|
||||
*/
|
||||
export const highlightLines = (fileExtension: string, lines: Array<string>) => {
|
||||
const formattedLines: Array<string> = [];
|
||||
let state: CompiledMode | Language | undefined;
|
||||
let fileformat = fileExtension;
|
||||
if (fileExtension === 'm') {
|
||||
fileformat = 'objectivec';
|
||||
}
|
||||
if (fileExtension === 'tsx') {
|
||||
fileformat = 'typescript';
|
||||
}
|
||||
if (fileExtension === 'jsx') {
|
||||
fileformat = 'javascript';
|
||||
}
|
||||
if (fileExtension === 'kt') {
|
||||
fileformat = 'kotlin';
|
||||
}
|
||||
|
||||
lines.forEach(line => {
|
||||
const result = highlight.highlight(fileformat, line, true, state);
|
||||
state = result.top;
|
||||
formattedLines.push(result.value);
|
||||
});
|
||||
return formattedLines;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { FileExplorer } from './FileExplorer';
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { Entity } from '@backstage/catalog-model';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import { MissingAnnotationEmptyState } from '@backstage/core';
|
||||
import { CodeCoveragePage } from './CodeCoveragePage';
|
||||
|
||||
export const isCodeCoverageAvailable = (entity: Entity) =>
|
||||
Boolean(entity.metadata.annotations?.['backstage.io/code-coverage']);
|
||||
|
||||
export const Router = () => {
|
||||
const { entity } = useEntity();
|
||||
|
||||
if (!isCodeCoverageAvailable(entity)) {
|
||||
return (
|
||||
<MissingAnnotationEmptyState annotation="backstage.io/code-coverage" />
|
||||
);
|
||||
}
|
||||
return <CodeCoveragePage />;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { createDevApp } from '@backstage/dev-utils';
|
||||
import { codeCoveragePlugin } from '../plugin';
|
||||
import { codeCoverageApiRef, CodeCoverageRestApi } from '../api';
|
||||
|
||||
createDevApp()
|
||||
.registerPlugin(codeCoveragePlugin)
|
||||
.registerApi({
|
||||
api: codeCoverageApiRef,
|
||||
deps: {},
|
||||
factory: () => new CodeCoverageRestApi('http://localhost:3000'),
|
||||
})
|
||||
.render();
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { codeCoveragePlugin, EntityCodeCoverageContent } from './plugin';
|
||||
export {
|
||||
Router,
|
||||
isCodeCoverageAvailable,
|
||||
isCodeCoverageAvailable as isPluginApplicableToEntity,
|
||||
} from './components/Router';
|
||||
|
||||
// export { createBackendRouter } from './backend/router';
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 { codeCoveragePlugin } from './plugin';
|
||||
|
||||
describe('code-coverage', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(codeCoveragePlugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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 {
|
||||
configApiRef,
|
||||
createApiFactory,
|
||||
createPlugin,
|
||||
createRoutableExtension,
|
||||
} from '@backstage/core';
|
||||
import { codeCoverageApiRef, CodeCoverageRestApi } from './api';
|
||||
|
||||
import { rootRouteRef } from './routes';
|
||||
|
||||
export const codeCoveragePlugin = createPlugin({
|
||||
id: 'code-coverage',
|
||||
routes: {
|
||||
root: rootRouteRef,
|
||||
},
|
||||
apis: [
|
||||
createApiFactory({
|
||||
api: codeCoverageApiRef,
|
||||
deps: { configApi: configApiRef },
|
||||
factory: ({ configApi }) => CodeCoverageRestApi.fromConfig(configApi),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export const EntityCodeCoverageContent = codeCoveragePlugin.provide(
|
||||
createRoutableExtension({
|
||||
component: () => import('./components/Router').then(m => m.Router),
|
||||
mountPoint: rootRouteRef,
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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';
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
title: 'code-coverage',
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* 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';
|
||||
Reference in New Issue
Block a user