feat: add sonarqube plugin (#3160)

This commit is contained in:
Dominik Henneke
2020-10-30 09:52:39 +01:00
committed by GitHub
parent c0d5242a0d
commit da0a79c3b8
23 changed files with 1214 additions and 0 deletions
@@ -173,6 +173,22 @@ The value of these annotations are the corresponding attributes that were found
when ingestion the entity from LDAP. Not all of them may be present, depending
on what attributes that the server presented at ingestion time.
### sonarqube.org/project-key
```yaml
# Example:
metadata:
annotations:
sonarqube.org/project-key: pump-station
```
The value of this annotation is the project key of a
[SonarQube](https://sonarqube.org) or [SonarCloud](https://sonarcloud.io)
project within your organization.
Specifying this annotation may enable SonarQube related features in Backstage
for that entity.
## Deprecated Annotations
The following annotations are deprecated, and only listed here to aid in
+9
View File
@@ -0,0 +1,9 @@
---
title: SonarQube
author: SDA SE
authorUrl: https://sda.se/
category: Quality
description: Components to display code quality metrics from SonarCloud and SonarQube.
documentation: https://github.com/spotify/backstage/blob/master/plugins/sonarqube/README.md
iconUrl: img/sonarqube-icon.svg
npmPackageName: '@backstage/plugin-sonarqube'
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<style type="text/css">
.st0{fill:#549DD0;}
.st1{clip-path:url(#SVGID_2_);fill:#FFFFFF;}
.st2{clip-path:url(#SVGID_4_);fill:#FFFFFF;}
.st3{clip-path:url(#SVGID_6_);fill:#FFFFFF;}
</style>
<rect class="st0" width="512" height="512"/>
<g>
<g>
<defs>
<rect id="SVGID_1_" y="0" width="512" height="512"/>
</defs>
<clipPath id="SVGID_2_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<path class="st1" d="M408.8,448.1h-22.3c0-179.4-148-325.3-329.9-325.3v-22.3C250.8,100.5,408.8,256.4,408.8,448.1"/>
</g>
<g>
<defs>
<rect id="SVGID_3_" y="0" width="512" height="512"/>
</defs>
<clipPath id="SVGID_4_">
<use xlink:href="#SVGID_3_" style="overflow:visible;"/>
</clipPath>
<path class="st2" d="M424.2,328.5C397.4,215.9,306.3,122,192,89.2l5.1-17.8C317.7,106,414,205.2,442.2,324.2L424.2,328.5z"/>
</g>
<g>
<defs>
<rect id="SVGID_5_" y="0" width="512" height="512"/>
</defs>
<clipPath id="SVGID_6_">
<use xlink:href="#SVGID_5_" style="overflow:visible;"/>
</clipPath>
<path class="st3" d="M441.3,222.9c-27.6-60.1-74.5-112.5-132.2-147.5l7.7-12.7c60.2,36.6,109.2,91.3,138,154L441.3,222.9z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+95
View File
@@ -0,0 +1,95 @@
# SonarQube Plugin
The SonarQube Plugin displays code statistics from [SonarCloud](https://sonarcloud.io) or [SonarQube](https://sonarqube.com).
![Sonar Card](./docs/sonar-card.png)
## Getting Started
1. Install the SonarQube Plugin:
```bash
yarn add @backstage/plugin-sonarqube
```
2. Add plugin to the app:
```js
// packages/app/src/plugins.ts
export { plugin as SonarQube } from '@backstage/plugin-sonarqube';
```
3. Add the `SonarQubeCard` to the EntityPage:
```jsx
// packages/app/src/components/catalog/EntityPage.tsx
import { SonarQubeCard } from '@backstage/plugin-sonarqube';
const OverviewContent = ({ entity }: { entity: Entity }) => (
<Grid container spacing={3} alignItems="stretch">
// ...
<Grid item xs={12} sm={6} md={4}>
<SonarQubeCard entity={entity} />
</Grid>
// ...
</Grid>
);
```
4. Add the proxy config:
**SonarCloud**
```yaml
// app-config.yaml
proxy:
'/sonarqube':
target: https://sonarcloud.io/api
allowedMethods: ['GET']
headers:
Authorization:
# Content: 'Basic base64("<api-key>:")' <-- note the trailing ':'
# Example: Basic bXktYXBpLWtleTo=
$env: SONARQUBE_AUTH_HEADER
```
**SonarQube**
```yaml
// app-config.yaml
proxy:
'/sonarqube':
target: https://your.sonarqube.instance.com/api
allowedMethods: ['GET']
headers:
Authorization:
# Content: 'Basic base64("<api-key>:")' <-- note the trailing ':'
# Example: Basic bXktYXBpLWtleTo=
$env: SONARQUBE_AUTH_HEADER
sonarQube:
baseUrl: https://your.sonarqube.instance.com
```
5. Get and provide `SONARQUBE_AUTH_HEADER` as env variable (https://sonarcloud.io/account/security or https://docs.sonarqube.org/latest/user-guide/user-token/)
6. Add the `sonarqube.org/project-key` annotation to your component-info.yaml file:
```yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage
description: |
Backstage is an open-source developer portal that puts the developer experience first.
annotations:
sonarqube.org/project-key: YOUR_PROJECT_KEY
spec:
type: library
owner: Spotify
lifecycle: experimental
```
+20
View File
@@ -0,0 +1,20 @@
/*
* 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 { plugin } from '../src/plugin';
createDevApp().registerPlugin(plugin).render();
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

+52
View File
@@ -0,0 +1,52 @@
{
"name": "@backstage/plugin-sonarqube",
"version": "0.1.1-alpha.26",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": false,
"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.1.1-alpha.26",
"@backstage/core": "^0.1.1-alpha.26",
"@backstage/theme": "^0.1.1-alpha.26",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@material-ui/styles": "^4.10.0",
"cross-fetch": "^3.0.6",
"rc-progress": "^3.0.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-use": "^15.3.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.26",
"@backstage/dev-utils": "^0.1.1-alpha.26",
"@backstage/test-utils": "^0.1.1-alpha.26",
"@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",
"cross-fetch": "^3.0.6",
"msw": "^0.21.2"
},
"files": [
"dist"
]
}
+161
View File
@@ -0,0 +1,161 @@
/*
* 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 { UrlPatternDiscovery } from '@backstage/core';
import { msw } from '@backstage/test-utils';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { FindingSummary, SonarQubeApi } from './index';
import { ComponentWrapper, MeasuresWrapper } from './types';
const server = setupServer();
describe('SonarQubeApi', () => {
msw.setupDefaultHandlers(server);
const mockBaseUrl = 'http://backstage:9191/api/proxy';
const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl);
const setupHandlers = () => {
server.use(
rest.get(`${mockBaseUrl}/sonarqube/components/show`, (req, res, ctx) => {
expect(req.url.searchParams.toString()).toBe('component=our-service');
return res(
ctx.json({
component: {
analysisDate: '2020-01-01T00:00:00Z',
},
} as ComponentWrapper),
);
}),
);
server.use(
rest.get(`${mockBaseUrl}/sonarqube/measures/search`, (req, res, ctx) => {
expect(req.url.searchParams.toString()).toBe(
'projectKeys=our-service&metricKeys=alert_status%2Cbugs%2Creliability_rating%2Cvulnerabilities%2Csecurity_rating%2Ccode_smells%2Csqale_rating%2Ccoverage%2Cduplicated_lines_density',
);
return res(
ctx.json({
measures: [
{
metric: 'alert_status',
value: 'OK',
component: 'our-service',
},
{
metric: 'alert_status',
value: 'ERROR',
component: 'other-service',
},
{
metric: 'bugs',
value: '2',
component: 'our-service',
},
{
metric: 'reliability_rating',
value: '3.0',
component: 'our-service',
},
{
metric: 'vulnerabilities',
value: '4',
component: 'our-service',
},
{
metric: 'security_rating',
value: '1.0',
component: 'our-service',
},
{
metric: 'code_smells',
value: '100',
component: 'our-service',
},
{
metric: 'sqale_rating',
value: '2.0',
component: 'our-service',
},
{
metric: 'coverage',
value: '55.5',
component: 'our-service',
},
{
metric: 'duplicated_lines_density',
value: '1.0',
component: 'our-service',
},
],
} as MeasuresWrapper),
);
}),
);
};
it('should report finding summary', async () => {
setupHandlers();
const client = new SonarQubeApi({ discoveryApi });
const summary = await client.getFindingSummary('our-service');
expect(summary).toEqual({
lastAnalysis: '2020-01-01T00:00:00Z',
metrics: {
alert_status: 'OK',
bugs: '2',
reliability_rating: '3.0',
vulnerabilities: '4',
security_rating: '1.0',
code_smells: '100',
sqale_rating: '2.0',
coverage: '55.5',
duplicated_lines_density: '1.0',
},
projectUrl: 'https://sonarcloud.io/dashboard?id=our-service',
} as FindingSummary);
});
it('should report finding summary (custom baseUrl)', async () => {
setupHandlers();
const client = new SonarQubeApi({
discoveryApi,
baseUrl: 'http://a.instance.local',
});
const summary = await client.getFindingSummary('our-service');
expect(summary).toEqual({
lastAnalysis: '2020-01-01T00:00:00Z',
metrics: {
alert_status: 'OK',
bugs: '2',
reliability_rating: '3.0',
vulnerabilities: '4',
security_rating: '1.0',
code_smells: '100',
sqale_rating: '2.0',
coverage: '55.5',
duplicated_lines_density: '1.0',
},
projectUrl: 'http://a.instance.local/dashboard?id=our-service',
} as FindingSummary);
});
});
+110
View File
@@ -0,0 +1,110 @@
/*
* 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, DiscoveryApi } from '@backstage/core';
import fetch from 'cross-fetch';
import { ComponentWrapper, MeasuresWrapper, MetricKey } from './types';
/**
* Define a type to make sure that all metrics are used
*/
type Metrics = {
[key in MetricKey]: string | undefined;
};
export interface FindingSummary {
lastAnalysis: string;
metrics: Metrics;
projectUrl: string;
}
export const sonarQubeApiRef = createApiRef<SonarQubeApi>({
id: 'plugin.sonarqube.service',
description: 'Used by the SonarQube plugin to make requests',
});
export class SonarQubeApi {
discoveryApi: DiscoveryApi;
baseUrl: string;
constructor({
discoveryApi,
baseUrl = 'https://sonarcloud.io/',
}: {
discoveryApi: DiscoveryApi;
baseUrl?: string;
}) {
this.discoveryApi = discoveryApi;
this.baseUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
}
private async callApi<T>(path: string): Promise<T | undefined> {
const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/sonarqube`;
const response = await fetch(`${apiUrl}/${path}`);
if (response.status === 200) {
return (await response.json()) as T;
}
return undefined;
}
async getFindingSummary(
componentKey?: string,
): Promise<FindingSummary | undefined> {
if (!componentKey) {
return undefined;
}
const component = await this.callApi<ComponentWrapper>(
`components/show?component=${componentKey}`,
);
if (!component) {
return undefined;
}
const metrics: Metrics = {
alert_status: undefined,
bugs: undefined,
reliability_rating: undefined,
vulnerabilities: undefined,
security_rating: undefined,
code_smells: undefined,
sqale_rating: undefined,
coverage: undefined,
duplicated_lines_density: undefined,
};
const measures = await this.callApi<MeasuresWrapper>(
`measures/search?projectKeys=${componentKey}&metricKeys=${Object.keys(
metrics,
).join(',')}`,
);
if (!measures) {
return undefined;
}
measures.measures
.filter(m => m.component === componentKey)
.forEach(m => {
metrics[m.metric] = m.value;
});
return {
lastAnalysis: component.component.analysisDate,
metrics,
projectUrl: `${this.baseUrl}dashboard?id=${componentKey}`,
};
}
}
+55
View File
@@ -0,0 +1,55 @@
/*
* 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 interface ComponentWrapper {
component: Component;
}
export interface Component {
analysisDate: string;
}
export interface MeasuresWrapper {
measures: Measure[];
}
export type MetricKey =
// alert status
| 'alert_status'
// bugs and rating (-> reliability)
| 'bugs'
| 'reliability_rating'
// vulnerabilities and rating (-> security)
| 'vulnerabilities'
| 'security_rating'
// code smells and rating (-> maintainability)
| 'code_smells'
| 'sqale_rating'
// coverage
| 'coverage'
// duplicated lines
| 'duplicated_lines_density';
export interface Measure {
metric: MetricKey;
value: string;
component: string;
}
@@ -0,0 +1,45 @@
/*
* 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 { BackstageTheme } from '@backstage/theme';
import { makeStyles } from '@material-ui/core/styles';
import { useTheme } from '@material-ui/styles';
import { Circle } from 'rc-progress';
import React from 'react';
const useStyles = makeStyles((theme: BackstageTheme) => ({
root: {
height: theme.spacing(3),
width: theme.spacing(3),
},
}));
export const Percentage = ({ value }: { value?: string }) => {
const classes = useStyles();
const theme = useTheme<BackstageTheme>();
return (
<Circle
strokeLinecap="butt"
percent={+(value || 0)}
strokeWidth={16}
strokeColor={theme.palette.status.ok}
trailColor={theme.palette.status.error}
trailWidth={16}
className={classes.root}
/>
);
};
@@ -0,0 +1,112 @@
/*
* 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 { BackstageTheme } from '@backstage/theme';
import { Avatar } from '@material-ui/core';
import { lighten, makeStyles } from '@material-ui/core/styles';
import { CSSProperties } from '@material-ui/styles';
import React, { useMemo } from 'react';
const useStyles = makeStyles((theme: BackstageTheme) => {
const commonCardRating: CSSProperties = {
height: theme.spacing(3),
width: theme.spacing(3),
color: theme.palette.common.white,
};
return {
ratingDefault: {
...commonCardRating,
background: theme.palette.status.aborted,
},
ratingA: {
...commonCardRating,
background: theme.palette.status.ok,
},
ratingB: {
...commonCardRating,
background: lighten(theme.palette.status.ok, 0.5),
},
ratingC: {
...commonCardRating,
background: theme.palette.status.pending,
},
ratingD: {
...commonCardRating,
background: theme.palette.status.warning,
},
ratingE: {
...commonCardRating,
background: theme.palette.error.main,
},
};
});
export const Rating = ({
rating,
hideValue,
}: {
rating?: string;
hideValue?: boolean;
}) => {
const classes = useStyles();
const ratingProp = useMemo(() => {
switch (rating) {
case '1.0':
return {
name: 'A',
className: classes.ratingA,
};
case '2.0':
return {
name: 'B',
className: classes.ratingB,
};
case '3.0':
return {
name: 'C',
className: classes.ratingC,
};
case '4.0':
return {
name: 'D',
className: classes.ratingD,
};
case '5.0':
return {
name: 'E',
className: classes.ratingE,
};
default:
return {
name: '',
className: classes.ratingDefault,
};
}
}, [classes, rating]);
return (
<Avatar className={ratingProp.className}>
{!hideValue && ratingProp.name}
</Avatar>
);
};
@@ -0,0 +1,78 @@
/*
* 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 { Grid, Typography } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import React, { ReactNode } from 'react';
const useStyles = makeStyles(theme => {
return {
root: {
margin: theme.spacing(1, 0),
},
upper: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
cardTitle: {
textAlign: 'center',
},
wrapIcon: {
display: 'inline-flex',
verticalAlign: 'baseline',
},
left: {
display: 'flex',
},
right: {
display: 'flex',
marginLeft: theme.spacing(0.5),
},
};
});
export const RatingCard = ({
leftSlot,
rightSlot,
title,
titleIcon,
}: {
leftSlot: ReactNode;
rightSlot: ReactNode;
title: string;
titleIcon?: ReactNode;
}) => {
const classes = useStyles();
return (
<Grid item className={classes.root}>
<Grid item className={classes.upper}>
<Grid item className={classes.left}>
{leftSlot}
</Grid>
<Grid item className={classes.right}>
{rightSlot}
</Grid>
</Grid>
<Grid item className={classes.cardTitle}>
<Typography variant="body1" className={classes.wrapIcon}>
{titleIcon} {title}
</Typography>
</Grid>
</Grid>
);
};
@@ -0,0 +1,230 @@
/*
* 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 { Entity } from '@backstage/catalog-model';
import {
EmptyState,
InfoCard,
MissingAnnotationEmptyState,
Progress,
useApi,
} from '@backstage/core';
import { Chip, Grid } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import BugReport from '@material-ui/icons/BugReport';
import LockOpen from '@material-ui/icons/LockOpen';
import SentimentVeryDissatisfied from '@material-ui/icons/SentimentVeryDissatisfied';
import React, { useMemo } from 'react';
import { useAsync } from 'react-use';
import { sonarQubeApiRef } from '../../api';
import {
SONARQUBE_PROJECT_KEY_ANNOTATION,
useProjectKey,
} from '../useProjectKey';
import { Percentage } from './Percentage';
import { Rating } from './Rating';
import { RatingCard } from './RatingCard';
import { Value } from './Value';
const useStyles = makeStyles(theme => ({
badgeLabel: {
color: theme.palette.common.white,
},
badgeError: {
margin: 0,
backgroundColor: theme.palette.error.main,
},
badgeSuccess: {
margin: 0,
backgroundColor: theme.palette.success.main,
},
header: {
padding: theme.spacing(2, 2, 2, 2.5),
},
action: {
margin: 0,
},
lastAnalyzed: {
color: theme.palette.text.secondary,
},
disabled: {
backgroundColor: theme.palette.background.default,
},
}));
interface DuplicationRating {
greaterThan: number;
rating: '1.0' | '2.0' | '3.0' | '4.0' | '5.0';
}
const defaultDuplicationRatings: DuplicationRating[] = [
{ greaterThan: 0, rating: '1.0' },
{ greaterThan: 3, rating: '2.0' },
{ greaterThan: 5, rating: '3.0' },
{ greaterThan: 10, rating: '4.0' },
{ greaterThan: 20, rating: '5.0' },
];
export const SonarQubeCard = ({
entity,
variant = 'gridItem',
duplicationRatings = defaultDuplicationRatings,
}: {
entity: Entity;
variant?: string;
duplicationRatings?: DuplicationRating[];
}) => {
const sonarQubeApi = useApi(sonarQubeApiRef);
const projectTitle = useProjectKey(entity);
const { value, loading } = useAsync(
async () => sonarQubeApi.getFindingSummary(projectTitle),
[sonarQubeApi, projectTitle],
);
const deepLink =
!loading && value
? {
title: 'View more',
link: value.projectUrl,
}
: undefined;
const gatePassed = value && value.metrics.alert_status === 'OK';
const classes = useStyles();
const qualityBadge = !loading && value && (
<Chip
label={gatePassed ? 'Gate Passed' : 'Gate Failed'}
classes={{
root: gatePassed ? classes.badgeSuccess : classes.badgeError,
label: classes.badgeLabel,
}}
/>
);
const duplicationRating = useMemo(() => {
if (loading || !value || !value.metrics.duplicated_lines_density) {
return '';
}
let rating = '';
for (const r of duplicationRatings) {
if (+value.metrics.duplicated_lines_density >= r.greaterThan) {
rating = r.rating;
}
}
return rating;
}, [loading, value, duplicationRatings]);
return (
<InfoCard
title="Code Quality"
deepLink={deepLink}
variant={variant}
headerProps={{
action: qualityBadge,
classes: {
root: classes.header,
action: classes.action,
},
}}
className={
!loading && (!projectTitle || !value) ? classes.disabled : undefined
}
>
{loading && <Progress />}
{!loading && !projectTitle && (
<MissingAnnotationEmptyState
annotation={SONARQUBE_PROJECT_KEY_ANNOTATION}
/>
)}
{!loading && projectTitle && !value && (
<EmptyState
missing="info"
title="No information to display"
description={`There is no SonarQube project with key '${projectTitle}'.`}
/>
)}
{!loading && value && (
<>
<Grid
item
container
direction="column"
justify="space-between"
alignItems="center"
style={{ height: '100%' }}
spacing={0}
>
<Grid item container justify="space-around">
<RatingCard
titleIcon={<BugReport />}
title="Bugs"
leftSlot={<Value value={value.metrics.bugs} />}
rightSlot={<Rating rating={value.metrics.reliability_rating} />}
/>
<RatingCard
titleIcon={<LockOpen />}
title="Vulnerabilities"
leftSlot={<Value value={value.metrics.vulnerabilities} />}
rightSlot={<Rating rating={value.metrics.security_rating} />}
/>
<RatingCard
titleIcon={<SentimentVeryDissatisfied />}
title="Code Smells"
leftSlot={<Value value={value.metrics.code_smells} />}
rightSlot={<Rating rating={value.metrics.sqale_rating} />}
/>
<div style={{ width: '100%' }} />
<RatingCard
title="Coverage"
leftSlot={<Percentage value={value.metrics.coverage} />}
rightSlot={<Value value={`${value.metrics.coverage}%`} />}
/>
<RatingCard
title="Duplications"
leftSlot={<Rating rating={duplicationRating} hideValue />}
rightSlot={
<Value value={`${value.metrics.duplicated_lines_density}%`} />
}
/>
</Grid>
<Grid item className={classes.lastAnalyzed}>
Last analyzed on{' '}
{new Date(value.lastAnalysis).toLocaleString('en-US', {
timeZone: 'UTC',
day: 'numeric',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false,
})}
</Grid>
</Grid>
</>
)}
</InfoCard>
);
};
@@ -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 { BackstageTheme } from '@backstage/theme';
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
const useStyles = makeStyles((theme: BackstageTheme) => {
return {
value: {
fontSize: '1.5rem',
fontWeight: theme.typography.fontWeightMedium,
},
};
});
export const Value = ({ value }: { value?: string }) => {
const classes = useStyles();
return <span className={classes.value}>{value}</span>;
};
@@ -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 { SonarQubeCard } from './SonarQubeCard';
+17
View File
@@ -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 * from './SonarQubeCard';
@@ -0,0 +1,23 @@
/*
* 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 { Entity } from '@backstage/catalog-model';
export const SONARQUBE_PROJECT_KEY_ANNOTATION = 'sonarqube.org/project-key';
export const useProjectKey = (entity: Entity) => {
return entity?.metadata.annotations?.[SONARQUBE_PROJECT_KEY_ANNOTATION] ?? '';
};
+18
View File
@@ -0,0 +1,18 @@
/*
* 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 * from './components';
export { plugin } from './plugin';
+23
View File
@@ -0,0 +1,23 @@
/*
* 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 { plugin } from './plugin';
describe('sonarqube', () => {
it('should export plugin', () => {
expect(plugin).toBeDefined();
});
});
+38
View File
@@ -0,0 +1,38 @@
/*
* 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 {
configApiRef,
createApiFactory,
createPlugin,
discoveryApiRef,
} from '@backstage/core';
import { SonarQubeApi, sonarQubeApiRef } from './api';
export const plugin = createPlugin({
id: 'sonarqube',
apis: [
createApiFactory({
api: sonarQubeApiRef,
deps: { configApi: configApiRef, discoveryApi: discoveryApiRef },
factory: ({ configApi, discoveryApi }) =>
new SonarQubeApi({
discoveryApi,
baseUrl: configApi.getOptionalString('sonarQube.baseUrl'),
}),
}),
],
});
+17
View File
@@ -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.
*/
import '@testing-library/jest-dom';