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
+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';