Setup a devapp for SonarQube that shows the different states of the widget
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
+4
-4
@@ -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`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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'),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user