feat: add sonarqube plugin (#3160)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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'
|
||||
@@ -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 |
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
# SonarQube Plugin
|
||||
|
||||
The SonarQube Plugin displays code statistics from [SonarCloud](https://sonarcloud.io) or [SonarQube](https://sonarqube.com).
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
```
|
||||
@@ -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 |
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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] ?? '';
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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'),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user