Merge pull request #3619 from SDA-SE/feat/sonarqube-standalone

Setup a devapp for SonarQube that shows the different states of the widget
This commit is contained in:
Patrik Oldsberg
2020-12-08 20:06:35 +01:00
committed by GitHub
6 changed files with 296 additions and 117 deletions
+144 -2
View File
@@ -14,7 +14,149 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import {
Content,
createPlugin,
createRouteRef,
Header,
Page,
} from '@backstage/core';
import { createDevApp } from '@backstage/dev-utils';
import { plugin } from '../src/plugin';
import { Grid } from '@material-ui/core';
import React from 'react';
import { SonarQubeCard } from '../src';
import { FindingSummary, SonarQubeApi, sonarQubeApiRef } from '../src/api';
import { SONARQUBE_PROJECT_KEY_ANNOTATION } from '../src/components/useProjectKey';
createDevApp().registerPlugin(plugin).render();
createDevApp()
.registerApi({
api: sonarQubeApiRef,
deps: {},
factory: () =>
({
getFindingSummary: async componentKey => {
switch (componentKey) {
case 'error':
throw new Error('Error!');
case 'never':
return new Promise(() => {});
case 'not-computed':
return {
lastAnalysis: new Date().toISOString(),
metrics: {
bugs: '0',
reliability_rating: '1.0',
vulnerabilities: '0',
security_rating: '1.0',
code_smells: '0',
sqale_rating: '1.0',
coverage: '0.0',
duplicated_lines_density: '0.0',
},
projectUrl: `/#${componentKey}`,
getIssuesUrl: i => `/#${componentKey}/issues/${i}`,
getComponentMeasuresUrl: i => `/#${componentKey}/measures/${i}`,
} as FindingSummary;
case 'failed':
return {
lastAnalysis: new Date().toISOString(),
metrics: {
alert_status: 'FAILED',
bugs: '4',
reliability_rating: '2.0',
vulnerabilities: '18',
security_rating: '3.0',
code_smells: '22',
sqale_rating: '5.0',
coverage: '15.7',
duplicated_lines_density: '15.6',
},
projectUrl: `/#${componentKey}`,
getIssuesUrl: i => `/#${componentKey}/issues/${i}`,
getComponentMeasuresUrl: i => `/#${componentKey}/measures/${i}`,
} as FindingSummary;
case 'passed':
return {
lastAnalysis: new Date().toISOString(),
metrics: {
alert_status: 'OK',
bugs: '0',
reliability_rating: '1.0',
vulnerabilities: '0',
security_rating: '1.0',
code_smells: '0',
sqale_rating: '1.0',
coverage: '100.0',
duplicated_lines_density: '0.0',
},
projectUrl: `/#${componentKey}`,
getIssuesUrl: i => `/#${componentKey}/issues/${i}`,
getComponentMeasuresUrl: i => `/#${componentKey}/measures/${i}`,
} as FindingSummary;
default:
return undefined;
}
},
} as SonarQubeApi),
})
.registerPlugin(
createPlugin({
id: 'defectdojo-demo',
register({ router }) {
const entity = (name?: string) =>
({
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
annotations: {
[SONARQUBE_PROJECT_KEY_ANNOTATION]: name,
},
name: name,
},
} as Entity);
const ExamplePage = () => (
<Page themeId="home">
<Header title="SonarQube" />
<Content>
<Grid container>
<Grid item xs={12} sm={6} md={4}>
<SonarQubeCard entity={entity('empty')} />
</Grid>
<Grid item xs={12} sm={6} md={4}>
<SonarQubeCard entity={entity('error')} />
</Grid>
<Grid item xs={12} sm={6} md={4}>
<SonarQubeCard entity={entity('never')} />
</Grid>
<Grid item xs={12} sm={6} md={4}>
<SonarQubeCard entity={entity('not-computed')} />
</Grid>
<Grid item xs={12} sm={6} md={4}>
<SonarQubeCard entity={entity('failed')} />
</Grid>
<Grid item xs={12} sm={6} md={4}>
<SonarQubeCard entity={entity('passed')} />
</Grid>
<Grid item xs={12}>
<SonarQubeCard entity={entity(undefined)} />
</Grid>
</Grid>
</Content>
</Page>
);
router.addRoute(
createRouteRef({ path: '/', title: 'SonarQube' }),
ExamplePage,
);
},
}),
)
.render();
+42
View File
@@ -0,0 +1,42 @@
/*
* 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 { MetricKey, SonarUrlProcessorFunc } from './types';
/**
* Define a type to make sure that all metrics are used
*/
export type Metrics = {
[key in MetricKey]: string | undefined;
};
export interface FindingSummary {
lastAnalysis: string;
metrics: Metrics;
projectUrl: string;
getIssuesUrl: SonarUrlProcessorFunc;
getComponentMeasuresUrl: SonarUrlProcessorFunc;
}
export const sonarQubeApiRef = createApiRef<SonarQubeApi>({
id: 'plugin.sonarqube.service',
description: 'Used by the SonarQube plugin to make requests',
});
export type SonarQubeApi = {
getFindingSummary(componentKey?: string): Promise<FindingSummary | undefined>;
};
@@ -18,12 +18,12 @@ 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 { FindingSummary, SonarQubeClient } from './index';
import { ComponentWrapper, MeasuresWrapper } from './types';
const server = setupServer();
describe('SonarQubeApi', () => {
describe('SonarQubeClient', () => {
msw.setupDefaultHandlers(server);
const mockBaseUrl = 'http://backstage:9191/api/proxy';
@@ -111,7 +111,7 @@ describe('SonarQubeApi', () => {
it('should report finding summary', async () => {
setupHandlers();
const client = new SonarQubeApi({ discoveryApi });
const client = new SonarQubeClient({ discoveryApi });
const summary = await client.getFindingSummary('our-service');
expect(summary).toEqual(
@@ -142,7 +142,7 @@ describe('SonarQubeApi', () => {
it('should report finding summary (custom baseUrl)', async () => {
setupHandlers();
const client = new SonarQubeApi({
const client = new SonarQubeClient({
discoveryApi,
baseUrl: 'http://a.instance.local',
});
@@ -0,0 +1,101 @@
/*
* 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 { DiscoveryApi } from '@backstage/core';
import fetch from 'cross-fetch';
import { FindingSummary, Metrics, SonarQubeApi } from './SonarQubeApi';
import { ComponentWrapper, MeasuresWrapper } from './types';
export class SonarQubeClient implements 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}`,
getIssuesUrl: identifier =>
`${
this.baseUrl
}project/issues?id=${componentKey}&types=${identifier.toUpperCase()}&resolved=false`,
getComponentMeasuresUrl: (identifier: string) =>
`${
this.baseUrl
}component_measures?id=${componentKey}&metric=${identifier.toLowerCase()}&resolved=false&view=list`,
};
}
}
+3 -109
View File
@@ -14,112 +14,6 @@
* limitations under the License.
*/
import { createApiRef, DiscoveryApi } from '@backstage/core';
import fetch from 'cross-fetch';
import {
ComponentWrapper,
MeasuresWrapper,
MetricKey,
SonarUrlProcessorFunc,
} 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;
getIssuesUrl: SonarUrlProcessorFunc;
getComponentMeasuresUrl: SonarUrlProcessorFunc;
}
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}`,
getIssuesUrl: identifier =>
`${
this.baseUrl
}project/issues?id=${componentKey}&types=${identifier.toUpperCase()}&resolved=false`,
getComponentMeasuresUrl: (identifier: string) =>
`${
this.baseUrl
}component_measures?id=${componentKey}&metric=${identifier.toLowerCase()}&resolved=false&view=list`,
};
}
}
export type { Metrics, FindingSummary, SonarQubeApi } from './SonarQubeApi';
export { sonarQubeApiRef } from './SonarQubeApi';
export { SonarQubeClient } from './SonarQubeClient';
+2 -2
View File
@@ -20,7 +20,7 @@ import {
createPlugin,
discoveryApiRef,
} from '@backstage/core';
import { SonarQubeApi, sonarQubeApiRef } from './api';
import { sonarQubeApiRef, SonarQubeClient } from './api';
export const plugin = createPlugin({
id: 'sonarqube',
@@ -29,7 +29,7 @@ export const plugin = createPlugin({
api: sonarQubeApiRef,
deps: { configApi: configApiRef, discoveryApi: discoveryApiRef },
factory: ({ configApi, discoveryApi }) =>
new SonarQubeApi({
new SonarQubeClient({
discoveryApi,
baseUrl: configApi.getOptionalString('sonarQube.baseUrl'),
}),