[Plugin] Code Climate

Signed-off-by: Frank Cycan <frank@hqo.co>
This commit is contained in:
Frank Cycan
2022-02-22 11:53:46 -05:00
parent d788fa8495
commit e621d9658b
27 changed files with 1389 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-code-climate': minor
---
Added Code Climate plugin
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+85
View File
@@ -0,0 +1,85 @@
# Code Climate Plugin
The Code Climate Plugin displays a few stats from the quality section from [Code Climate](https://codeclimate.com).
![Code Climate Card](./docs/code-climate-card.png)
## Getting Started
1. Install the Code Climate Plugin:
```bash
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-code-climate
```
2. Add the `EntityCodeClimateCard` to the EntityPage:
```jsx
// packages/app/src/components/catalog/EntityPage.tsx
import { EntityCodeClimateCard } from '@backstage/plugin-code-climate';
const overviewContent = (
<Grid container spacing={3} alignItems="stretch">
// ...
<Grid item>
<EntityCodeClimateCard />
</Grid>
// ...
</Grid>
);
```
3. Add the proxy config:
```yaml
# app-config.yaml
proxy:
'/codeclimate/api':
target: https://api.codeclimate.com/v1
headers:
Authorization: Token token=${CODECLIMATE_TOKEN}
```
4. Create a new API access token (https://codeclimate.com/profile/tokens) and provide `CODECLIMATE_TOKEN` as an env variable.
5. Add the `codeclimate.com/repo-id` annotation to your `catalog-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:
codeclimate.com/repo-id: YOUR_REPO_ID
spec:
type: library
owner: CNCF
lifecycle: experimental
```
### Demo Mode
The plugin provides a MockAPI that always returns dummy data instead of talking to the Code Climate backend.
You can add it by overriding the `codeClimateApiRef`:
```ts
// packages/app/src/apis.ts
import { createApiFactory } from '@backstage/core-plugin-api';
import {
MockCodeClimateApi,
codeClimateApiRef,
} from '@backstage/plugin-code-climate';
export const apis = [
// ...
createApiFactory(codeClimateApiRef, new MockCodeClimateApi()),
];
```
+87
View File
@@ -0,0 +1,87 @@
## API Report File for "@backstage/plugin-code-climate"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="react" />
import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { RouteRef } from '@backstage/core-plugin-api';
// Warning: (ae-missing-release-tag) "CodeClimateApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface CodeClimateApi {
// (undocumented)
fetchData(repoID: string): Promise<CodeClimateData>;
}
// Warning: (ae-missing-release-tag) "codeClimateApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const codeClimateApiRef: ApiRef<CodeClimateApi>;
// Warning: (ae-missing-release-tag) "CodeClimateData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type CodeClimateData = {
repoID: string;
maintainability: {
letter: string;
value: string;
};
testCoverage: {
letter: string;
value: string;
};
numberOfCodeSmells: number;
numberOfDuplication: number;
numberOfOtherIssues: number;
};
// Warning: (ae-missing-release-tag) "codeClimatePlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const codeClimatePlugin: BackstagePlugin<
{
root: RouteRef<undefined>;
},
{}
>;
// Warning: (ae-missing-release-tag) "EntityCodeClimateCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const EntityCodeClimateCard: () => JSX.Element;
// Warning: (ae-missing-release-tag) "MockCodeClimateApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export class MockCodeClimateApi implements CodeClimateApi {
// (undocumented)
fetchData(): Promise<CodeClimateData>;
}
// Warning: (ae-missing-release-tag) "mockData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const mockData: CodeClimateData;
// Warning: (ae-missing-release-tag) "ProductionCodeClimateApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export class ProductionCodeClimateApi implements CodeClimateApi {
constructor(discoveryApi: DiscoveryApi);
// (undocumented)
fetchAllData(options: {
apiUrl: string;
repoID: string;
snapshotID: string;
testReportID: string;
}): Promise<any>;
// (undocumented)
fetchData(repoID: string): Promise<CodeClimateData>;
}
```
+93
View File
@@ -0,0 +1,93 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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';
// eslint-disable-next-line import/no-extraneous-dependencies
import { createDevApp, EntityGridItem } from '@backstage/dev-utils';
import { Grid } from '@material-ui/core';
import React from 'react';
import {
EntityCodeClimateCard,
MockCodeClimateApi,
CodeClimateApi,
codeClimateApiRef,
} from '../src';
import { CODECLIMATE_REPO_ID_ANNOTATION } from '../src/plugin';
import { Content, Header, Page } from '@backstage/core-components';
const entity = (name?: string) =>
({
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
annotations: {
[CODECLIMATE_REPO_ID_ANNOTATION]: name,
},
name: name,
},
} as Entity);
createDevApp()
.registerApi({
api: codeClimateApiRef,
deps: {},
factory: () =>
({
fetchData: async (repoID: string) => {
switch (repoID) {
case 'error':
throw new Error('Error!');
case 'never':
return new Promise(() => {});
case 'no-values':
return undefined;
case 'with-values':
return new MockCodeClimateApi().fetchData();
default:
return [];
}
},
} as CodeClimateApi),
})
.addPage({
title: 'Cards',
element: (
<Page themeId="home">
<Header title="Code Climate" />
<Content>
<Grid container>
<EntityGridItem xs={12} md={6} entity={entity('error')}>
<EntityCodeClimateCard />
</EntityGridItem>
<EntityGridItem xs={12} md={6} entity={entity('never')}>
<EntityCodeClimateCard />
</EntityGridItem>
<EntityGridItem xs={12} md={6} entity={entity('no-values')}>
<EntityCodeClimateCard />
</EntityGridItem>
<EntityGridItem xs={12} md={6} entity={entity('with-values')}>
<EntityCodeClimateCard />
</EntityGridItem>
</Grid>
</Content>
</Page>
),
})
.render();
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

+56
View File
@@ -0,0 +1,56 @@
{
"name": "@backstage/plugin-code-climate",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"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.10.0",
"@backstage/core-components": "^0.8.9",
"@backstage/core-plugin-api": "^0.6.1",
"@backstage/plugin-catalog-react": "^0.6.15",
"@backstage/theme": "^0.2.15",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
"humanize-duration": "^3.27.1",
"luxon": "^2.0.2",
"react-router": "6.0.0-beta.0",
"react-use": "^17.2.4"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.14.0",
"@backstage/dev-utils": "^0.2.22",
"@backstage/test-utils": "^0.2.5",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^13.1.8",
"@types/humanize-duration": "^3.27.1",
"@types/jest": "^26.0.7",
"@types/luxon": "^2.0.9",
"@types/node": "^14.14.32",
"cross-fetch": "^3.1.5",
"msw": "^0.35.0"
},
"files": [
"dist"
]
}
@@ -0,0 +1,26 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { CodeClimateData } from './code-climate-data';
import { createApiRef } from '@backstage/core-plugin-api';
export const codeClimateApiRef = createApiRef<CodeClimateApi>({
id: 'plugin.code-climate.service',
});
export interface CodeClimateApi {
fetchData(repoID: string): Promise<CodeClimateData>;
}
@@ -0,0 +1,184 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 type CodeClimateRepoData = {
data: {
id: string;
type: string;
attributes: {
analysis_version: number;
badge_token: string;
branch: string;
created_at: string;
delegated_config_repo_id: string;
diff_coverage_enforced: boolean;
diff_coverage_threshold: number;
enable_notifications: boolean;
github_slug: string;
human_name: string;
last_activity_at: string;
test_reporter_id: string | null;
total_coverage_enforced: boolean;
vcs_database_id: string;
vcs_host: string;
score: string | null;
};
relationships: {
latest_default_branch_snapshot: {
data: { id: string; type: string };
};
latest_default_branch_test_report: {
data: { id: string; type: string };
};
account: {
data: { id: string; type: string };
};
};
links: {
self: string;
services: string;
web_coverage: string;
web_issues: string;
maintainability_badge: string;
test_coverage_badge: string;
};
meta: { permissions: { admin: boolean } };
};
};
export type CodeClimateMaintainabilityData = {
data: {
id: string;
type: string;
attributes: {
commit_sha: string;
committed_at: string;
created_at: string;
lines_of_code: number;
ratings: [
{
path: string;
letter: string;
measure: {
value: number;
unit: string;
meta: {
remediation_time: { value: number; unit: string };
implementation_time: {
value: number;
unit: string;
};
};
};
pillar: string;
},
];
gpa: string | null;
worker_version: number;
};
meta: {
issues_count: number;
measures: {
remediation: { value: number; unit: string };
technical_debt_ratio: {
value: number;
unit: string;
meta: {
remediation_time: { value: number; unit: string };
implementation_time: {
value: number;
unit: string;
};
};
};
};
};
};
};
export type CodeClimateTestCoverageData = {
data: {
id: string;
type: string;
attributes: {
branch: string;
commit_sha: string;
committed_at: string;
covered_percent: number;
lines_of_code: number;
rating: {
path: string;
letter: string;
measure: { value: number; unit: string };
pillar: string;
};
received_at: string;
state: string;
};
};
};
export type CodeClimateIssuesData = {
data: [
{
id: string;
type: string;
attributes: {
categories: string[];
check_name: string;
constant_name: string;
content: { body: string };
description: string;
engine_name: string;
fingerprint: string;
location: {
path: string;
end_line: number;
start_line: number;
};
other_locations: string[];
remediation_points: number;
severity: string;
};
meta: { permissions: { manageable: boolean } };
},
];
links: {
self: string;
next: string;
last: string;
};
meta: { current_page: number; total_pages: number; total_count: number };
};
export type CodeClimateData = {
repoID: string;
maintainability: {
letter: string;
value: string;
};
testCoverage: {
letter: string;
value: string;
};
numberOfCodeSmells: number;
numberOfDuplication: number;
numberOfOtherIssues: number;
};
export type CodeClimateApiError = {
detail: string;
};
+21
View File
@@ -0,0 +1,21 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 './mock';
export type { CodeClimateApi } from './code-climate-api';
export { codeClimateApiRef } from './code-climate-api';
export type { CodeClimateData } from './code-climate-data';
export { ProductionCodeClimateApi } from './production-api';
@@ -0,0 +1,49 @@
{
"data": {
"id": "6b8cc37a64b741dd9d516119",
"type": "snapshots",
"attributes": {
"commit_sha": "d670460b4b4aece5915caf5c68d12f560a9fe3e4",
"committed_at": "2022-01-14T15:17:29.311Z",
"created_at": "2022-01-31T10:07:40.415Z",
"lines_of_code": 8854,
"ratings": [
{
"path": "/",
"letter": "B",
"measure": {
"value": 5.264879308188034,
"unit": "percent",
"meta": {
"remediation_time": { "value": 7186.52, "unit": "minute" },
"implementation_time": {
"value": 136499.2353922225,
"unit": "minute"
}
}
},
"pillar": "Maintainability"
}
],
"gpa": null,
"worker_version": 69440
},
"meta": {
"issues_count": 127,
"measures": {
"remediation": { "value": 7186.52, "unit": "minute" },
"technical_debt_ratio": {
"value": 5.264879308188034,
"unit": "percent",
"meta": {
"remediation_time": { "value": 7186.52, "unit": "minute" },
"implementation_time": {
"value": 136499.2353922225,
"unit": "minute"
}
}
}
}
}
}
}
@@ -0,0 +1,21 @@
{
"data": {
"id": "6b8cc37a64b741dd9d516119",
"type": "test_reports",
"attributes": {
"branch": "master",
"commit_sha": "d670460b4b4aece5915caf5c68d12f560a9fe3e4",
"committed_at": "2022-01-14T15:17:26.000Z",
"covered_percent": 88.41059602649007,
"lines_of_code": 8854,
"rating": {
"path": "/",
"letter": "B",
"measure": { "value": 88.41059602649007, "unit": "percent" },
"pillar": "Test Coverage"
},
"received_at": "2022-01-14T15:25:38.352Z",
"state": "done"
}
}
}
@@ -0,0 +1,18 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { mockData } from './mock-api';
export { MockCodeClimateApi } from './mock-api';
@@ -0,0 +1,56 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { CodeClimateData } from '../code-climate-data';
import { CodeClimateApi } from '../code-climate-api';
import maintainabilityMock from './code-climate-maintainability-mock.json';
import testCoverageMock from './code-climate-test-coverage-mock.json';
import { Duration } from 'luxon';
import humanizeDuration from 'humanize-duration';
const maintainabilityData = maintainabilityMock.data.attributes.ratings[0];
const testCoverageData = testCoverageMock.data.attributes.rating;
const maintainabilityValue: any = {};
maintainabilityValue[
maintainabilityData.measure.meta.implementation_time.unit
] = maintainabilityData.measure.meta.implementation_time.value.toFixed();
export const mockData: CodeClimateData = {
repoID: '6b8cc37a64b741dd9d516119',
maintainability: {
letter: maintainabilityData.letter,
value: humanizeDuration(
Duration.fromObject(maintainabilityValue).toMillis(),
{ largest: 1 },
),
},
testCoverage: {
letter: testCoverageData.letter,
value: testCoverageData.measure.value.toFixed(),
},
numberOfCodeSmells: 97,
numberOfDuplication: 49,
numberOfOtherIssues: 26,
};
export class MockCodeClimateApi implements CodeClimateApi {
fetchData(): Promise<CodeClimateData> {
return new Promise(resolve => {
setTimeout(() => resolve(mockData), 800);
});
}
}
@@ -0,0 +1,160 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 {
CodeClimateData,
CodeClimateRepoData,
CodeClimateMaintainabilityData,
CodeClimateTestCoverageData,
CodeClimateIssuesData,
} from './code-climate-data';
import { CodeClimateApi } from './code-climate-api';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { Duration } from 'luxon';
import humanizeDuration from 'humanize-duration';
const pageQuery = encodeURIComponent('page[size]');
const statusFilter = encodeURIComponent('filter[status][$in][]');
const categoriesFilter = encodeURIComponent('filter[categories][$in][]');
const basicIssuesOptions = `${pageQuery}=1&${statusFilter}=open&${statusFilter}=confirmed`;
const codeSmellsQuery = `${basicIssuesOptions}&${categoriesFilter}=Complexity`;
const duplicationQuery = `${basicIssuesOptions}&${categoriesFilter}=Duplication`;
const otherIssuesQuery = `${basicIssuesOptions}&${categoriesFilter}=Bug%20Risk`;
export class ProductionCodeClimateApi implements CodeClimateApi {
constructor(private readonly discoveryApi: DiscoveryApi) {}
async fetchAllData(options: {
apiUrl: string;
repoID: string;
snapshotID: string;
testReportID: string;
}): Promise<any> {
const { apiUrl, repoID, snapshotID, testReportID } = options;
const [
maintainabilityResponse,
testCoverageResponse,
codeSmellsResponse,
duplicationResponse,
otherIssuesResponse,
] = await Promise.all([
await fetch(`${apiUrl}/repos/${repoID}/snapshots/${snapshotID}`),
await fetch(`${apiUrl}/repos/${repoID}/test_reports/${testReportID}`),
await fetch(
`${apiUrl}/repos/${repoID}/snapshots/${snapshotID}/issues?${codeSmellsQuery}`,
),
await fetch(
`${apiUrl}/repos/${repoID}/snapshots/${snapshotID}/issues?${duplicationQuery}`,
),
await fetch(
`${apiUrl}/repos/${repoID}/snapshots/${snapshotID}/issues?${otherIssuesQuery}`,
),
]);
if (
!maintainabilityResponse.ok ||
!testCoverageResponse.ok ||
!codeSmellsResponse.ok ||
!duplicationResponse.ok ||
!otherIssuesResponse.ok
) {
throw new Error('Failed fetching Code Climate info');
}
const maintainabilityData = (
(await maintainabilityResponse.json()) as CodeClimateMaintainabilityData
).data.attributes.ratings[0];
const testCoverageData = (
(await testCoverageResponse.json()) as CodeClimateTestCoverageData
).data.attributes.rating;
const codeSmellsData = (
(await codeSmellsResponse.json()) as CodeClimateIssuesData
).meta.total_count;
const duplicationData = (
(await duplicationResponse.json()) as CodeClimateIssuesData
).meta.total_count;
const otherIssuesData = (
(await otherIssuesResponse.json()) as CodeClimateIssuesData
).meta.total_count;
return [
maintainabilityData,
testCoverageData,
codeSmellsData,
duplicationData,
otherIssuesData,
];
}
async fetchData(repoID: string): Promise<CodeClimateData> {
if (!repoID) {
throw new Error('No Repo id found');
}
const apiUrl = `${await this.discoveryApi.getBaseUrl(
'proxy',
)}/codeclimate/api`;
const repoResponse = await fetch(`${apiUrl}/repos/${repoID}`);
if (!repoResponse.ok) {
throw new Error('Failed fetching Code Climate info');
}
const repoData = ((await repoResponse.json()) as CodeClimateRepoData).data;
const snapshotID =
repoData.relationships.latest_default_branch_snapshot.data.id;
const testReportID =
repoData.relationships.latest_default_branch_test_report.data.id;
const [
maintainabilityData,
testCoverageData,
codeSmellsData,
duplicationData,
otherIssuesData,
] = await this.fetchAllData({
apiUrl,
repoID,
snapshotID,
testReportID,
});
const maintainabilityValue: any = {};
maintainabilityValue[
maintainabilityData.measure.meta.implementation_time.unit
] = maintainabilityData.measure.meta.implementation_time.value.toFixed();
return {
repoID,
maintainability: {
letter: maintainabilityData.letter,
value: humanizeDuration(
Duration.fromObject(maintainabilityValue).toMillis(),
{ largest: 1 },
),
},
testCoverage: {
letter: testCoverageData.letter,
value: testCoverageData.measure.value.toFixed(),
},
numberOfCodeSmells: codeSmellsData,
numberOfDuplication: duplicationData,
numberOfOtherIssues: otherIssuesData,
};
}
}
@@ -0,0 +1,25 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 React from 'react';
import { InfoCard } from '@backstage/core-components';
import { CodeClimateCardContents } from '../CodeClimateCardContents';
export const CodeClimateCard = () => (
<InfoCard title="Code Climate Summary">
<CodeClimateCardContents />
</InfoCard>
);
@@ -0,0 +1,17 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { CodeClimateCard } from './CodeClimateCard';
@@ -0,0 +1,64 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 React from 'react';
import useAsync from 'react-use/lib/useAsync';
import { codeClimateApiRef } from '../../api';
import { CodeClimateTable } from '../CodeClimateTable';
import { CODECLIMATE_REPO_ID_ANNOTATION } from '../../plugin';
import { useEntity } from '@backstage/plugin-catalog-react';
import {
EmptyState,
ErrorPanel,
MissingAnnotationEmptyState,
Progress,
} from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
export const CodeClimateCardContents = () => {
const { entity } = useEntity();
const codeClimateApi = useApi(codeClimateApiRef);
const repoID =
entity?.metadata.annotations?.[CODECLIMATE_REPO_ID_ANNOTATION] ?? '';
const { loading, value, error } = useAsync(
() => codeClimateApi.fetchData(repoID),
[codeClimateApi, repoID],
);
if (loading) {
return <Progress />;
} else if (!repoID) {
return (
<MissingAnnotationEmptyState
annotation={CODECLIMATE_REPO_ID_ANNOTATION}
/>
);
} else if (error) {
return <ErrorPanel error={error} />;
} else if (!value) {
return (
<EmptyState
missing="info"
title="No information to display"
description={`There is no Code Climate repo setup with id '${repoID}'.`}
/>
);
}
return <CodeClimateTable codeClimateData={value} />;
};
@@ -0,0 +1,17 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { CodeClimateCardContents } from './CodeClimateCardContents';
@@ -0,0 +1,37 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 React from 'react';
import { render } from '@testing-library/react';
import { CodeClimateTable } from './CodeClimateTable';
import { mockData } from '../../api/mock';
import { ThemeProvider } from '@material-ui/core';
import { lightTheme } from '@backstage/theme';
describe('CodeClimateTable', () => {
it('should render values in a table', async () => {
const table = await render(
<ThemeProvider theme={lightTheme}>
<CodeClimateTable codeClimateData={mockData} />
</ThemeProvider>,
);
expect(await table.findByText('3 months')).toBeInTheDocument();
expect(await table.findByText('88%')).toBeInTheDocument();
expect(await table.findByText('97')).toBeInTheDocument();
expect(await table.findByText('49')).toBeInTheDocument();
expect(await table.findByText('26')).toBeInTheDocument();
});
});
@@ -0,0 +1,212 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 React from 'react';
import { CodeClimateData } from '../../api';
import { Link } from '@backstage/core-components';
import { Box, makeStyles, Typography } from '@material-ui/core';
import { BackstageTheme } from '@backstage/theme';
const letterStyle = {
color: 'white',
border: 0,
borderRadius: '3px',
fontSize: '40px',
padding: '5px 20px',
};
const fontSize = {
fontSize: '25px',
};
const letterColor = (letter: string) => {
if (letter === 'A') {
return '#45d298';
} else if (letter === 'B') {
return '#a5d86e';
} else if (letter === 'C') {
return '#f1ce0c';
} else if (letter === 'D') {
return '#f29141';
} else if (letter === 'F') {
return '#df5869';
}
return '#45d298';
};
const useStyles = makeStyles<
BackstageTheme,
{
maintainabilityLetter: string;
testCoverageLetter: string;
}
>({
spaceAround: {
display: 'flex',
justifyContent: 'space-around',
},
spaceBetween: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
maintainabilityLetterColor: {
...letterStyle,
backgroundColor: props => letterColor(props.maintainabilityLetter),
},
testCoverageLetterColor: {
...letterStyle,
backgroundColor: props => letterColor(props.testCoverageLetter),
},
fontSize: {
...fontSize,
},
letterDetails: {
...fontSize,
paddingLeft: '10px',
},
paddingSides20: {
padding: '0px 20px',
},
});
export const CodeClimateTable = ({
codeClimateData,
}: {
codeClimateData: CodeClimateData;
}) => {
const {
repoID,
maintainability: {
letter: maintainabilityLetter,
value: maintainabilityValue,
},
testCoverage: { letter: testCoverageLetter, value: testCoverageValue },
numberOfCodeSmells,
numberOfDuplication,
numberOfOtherIssues,
} = codeClimateData;
const classes = useStyles({ maintainabilityLetter, testCoverageLetter });
if (!codeClimateData) {
return null;
}
return (
<>
<div className={classes.spaceAround}>
<div>
<Typography variant="h6" component="p">
Maintainability
</Typography>
<div className={classes.spaceBetween}>
<Typography
className={classes.maintainabilityLetterColor}
variant="body2"
component="p"
>
{maintainabilityLetter}
</Typography>
<Link to={`https://codeclimate.com/repos/${repoID}`}>
<Typography
className={classes.letterDetails}
variant="body2"
component="p"
>
{maintainabilityValue}
</Typography>
</Link>
</div>
</div>
<div>
<Typography variant="h6" component="p">
Test Coverage
</Typography>
<div className={classes.spaceBetween}>
<Typography
className={classes.testCoverageLetterColor}
variant="body2"
component="p"
>
{testCoverageLetter}
</Typography>
<Link to={`https://codeclimate.com/repos/${repoID}`}>
<Typography
className={classes.letterDetails}
variant="body2"
component="p"
>
{testCoverageValue}%
</Typography>
</Link>
</div>
</div>
</div>
<Box className={classes.spaceAround} paddingTop="30px">
<div>
<Typography variant="h6" component="p">
Code Smells:
</Typography>
<Link
to={`https://codeclimate.com/repos/${repoID}/issues?category%5B%5D=complexity&status%5B%5D=&status%5B%5D=open&status%5B%5D=confirmed`}
>
<Typography
className={classes.fontSize}
variant="body2"
component="p"
>
{numberOfCodeSmells}
</Typography>
</Link>
</div>
<Box paddingLeft="20" paddingRight="20">
<Typography variant="h6" component="p">
Duplication:
</Typography>
<Link
to={`https://codeclimate.com/repos/${repoID}/issues?category%5B%5D=duplication&status%5B%5D=&status%5B%5D=open&status%5B%5D=confirmed`}
>
<Typography
className={classes.fontSize}
variant="body2"
component="p"
>
{numberOfDuplication}
</Typography>
</Link>
</Box>
<div>
<Typography variant="h6" component="p">
Other Issues:
</Typography>
<Link
to={`https://codeclimate.com/repos/${repoID}/issues?category%5B%5D=bugrisk&status%5B%5D=&status%5B%5D=open&status%5B%5D=confirmed`}
>
<Typography
className={classes.fontSize}
variant="body2"
component="p"
>
{numberOfOtherIssues}
</Typography>
</Link>
</div>
</Box>
</>
);
};
@@ -0,0 +1,17 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { CodeClimateTable } from './CodeClimateTable';
+24
View File
@@ -0,0 +1,24 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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.
*/
/**
* A Backstage plugin that integrates towards Code Climate
*
* @packageDocumentation
*/
export * from './api';
export { codeClimatePlugin, EntityCodeClimateCard } from './plugin';
+23
View File
@@ -0,0 +1,23 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { codeClimatePlugin } from './plugin';
describe('codeclimate', () => {
it('should export plugin', () => {
expect(codeClimatePlugin).toBeDefined();
});
});
+58
View File
@@ -0,0 +1,58 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { ProductionCodeClimateApi, codeClimateApiRef } from './api';
import {
createApiFactory,
createPlugin,
createRouteRef,
discoveryApiRef,
identityApiRef,
createComponentExtension,
} from '@backstage/core-plugin-api';
export const CODECLIMATE_REPO_ID_ANNOTATION = 'codeclimate.com/repo-id';
export const rootRouteRef = createRouteRef({
id: 'code-climate',
});
export const codeClimatePlugin = createPlugin({
id: 'code-climate',
apis: [
createApiFactory({
api: codeClimateApiRef,
deps: {
discoveryApi: discoveryApiRef,
identityApi: identityApiRef,
},
factory: ({ discoveryApi }) => new ProductionCodeClimateApi(discoveryApi),
}),
],
routes: {
root: rootRouteRef,
},
});
export const EntityCodeClimateCard = codeClimatePlugin.provide(
createComponentExtension({
name: 'EntityCodeClimateCard',
component: {
lazy: () =>
import('./components/CodeClimateCard').then(m => m.CodeClimateCard),
},
}),
);
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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.
*/
/* eslint-disable import/no-extraneous-dependencies */
import '@testing-library/jest-dom';
import 'cross-fetch/polyfill';
+11 -1
View File
@@ -5785,6 +5785,11 @@
resolved "https://registry.npmjs.org/@types/humanize-duration/-/humanize-duration-3.25.1.tgz#b6140d5fc00ff3917b3f521784abef4bc0387ccc"
integrity sha512-WZU/4bb+lvzyDmZzjJtp++9mfKy6B3lH6gGISgkcz6SU8hMILKRM0vi08TxIsb0dQB4Gzo68MWLmctu6xqUi9g==
"@types/humanize-duration@^3.27.1":
version "3.27.1"
resolved "https://registry.npmjs.org/@types/humanize-duration/-/humanize-duration-3.27.1.tgz#f14740d1f585a0a8e3f46359b62fda8b0eaa31e7"
integrity sha512-K3e+NZlpCKd6Bd/EIdqjFJRFHbrq5TzPPLwREk5Iv/YoIjQrs6ljdAUCo+Lb2xFlGNOjGSE0dqsVD19cZL137w==
"@types/inquirer@^7.3.3":
version "7.3.3"
resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-7.3.3.tgz#92e6676efb67fa6925c69a2ee638f67a822952ac"
@@ -5954,6 +5959,11 @@
resolved "https://registry.npmjs.org/@types/luxon/-/luxon-2.0.9.tgz#782a0edfa6d699191292c13168bd496cd66b87c6"
integrity sha512-ZuzIc7aN+i2ZDMWIiSmMdubR9EMMSTdEzF6R+FckP4p6xdnOYKqknTo/k+xXQvciSXlNGIwA4OPU5X7JIFzYdA==
"@types/luxon@^2.0.9":
version "2.0.9"
resolved "https://registry.npmjs.org/@types/luxon/-/luxon-2.0.9.tgz#782a0edfa6d699191292c13168bd496cd66b87c6"
integrity sha512-ZuzIc7aN+i2ZDMWIiSmMdubR9EMMSTdEzF6R+FckP4p6xdnOYKqknTo/k+xXQvciSXlNGIwA4OPU5X7JIFzYdA==
"@types/mdast@^3.0.0", "@types/mdast@^3.0.3":
version "3.0.3"
resolved "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.3.tgz#2d7d671b1cd1ea3deb306ea75036c2a0407d2deb"
@@ -13948,7 +13958,7 @@ human-signals@^2.1.0:
resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
humanize-duration@^3.25.1, humanize-duration@^3.26.0, humanize-duration@^3.27.0:
humanize-duration@^3.25.1, humanize-duration@^3.26.0, humanize-duration@^3.27.0, humanize-duration@^3.27.1:
version "3.27.1"
resolved "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.27.1.tgz#2cd4ea4b03bd92184aee6d90d77a8f3d7628df69"
integrity sha512-jCVkMl+EaM80rrMrAPl96SGG4NRac53UyI1o/yAzebDntEY6K6/Fj2HOjdPg8omTqIe5Y0wPBai2q5xXrIbarA==