diff --git a/plugins/sonarqube/src/api/index.test.ts b/plugins/sonarqube/src/api/index.test.ts index 272d021493..2d96fc2135 100644 --- a/plugins/sonarqube/src/api/index.test.ts +++ b/plugins/sonarqube/src/api/index.test.ts @@ -114,22 +114,29 @@ describe('SonarQubeApi', () => { 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); + expect(summary).toEqual( + expect.objectContaining({ + 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', + }), + ); + expect(summary?.getIssuesUrl('CODE_SMELL')).toEqual( + 'https://sonarcloud.io/project/issues?id=our-service&types=CODE_SMELL&resolved=false', + ); + expect(summary?.getComponentMeasuresUrl('COVERAGE')).toEqual( + 'https://sonarcloud.io/component_measures?id=our-service&metric=coverage&resolved=false&view=list', + ); }); it('should report finding summary (custom baseUrl)', async () => { @@ -142,20 +149,28 @@ describe('SonarQubeApi', () => { 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); + expect(summary).toEqual( + expect.objectContaining({ + 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, + ); + expect(summary?.getIssuesUrl('CODE_SMELL')).toEqual( + 'http://a.instance.local/project/issues?id=our-service&types=CODE_SMELL&resolved=false', + ); + expect(summary?.getComponentMeasuresUrl('COVERAGE')).toEqual( + 'http://a.instance.local/component_measures?id=our-service&metric=coverage&resolved=false&view=list', + ); }); }); diff --git a/plugins/sonarqube/src/api/index.ts b/plugins/sonarqube/src/api/index.ts index 01f6f74fe3..f2c616f53c 100644 --- a/plugins/sonarqube/src/api/index.ts +++ b/plugins/sonarqube/src/api/index.ts @@ -16,7 +16,12 @@ import { createApiRef, DiscoveryApi } from '@backstage/core'; import fetch from 'cross-fetch'; -import { ComponentWrapper, MeasuresWrapper, MetricKey } from './types'; +import { + ComponentWrapper, + MeasuresWrapper, + MetricKey, + SonarUrlProcessorFunc, +} from './types'; /** * Define a type to make sure that all metrics are used @@ -29,6 +34,8 @@ export interface FindingSummary { lastAnalysis: string; metrics: Metrics; projectUrl: string; + getIssuesUrl: SonarUrlProcessorFunc; + getComponentMeasuresUrl: SonarUrlProcessorFunc; } export const sonarQubeApiRef = createApiRef({ @@ -105,6 +112,14 @@ export class SonarQubeApi { 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`, }; } } diff --git a/plugins/sonarqube/src/api/types.ts b/plugins/sonarqube/src/api/types.ts index 17539070cb..fc99b2d87d 100644 --- a/plugins/sonarqube/src/api/types.ts +++ b/plugins/sonarqube/src/api/types.ts @@ -53,3 +53,5 @@ export interface Measure { value: string; component: string; } + +export type SonarUrlProcessorFunc = (identifier: string) => string; diff --git a/plugins/sonarqube/src/components/SonarQubeCard/RatingCard.tsx b/plugins/sonarqube/src/components/SonarQubeCard/RatingCard.tsx index cd678b7366..6a94f7e8f4 100644 --- a/plugins/sonarqube/src/components/SonarQubeCard/RatingCard.tsx +++ b/plugins/sonarqube/src/components/SonarQubeCard/RatingCard.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Grid, Typography } from '@material-ui/core'; +import { Grid, Typography, Link } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; import React, { ReactNode } from 'react'; @@ -50,29 +50,33 @@ export const RatingCard = ({ rightSlot, title, titleIcon, + link, }: { leftSlot: ReactNode; rightSlot: ReactNode; title: string; titleIcon?: ReactNode; + link: string; }) => { const classes = useStyles(); return ( - - - - {leftSlot} + + + + + {leftSlot} + + + {rightSlot} + - - {rightSlot} + + + {titleIcon} {title} + - - - {titleIcon} {title} - - - + ); }; diff --git a/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx b/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx index 8e6cc817a1..2b836d714b 100644 --- a/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx +++ b/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx @@ -181,29 +181,34 @@ export const SonarQubeCard = ({ } title="Bugs" + link={value.getIssuesUrl('BUG')} leftSlot={} rightSlot={} /> } title="Vulnerabilities" + link={value.getIssuesUrl('VULNERABILITY')} leftSlot={} rightSlot={} /> } title="Code Smells" + link={value.getIssuesUrl('CODE_SMELL')} leftSlot={} rightSlot={} />
} rightSlot={} /> } rightSlot={