Add CodeScene plugin

[CodeScene](https://codescene.com/) is a multi-purpose tool bridging code, business and people. See hidden risks and social patterns in your code. Prioritize and reduce technical debt.

Signed-off-by: Julio Zynger <julio.zynger@soundcloud.com>
This commit is contained in:
Julio Zynger
2022-04-12 12:41:22 +02:00
parent 6de4927c75
commit 9bc6e9493f
31 changed files with 1615 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-codescene': minor
---
Add CodeScene plugin
+2
View File
@@ -50,6 +50,8 @@ codeblocks
Codecov
codehilite
Codehilite
codescene
CodeScene
codemod
codemods
codeowners
+9
View File
@@ -0,0 +1,9 @@
---
title: CodeScene
author: CodeScene
authorUrl: https://codescene.com/
category: Quality
description: CodeScene is a multi-purpose tool bridging code, business and people. See hidden risks and social patterns in your code. Prioritize and reduce technical debt.
documentation: https://github.com/backstage/backstage/tree/master/plugins/codescene
iconUrl: img/codescene_logo.svg
npmPackageName: '@backstage/plugin-codescene'
+50
View File
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 151.82 151.82" style="enable-background:new 0 0 151.82 151.82;" xml:space="preserve">
<style type="text/css">
.st0{fill:#003263;}
</style>
<g>
<path class="st0" d="M37.48,128.56c1.84,0,3.1,1,3.48,2.7l1.52-0.34c-0.49-2.28-2.4-3.73-4.98-3.73c-2.95,0-5.74,1.97-5.74,6.04
c0,4.07,2.77,6.04,5.74,6.04c2.55,0,4.39-1.42,4.98-3.64l-1.54-0.32c-0.42,1.64-1.62,2.58-3.46,2.58c-2.4,0-4.13-1.64-4.13-4.66
C33.35,130.21,35.09,128.56,37.48,128.56z"/>
<path class="st0" d="M48.11,130.5c-2.53,0-4.32,1.84-4.32,4.39c0,2.55,1.79,4.39,4.32,4.39c2.51,0,4.34-1.84,4.34-4.39
C52.45,132.34,50.63,130.5,48.11,130.5z M48.11,137.99c-1.7,0-2.85-1.28-2.85-3.1c0-1.82,1.15-3.1,2.85-3.1
c1.7,0,2.85,1.28,2.85,3.1C50.97,136.71,49.82,137.99,48.11,137.99z"/>
<path class="st0" d="M60.67,131.85c-0.54-0.79-1.65-1.35-2.78-1.35c-2.63,0-4.08,1.89-4.08,4.39c0,2.5,1.45,4.39,4.1,4.39
c1.25,0,2.38-0.59,2.77-1.37v1.23h1.43v-11.95h-1.43V131.85z M58.03,137.99c-1.67,0-2.77-1.23-2.77-3.1c0-1.87,1.1-3.1,2.77-3.1
c1.67,0,2.77,1.23,2.77,3.1C60.8,136.76,59.71,137.99,58.03,137.99z"/>
<path class="st0" d="M68.06,130.5c-2.56,0-4.12,1.97-4.12,4.39c0,2.41,1.6,4.39,4.15,4.39c1.96,0,3.37-0.96,3.8-2.62l-1.35-0.32
c-0.25,1.06-1.15,1.67-2.45,1.67c-1.54,0-2.62-1.13-2.72-2.85h6.72c0.02-0.15,0.03-0.46,0.03-0.66
C72.12,132.3,70.57,130.5,68.06,130.5z M65.46,133.96c0.29-1.42,1.27-2.26,2.6-2.26c1.5,0,2.41,0.96,2.55,2.26H65.46z"/>
<path class="st0" d="M75.31,130.28c0-1.06,0.91-1.72,2.43-1.72c1.49,0,2.5,0.81,2.73,2.09l1.4-0.54c-0.3-1.75-1.97-2.92-4.13-2.92
c-2.33,0-3.98,1.25-3.98,3.09c0,4.18,6.72,2.53,6.72,5.69c0,1.18-1.05,1.94-2.67,1.94c-1.57,0-2.83-1.08-3.07-2.73l-1.45,0.47
c0.39,2.16,2.21,3.63,4.49,3.63c2.58,0,4.29-1.32,4.29-3.32C82.06,131.22,75.31,132.98,75.31,130.28z"/>
<path class="st0" d="M87.61,137.99c-1.67,0-2.75-1.23-2.75-3.1c0-1.87,1.08-3.1,2.75-3.1c1.2,0,2.09,0.74,2.28,1.77l1.42-0.29
c-0.3-1.69-1.75-2.77-3.73-2.77c-2.53,0-4.22,1.75-4.22,4.39s1.69,4.39,4.22,4.39c1.96,0,3.37-1.03,3.75-2.78l-1.4-0.29
C89.69,137.33,88.81,137.99,87.61,137.99z"/>
<path class="st0" d="M96.74,130.5c-2.56,0-4.12,1.97-4.12,4.39c0,2.41,1.6,4.39,4.15,4.39c1.96,0,3.37-0.96,3.8-2.62l-1.35-0.32
c-0.25,1.06-1.15,1.67-2.45,1.67c-1.54,0-2.62-1.13-2.72-2.85h6.72c0.02-0.15,0.03-0.46,0.03-0.66
C100.81,132.3,99.26,130.5,96.74,130.5z M94.14,133.96c0.29-1.42,1.27-2.26,2.6-2.26c1.5,0,2.41,0.96,2.55,2.26H94.14z"/>
<path class="st0" d="M107,130.5c-1.3,0-2.33,0.73-2.92,1.74v-1.6h-1.43v8.5h1.43v-3.73c0-2.24,1.01-3.61,2.6-3.61
c1.2,0,1.92,0.83,1.92,1.97v5.37h1.43v-5.64C110.04,131.7,108.77,130.5,107,130.5z"/>
<path class="st0" d="M115.99,130.5c-2.56,0-4.12,1.97-4.12,4.39c0,2.41,1.6,4.39,4.15,4.39c1.96,0,3.37-0.96,3.8-2.62l-1.35-0.32
c-0.25,1.06-1.15,1.67-2.45,1.67c-1.54,0-2.62-1.13-2.72-2.85h6.72c0.02-0.15,0.03-0.46,0.03-0.66
C120.06,132.3,118.51,130.5,115.99,130.5z M113.39,133.96c0.29-1.42,1.27-2.26,2.6-2.26c1.5,0,2.41,0.96,2.55,2.26H113.39z"/>
<path class="st0" d="M122.68,41.11c-6.61-18.18-23.07-28.56-46.76-28.56S35.76,22.92,29.15,41.11c-5.7,1.97-9.93,12.54-9.93,25.68
c0,14.59,5.21,26.02,11.86,26.02c0.15,0,0.3-0.01,0.45-0.02c2.28,4.52,5.27,7.99,9.01,10.52c9.03,6.11,21.6,6.53,34.84,6.53h1.06
c0.17,0,0.34,0,0.51,0c13.08,0,25.42-0.51,34.33-6.53c3.74-2.53,6.74-6,9.01-10.52c0.15,0.01,0.3,0.02,0.45,0.02
c6.65,0,11.86-11.43,11.86-26.02C132.6,53.65,128.38,43.07,122.68,41.11z M21.44,66.79c0-11.22,3.1-19.51,6.69-22.53
c-1.46,5.05-2.22,10.63-2.22,16.71c0,12.49,1.48,22.21,4.57,29.53C26.01,89.74,21.44,80.42,21.44,66.79z M119.05,90.17
c-0.31,0.69-0.63,1.36-0.96,2c-2.08,3.97-4.75,7.05-8.06,9.29c-8.35,5.65-19.8,6.25-33.05,6.25c-0.18,0-0.36,0-0.54,0h-0.53h-0.53
c-13.5-0.01-25.13-0.52-33.59-6.25c-3.31-2.24-5.97-5.32-8.06-9.29c-0.34-0.64-0.66-1.31-0.96-2c-3.13-7.07-4.64-16.66-4.64-29.19
c0-6.62,0.95-12.62,2.75-17.95c0.25-0.75,0.53-1.49,0.81-2.22c6.6-16.59,22.11-26.03,44.22-26.03s37.62,9.44,44.22,26.03
c0.29,0.73,0.56,1.46,0.81,2.22c1.8,5.33,2.75,11.33,2.75,17.95C123.69,73.51,122.18,83.09,119.05,90.17z M121.34,90.51
c3.09-7.32,4.57-17.04,4.57-29.53c0-6.08-0.76-11.66-2.22-16.71c3.59,3.02,6.69,11.31,6.69,22.53
C130.38,80.42,125.81,89.74,121.34,90.51z"/>
<path class="st0" d="M75.91,28.64c-29.98,0-43.35,11.92-43.35,38.65c0,26,14.18,38.64,43.35,38.64s43.35-12.64,43.35-38.64
C119.26,40.56,105.89,28.64,75.91,28.64z M75.91,103.7c-28.06,0-41.13-11.57-41.13-36.42c0-16.96,4.67-36.42,41.13-36.42
s41.13,19.46,41.13,36.42C117.04,92.13,103.97,103.7,75.91,103.7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+62
View File
@@ -0,0 +1,62 @@
# codescene
[CodeScene](https://codescene.com/) is a multi-purpose tool bridging code, business and people. See hidden risks and social patterns in your code. Prioritize and reduce technical debt.
![codescene-logo](./src/assets/codescene.icon.svg)
The CodeScene Backstage Plugin exposes a page component that will list the existing projects and their analysis data on your CodeScene instance.
![screenshot](./docs/codescene-plugin-screenshot.png)
## Setup
1. Install the plugin by running:
```bash
# From your Backstage root directory
yarn add --cwd packages/app @backstage/plugin-codescene
```
2. Add the routes and pages to your `App.tsx`:
```tsx
import {
CodescenePage,
CodesceneProjectDetailsPage,
} from '@backstage/plugin-codescene';
...
<Route path="/codescene" element={<CodescenePage />} />
<Route
path="/codescene/:projectId"
element={<CodesceneProjectDetailsPage />}
/>
```
3. Add to the sidebar item routing to the new page:
```tsx
// In packages/app/src/components/Root/Root.tsx
import { CodeSceneIcon } from '@backstage/plugin-codescene';
{
/* other sidebar items... */
}
<SidebarItem icon={CodeSceneIcon} to="codescene" text="CodeScene" />;
```
4. Setup the `app-config.yaml` `codescene` proxy and configuration blocks:
```yaml
proxy:
'/codescene-api':
target: '<INSTANCE_HOSTNAME>/api/v1'
allowedMethods: ['GET']
allowedHeaders: ['Authorization']
headers:
Authorization: Basic ${CODESCENE_AUTH_CREDENTIALS}
---
codescene:
baseUrl: <INSTANCE_HOSTNAME>
```
+33
View File
@@ -0,0 +1,33 @@
## API Report File for "@backstage/plugin-codescene"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="react" />
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
import { RouteRef } from '@backstage/core-plugin-api';
// @public (undocumented)
export const CodeSceneIcon: IconComponent;
// @public (undocumented)
export const CodescenePage: () => JSX.Element;
// @public (undocumented)
export const codescenePlugin: BackstagePlugin<
{
root: RouteRef<undefined>;
projectPage: RouteRef<{
projectId: string;
}>;
},
{}
>;
// @public (undocumented)
export const CodesceneProjectDetailsPage: () => JSX.Element;
// (No @packageDocumentation comment for this package)
```
+27
View File
@@ -0,0 +1,27 @@
/*
* Copyright 2022 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 interface Config {
/**
* Configuration options for the CodeScene plugin
*/
codescene?: {
/**
* The base url of the CodeScene installation.
* @visibility frontend
*/
baseUrl: string;
};
}
+36
View File
@@ -0,0 +1,36 @@
/*
* Copyright 2022 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 { createDevApp } from '@backstage/dev-utils';
import {
codescenePlugin,
CodescenePage,
CodesceneProjectDetailsPage,
} from '../src/plugin';
createDevApp()
.registerPlugin(codescenePlugin)
.addPage({
element: <CodescenePage />,
title: 'Root Page',
path: '/codescene',
})
.addPage({
element: <CodesceneProjectDetailsPage />,
title: 'Details Page',
path: '/codescene/:projectId',
})
.render();
Binary file not shown.

After

Width:  |  Height:  |  Size: 746 KiB

+58
View File
@@ -0,0 +1,58 @@
{
"name": "@backstage/plugin-codescene",
"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"
},
"backstage": {
"role": "frontend-plugin"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/config": "^1.0.0",
"@backstage/core-components": "^0.9.3-next.1",
"@backstage/core-plugin-api": "^1.0.0",
"@backstage/errors": "^1.0.0",
"@backstage/theme": "^0.2.15",
"@material-ui/core": "^4.9.10",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.57",
"rc-progress": "3.2.4",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^17.2.4"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.17.0-next.2",
"@backstage/core-app-api": "^1.0.1-next.0",
"@backstage/dev-utils": "^1.0.1-next.0",
"@backstage/test-utils": "^1.0.1-next.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^13.1.8",
"@types/jest": "*",
"@types/node": "*",
"msw": "^0.35.0",
"cross-fetch": "^3.1.5"
},
"files": [
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts"
}
+77
View File
@@ -0,0 +1,77 @@
/*
* Copyright 2022 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 { createApiRef, DiscoveryApi } from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
import {
FetchProjectsResponse,
Project,
FetchAnalysesResponse,
Analysis,
} from './types';
export const codesceneApiRef = createApiRef<CodeSceneApi>({
id: 'plugin.codescene.service',
});
export interface CodeSceneApi {
fetchProjects(): Promise<FetchProjectsResponse>;
fetchProject(projectId: number): Promise<Project>;
fetchAnalyses(projectId: number): Promise<FetchAnalysesResponse>;
fetchLatestAnalysis(projectId: number): Promise<Analysis>;
}
type Options = {
discoveryApi: DiscoveryApi;
};
export class CodeSceneClient implements CodeSceneApi {
private readonly discoveryApi: DiscoveryApi;
constructor(options: Options) {
this.discoveryApi = options.discoveryApi;
}
async fetchProjects(): Promise<FetchProjectsResponse> {
return this.fetchFromApi('projects');
}
async fetchProject(projectId: number): Promise<Project> {
return this.fetchFromApi(`projects/${projectId}`);
}
async fetchAnalyses(projectId: number): Promise<FetchAnalysesResponse> {
return this.fetchFromApi(`projects/${projectId}/analyses`);
}
async fetchLatestAnalysis(projectId: number): Promise<Analysis> {
return this.fetchFromApi(`projects/${projectId}/analyses/latest`);
}
private async fetchFromApi(path: string): Promise<any> {
const apiUrl = await this.getApiUrl();
const res = await fetch(`${apiUrl}/${path}`);
if (!res.ok) {
throw await ResponseError.fromResponse(res);
}
return await res.json();
}
private async getApiUrl() {
const proxyUrl = await this.discoveryApi.getBaseUrl('proxy');
return `${proxyUrl}/codescene-api`;
}
}
+84
View File
@@ -0,0 +1,84 @@
/*
* Copyright 2022 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 interface Project {
id: number;
name: string;
description?: string;
project_owner?: string;
}
export interface Analysis {
id: number;
name: string;
project_id: number;
description?: string;
readable_analysis_time: string;
summary: Summary;
file_summary: FileSummary[];
high_level_metrics: HighLevelMetrics;
}
export interface Summary {
unique_issue_ids: number;
issues_filtered_as_outliers: number;
entities: number;
commits_with_issue_ids: number;
authors_count: number;
active_authors_count: number;
issues_with_cycle_time: number;
commits: number;
issue_ids_matched_to_issues: number;
issues_classed_as_defects: number;
issues_with_cost: number;
}
export interface FileSummary {
language: string;
number_of_files: number;
blank: number;
comment: number;
code: number;
}
export interface HighLevelMetrics {
current_score: number;
month_score: number;
year_score: number;
active_developers: number;
lines_of_code: number;
system_mastery: number;
code_health_weighted_average_last_month?: number;
code_health_month_worst_performer?: number;
code_health_weighted_average_current?: number;
hotspots_code_health_now_weighted_average?: number;
code_health_weighted_average_last_year?: number;
code_health_year_worst_performer?: number;
hotspots_code_health_month_weighted_average?: number;
hotspots_code_health_year_weighted_average?: number;
code_health_now_worst_performer?: number;
}
export interface FetchProjectsResponse {
page: number;
max_pages: number;
projects: Project[];
}
export interface FetchAnalysesResponse {
page: number;
max_pages: number;
analyses: Analysis[];
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 21" version="1.1">
<g id="surface1">
<path d="M 21.898438 6.167969 C 20.5 2.242188 17.015625 0.00390625 12.003906 0.00390625 C 6.988281 0.00390625 3.5 2.242188 2.101562 6.167969 C 0.894531 6.589844 0 8.871094 0 11.707031 C 0 14.855469 1.101562 17.320312 2.511719 17.320312 C 2.542969 17.320312 2.574219 17.320312 2.605469 17.316406 C 3.089844 18.292969 3.722656 19.042969 4.511719 19.585938 C 6.425781 20.90625 9.085938 20.996094 11.886719 20.996094 L 12.113281 20.996094 C 12.148438 20.996094 12.183594 20.996094 12.21875 20.996094 C 14.988281 20.996094 17.601562 20.886719 19.488281 19.585938 C 20.277344 19.042969 20.914062 18.292969 21.394531 17.316406 C 21.425781 17.320312 21.457031 17.320312 21.488281 17.320312 C 22.898438 17.320312 24 14.855469 24 11.707031 C 24 8.871094 23.105469 6.589844 21.898438 6.167969 Z M 0.46875 11.707031 C 0.46875 9.285156 1.125 7.496094 1.886719 6.847656 C 1.578125 7.9375 1.417969 9.140625 1.417969 10.453125 C 1.417969 13.144531 1.730469 15.242188 2.382812 16.824219 C 1.4375 16.660156 0.46875 14.648438 0.46875 11.707031 Z M 21.132812 16.75 C 21.066406 16.902344 21 17.046875 20.929688 17.183594 C 20.488281 18.039062 19.921875 18.703125 19.222656 19.1875 C 17.453125 20.40625 15.03125 20.535156 12.226562 20.535156 C 12.1875 20.535156 12.148438 20.535156 12.113281 20.535156 L 11.886719 20.535156 C 9.03125 20.535156 6.566406 20.425781 4.777344 19.1875 C 4.078125 18.703125 3.515625 18.039062 3.070312 17.183594 C 3 17.046875 2.933594 16.902344 2.867188 16.75 C 2.207031 15.226562 1.886719 13.15625 1.886719 10.453125 C 1.886719 9.023438 2.085938 7.730469 2.46875 6.582031 C 2.519531 6.417969 2.582031 6.257812 2.640625 6.101562 C 4.035156 2.523438 7.320312 0.484375 12 0.484375 C 16.679688 0.484375 19.964844 2.523438 21.359375 6.101562 C 21.421875 6.257812 21.480469 6.417969 21.53125 6.582031 C 21.914062 7.730469 22.113281 9.023438 22.113281 10.453125 C 22.113281 13.15625 21.792969 15.222656 21.132812 16.75 Z M 21.617188 16.824219 C 22.269531 15.246094 22.582031 13.148438 22.582031 10.453125 C 22.582031 9.140625 22.421875 7.9375 22.113281 6.847656 C 22.875 7.5 23.53125 9.289062 23.53125 11.710938 C 23.53125 14.648438 22.5625 16.660156 21.617188 16.824219 Z M 21.617188 16.824219 "/>
<path d="M 12 3.476562 C 5.652344 3.476562 2.824219 6.046875 2.824219 11.816406 C 2.824219 17.425781 5.824219 20.152344 12 20.152344 C 18.175781 20.152344 21.175781 17.425781 21.175781 11.816406 C 21.175781 6.046875 18.347656 3.476562 12 3.476562 Z M 12 19.671875 C 6.058594 19.671875 3.292969 17.175781 3.292969 11.8125 C 3.292969 8.152344 4.28125 3.953125 12 3.953125 C 19.71875 3.953125 20.707031 8.152344 20.707031 11.8125 C 20.707031 17.175781 17.941406 19.671875 12 19.671875 Z M 12 19.671875 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,147 @@
/*
* Copyright 2022 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 { EmptyState, InfoCard } from '@backstage/core-components';
import { Button, Grid, makeStyles, Typography } from '@material-ui/core';
import React, { PropsWithChildren } from 'react';
import { Analysis } from '../../api/types';
import { Gauge } from '../Gauge/Gauge';
type CodeHealthKpi = {
name: string;
description: string;
value: number;
oldValue: number;
};
const CodeHealthKpiInfoCard = ({ children }: PropsWithChildren<{}>) => {
const linkInfo = {
title: 'What does this mean?',
link: 'https://codescene.com/blog/3-code-health-kpis/',
};
return (
<InfoCard
title="Code Health KPIs"
subheader="The Code Health metric identifies source code that is expensive and risky to maintain."
deepLink={linkInfo}
>
{children}
</InfoCard>
);
};
const useStyles = makeStyles({
root: {
height: '100%',
width: 100,
margin: '12.5px 0px 0px 50px',
},
});
export const CodeHealthKpisCard = ({
codesceneHost,
analysis,
}: {
codesceneHost: string;
analysis: Analysis;
}): JSX.Element => {
const classes = useStyles();
if (
!analysis.high_level_metrics.hotspots_code_health_now_weighted_average ||
!analysis.high_level_metrics.hotspots_code_health_month_weighted_average ||
!analysis.high_level_metrics.code_health_weighted_average_current ||
!analysis.high_level_metrics.code_health_weighted_average_last_month ||
!analysis.high_level_metrics.code_health_now_worst_performer ||
!analysis.high_level_metrics.code_health_month_worst_performer
) {
return (
<CodeHealthKpiInfoCard>
<EmptyState
missing="data"
title="Your project does not expose Code Health KPIs"
description="Enable the 'Full Code Health Scan' option and trigger an analysis to scan the health of all code in the project."
action={
<Button
color="primary"
href={`${codesceneHost}/projects/${analysis.project_id}/configuration#hotspots`}
variant="contained"
>
Configuration
</Button>
}
/>
</CodeHealthKpiInfoCard>
);
}
const hotspotCodeHealth: CodeHealthKpi = {
name: 'Hotspot Code Health',
description:
'A weighted average of the code health in your hotspots. Generally, this is the most critical metric since low code health in a hotspot will be expensive.',
value:
analysis.high_level_metrics.hotspots_code_health_now_weighted_average,
oldValue:
analysis.high_level_metrics.hotspots_code_health_month_weighted_average,
};
const averageCodeHealth: CodeHealthKpi = {
name: 'Average Code Health',
description:
'A weighted average of all the files in the codebase. This KPI indicates how deep any potential code health issues go.',
value: analysis.high_level_metrics.code_health_weighted_average_current,
oldValue:
analysis.high_level_metrics.code_health_weighted_average_last_month,
};
const worstPerformer: CodeHealthKpi = {
name: 'Worst Performer',
description:
'A single file code health score representing the lowest code health in any module across the codebase. Points out long-term risks.',
value: analysis.high_level_metrics.code_health_now_worst_performer,
oldValue: analysis.high_level_metrics.code_health_month_worst_performer,
};
const kpis = [hotspotCodeHealth, averageCodeHealth, worstPerformer];
const cards = kpis.map(kpi => {
const lastMonthText =
Math.abs(kpi.oldValue - kpi.value) < 0.1
? undefined
: `(from ${kpi.oldValue} last month)`;
return (
<div key={kpi.name}>
<Grid
container
spacing={2}
direction="row"
wrap="nowrap"
justifyContent="space-between"
alignItems="center"
>
<Grid item md={4}>
<div className={classes.root}>
<Gauge value={kpi.value} max={10} tooltipText={kpi.description} />
</div>
</Grid>
<Grid item md={8}>
<Typography variant="body1">{kpi.name}</Typography>
<Typography variant="h6">
<b>{Math.round(kpi.value * 10) / 10}</b> {lastMonthText}
</Typography>
</Grid>
</Grid>
</div>
);
});
return <CodeHealthKpiInfoCard>{cards}</CodeHealthKpiInfoCard>;
};
@@ -0,0 +1,69 @@
/*
* Copyright 2022 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 { CodeScenePageComponent } from './CodeScenePageComponent';
import { ThemeProvider } from '@material-ui/core';
import { lightTheme } from '@backstage/theme';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { rootRouteRef } from '../../routes';
import {
setupRequestMockHandlers,
renderInTestApp,
TestApiRegistry,
} from '@backstage/test-utils';
import { codesceneApiRef, CodeSceneClient } from '../../api/api';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { ApiProvider } from '@backstage/core-app-api';
describe('CodeScenePageComponent', () => {
const server = setupServer();
// Enable sane handlers for network requests
setupRequestMockHandlers(server);
let apis: TestApiRegistry;
// setup mock response
beforeEach(() => {
server.use(
rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))),
);
const discoveryApi: DiscoveryApi = {
async getBaseUrl(pluginId) {
return `http://base.com/${pluginId}`;
},
};
apis = TestApiRegistry.from([
codesceneApiRef,
new CodeSceneClient({ discoveryApi }),
]);
});
it('should render', async () => {
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<ThemeProvider theme={lightTheme}>
<CodeScenePageComponent />
</ThemeProvider>
</ApiProvider>,
{
mountedRoutes: {
'/codescene': rootRouteRef,
},
},
);
expect(rendered.getByText('CodeScene')).toBeInTheDocument();
});
});
@@ -0,0 +1,43 @@
/*
* Copyright 2022 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 { Grid } from '@material-ui/core';
import {
Header,
Page,
Content,
SupportButton,
} from '@backstage/core-components';
import { ProjectsComponent } from '../ProjectsComponent';
export const CodeScenePageComponent = () => (
<Page themeId="tool">
<Header
title="CodeScene"
subtitle="See hidden risks and social patterns in your code. Prioritize and reduce
technical debt."
>
<SupportButton />
</Header>
<Content>
<Grid container spacing={3} direction="column">
<Grid item>
<ProjectsComponent />
</Grid>
</Grid>
</Content>
</Page>
);
@@ -0,0 +1,16 @@
/*
* Copyright 2022 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 { CodeScenePageComponent } from './CodeScenePageComponent';
@@ -0,0 +1,109 @@
/*
* Copyright 2022 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 { CodeSceneProjectDetailsPage } from './CodeSceneProjectDetailsPage';
import { ThemeProvider } from '@material-ui/core';
import { lightTheme } from '@backstage/theme';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import {
setupRequestMockHandlers,
renderInTestApp,
TestApiRegistry,
} from '@backstage/test-utils';
import { CodeSceneApi, codesceneApiRef } from '../../api/api';
import { ApiProvider } from '@backstage/core-app-api';
import { Analysis } from '../../api/types';
import { ConfigReader } from '@backstage/config';
import { configApiRef } from '@backstage/core-plugin-api';
jest.mock('react-router-dom', () => {
return {
...(jest.requireActual('react-router-dom') as any),
useParams: () => ({
projectId: 123,
}),
};
});
describe('CodesceneProjectDetailsPage', () => {
const server = setupServer();
// Enable sane handlers for network requests
setupRequestMockHandlers(server);
let apis: TestApiRegistry;
// setup mock response
beforeEach(() => {
server.use(
rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))),
);
const config = new ConfigReader({
codescene: {
baseUrl: 'www.fake-url.com',
},
});
const analysis: Analysis = {
id: 1,
name: 'test-project',
project_id: 123,
readable_analysis_time: '2022-03-22',
summary: {
unique_issue_ids: 0,
issues_filtered_as_outliers: 0,
entities: 0,
commits_with_issue_ids: 0,
authors_count: 0,
active_authors_count: 0,
issues_with_cycle_time: 0,
commits: 0,
issue_ids_matched_to_issues: 0,
issues_classed_as_defects: 0,
issues_with_cost: 0,
},
file_summary: [],
high_level_metrics: {
current_score: 0,
month_score: 0,
year_score: 0,
active_developers: 0,
lines_of_code: 0,
system_mastery: 0,
},
};
apis = TestApiRegistry.from(
[
codesceneApiRef,
{
fetchLatestAnalysis: jest.fn(() => analysis),
} as unknown as CodeSceneApi,
],
[configApiRef, config],
);
});
it('should render', async () => {
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<ThemeProvider theme={lightTheme}>
<CodeSceneProjectDetailsPage />
</ThemeProvider>
</ApiProvider>,
);
expect(rendered.getByText('CodeScene: test-project')).toBeInTheDocument();
});
});
@@ -0,0 +1,158 @@
/*
* Copyright 2022 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 {
Content,
EmptyState,
GaugeCard,
Header,
InfoCard,
Page,
Progress,
SupportButton,
Table,
TableColumn,
} from '@backstage/core-components';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
import { Grid, Typography } from '@material-ui/core';
import Alert from '@material-ui/lab/Alert';
import React from 'react';
import { useParams } from 'react-router-dom';
import useAsync from 'react-use/lib/useAsync';
import { codesceneApiRef } from '../../api/api';
import { Analysis } from '../../api/types';
import { CodeHealthKpisCard } from '../CodeHealthKpisCard/CodeHealthKpisCard';
export const CodeSceneProjectDetailsPage = () => {
const params = useParams();
const projectId = Number(params.projectId);
const codesceneApi = useApi(codesceneApiRef);
const config = useApi(configApiRef);
const {
value: analysis,
loading,
error,
} = useAsync(async (): Promise<Analysis> => {
return await codesceneApi.fetchLatestAnalysis(projectId);
}, []);
if (loading) {
return <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
} else if (!analysis) {
return (
<EmptyState
missing="content"
title="CodeScene analysis"
description={`No analysis found for project with id ${params.projectId}`}
/>
);
}
const columns: TableColumn[] = [
{
title: 'Language',
field: 'language',
highlight: true,
},
{
title: 'Files',
field: 'number_of_files',
type: 'numeric',
},
{
title: 'Code',
field: 'code',
type: 'numeric',
},
{
title: 'Comment',
field: 'comment',
type: 'numeric',
},
];
const fileSummaryTable = (
<Table
options={{ paging: false, padding: 'dense' }}
data={analysis.file_summary.sort((a, b) => b.code - a.code)}
columns={columns}
title="File Summary"
/>
);
const codesceneHost = config.getString('codescene.baseUrl');
const analysisPath = `${codesceneHost}/${projectId}/analyses/${analysis.id}`;
const analysisLink = { title: 'Check the analysis', link: analysisPath };
const analysisBody = (
<>
<Typography variant="body1">
<b>Active authors</b>: {analysis.summary.active_authors_count}
</Typography>
<Typography variant="body1">
<b>Total authors</b>: {analysis.summary.authors_count}
</Typography>
<Typography variant="body1">
<b>Commits</b>: {analysis.summary.commits}
</Typography>
</>
);
return (
<Page themeId="tool">
<Header
title={`CodeScene: ${analysis.name}`}
subtitle={`Last analysis: ${analysis.readable_analysis_time}`}
>
<SupportButton>
See hidden risks and social patterns in your code. Prioritize and
reduce technical debt.
</SupportButton>
</Header>
<Content>
<Grid container spacing={2}>
<Grid item xs sm md>
<CodeHealthKpisCard
codesceneHost={codesceneHost}
analysis={analysis}
/>
</Grid>
<Grid item>
<GaugeCard
title="System Mastery"
progress={analysis.high_level_metrics.system_mastery / 100}
/>
</Grid>
<Grid item>
<GaugeCard
title="Current Score"
progress={analysis.high_level_metrics.current_score / 10}
/>
</Grid>
<Grid item style={{ minWidth: '460px' }}>
<InfoCard title="Analysis" deepLink={analysisLink}>
{analysisBody}
</InfoCard>
</Grid>
</Grid>
<Grid container spacing={2} direction="column">
<Grid item>{fileSummaryTable}</Grid>
</Grid>
</Content>
</Page>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2022 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 { CodeSceneProjectDetailsPage } from './CodeSceneProjectDetailsPage';
@@ -0,0 +1,142 @@
/*
* Copyright 2022 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 { BackstagePalette, BackstageTheme } from '@backstage/theme';
import { makeStyles, useTheme } from '@material-ui/core/styles';
import Tooltip from '@material-ui/core/Tooltip';
import { Circle } from 'rc-progress';
import React, { useEffect, useState } from 'react';
const useStyles = makeStyles<BackstageTheme>(
theme => ({
root: {
position: 'relative',
lineHeight: 0,
},
overlay: {
position: 'absolute',
top: '50%',
left: '60%',
transform: 'translate(-50%, -60%)',
fontSize: 30,
fontWeight: 'bold',
color: theme.palette.textContrast,
},
circle: {
width: '100%',
transform: 'translate(10%, 0)',
},
colorUnknown: {},
}),
{ name: 'CodeSceneGauge' },
);
/** @public */
export type GaugeProps = {
value: number;
max: number;
tooltipDelay?: number;
tooltipText: string;
};
/** @public */
export type GaugePropsGetColorOptions = {
palette: BackstagePalette;
value: number;
max: number;
};
/** @public */
export type GaugePropsGetColor = (args: GaugePropsGetColorOptions) => string;
export const getProgressColor: GaugePropsGetColor = ({
palette,
value,
max,
}) => {
if (isNaN(value)) {
return '#ddd';
}
if (value < max / 3) {
return palette.status.error;
} else if (value < max * (2 / 3)) {
return palette.status.warning;
}
return palette.status.ok;
};
/**
* Circular Progress Bar
*
* @public
*
*/
export function Gauge(props: GaugeProps) {
const classes = useStyles(props);
const { palette } = useTheme<BackstageTheme>();
const [hoverRef, setHoverRef] = useState<HTMLDivElement | null>(null);
const [open, setOpen] = useState(false);
const { value, max, tooltipDelay, tooltipText } = {
...props,
};
useEffect(() => {
const node = hoverRef;
const handleMouseOver = () => setOpen(true);
const handleMouseOut = () => setOpen(false);
if (node && tooltipText) {
node.addEventListener('mouseenter', handleMouseOver);
node.addEventListener('mouseleave', handleMouseOut);
return () => {
node.removeEventListener('mouseenter', handleMouseOver);
node.removeEventListener('mouseleave', handleMouseOut);
};
}
return () => {
setOpen(false);
};
}, [tooltipText, hoverRef]);
const asPercentage = (value * 100) / max;
return (
<div ref={setHoverRef} className={classes.root}>
<Tooltip
id="gauge-tooltip"
title={tooltipText}
placement="top"
leaveDelay={tooltipDelay}
onClose={() => setOpen(false)}
open={open}
>
<div>
<Circle
strokeLinecap="butt"
percent={asPercentage}
strokeWidth={12}
trailWidth={12}
strokeColor={getProgressColor({ palette, value, max })}
className={classes.circle}
/>
<div className={classes.overlay}>
{isNaN(value) ? 'N/A' : `${Math.round(value * 10) / 10}`}
</div>
</div>
</Tooltip>
</div>
);
}
@@ -0,0 +1,106 @@
/*
* Copyright 2022 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 { ProjectsComponent } from './ProjectsComponent';
import { ThemeProvider } from '@material-ui/core';
import { lightTheme } from '@backstage/theme';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { rootRouteRef } from '../../routes';
import {
setupRequestMockHandlers,
renderInTestApp,
TestApiRegistry,
} from '@backstage/test-utils';
import { CodeSceneApi, codesceneApiRef } from '../../api/api';
import { ApiProvider } from '@backstage/core-app-api';
import { FetchProjectsResponse, Analysis } from '../../api/types';
describe('ProjectsComponent', () => {
const server = setupServer();
// Enable sane handlers for network requests
setupRequestMockHandlers(server);
let apis: TestApiRegistry;
// setup mock response
beforeEach(() => {
server.use(
rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))),
);
const projectsResponse: FetchProjectsResponse = {
page: 1,
max_pages: 1,
projects: [
{
id: 123,
name: 'test-project',
},
],
};
const analysis: Analysis = {
id: 1,
name: 'test-project',
project_id: 123,
readable_analysis_time: '2022-03-22',
summary: {
unique_issue_ids: 0,
issues_filtered_as_outliers: 0,
entities: 0,
commits_with_issue_ids: 0,
authors_count: 0,
active_authors_count: 0,
issues_with_cycle_time: 0,
commits: 0,
issue_ids_matched_to_issues: 0,
issues_classed_as_defects: 0,
issues_with_cost: 0,
},
file_summary: [],
high_level_metrics: {
current_score: 0,
month_score: 0,
year_score: 0,
active_developers: 0,
lines_of_code: 0,
system_mastery: 0,
},
};
apis = TestApiRegistry.from([
codesceneApiRef,
{
fetchProjects: jest.fn(() => projectsResponse),
fetchLatestAnalysis: jest.fn(() => analysis),
} as unknown as CodeSceneApi,
]);
});
it('should render', async () => {
const rendered = await renderInTestApp(
<ApiProvider apis={apis}>
<ThemeProvider theme={lightTheme}>
<ProjectsComponent />
</ThemeProvider>
</ApiProvider>,
{
mountedRoutes: {
'/codescene': rootRouteRef,
},
},
);
expect(await rendered.findByText('Projects')).toBeInTheDocument();
expect(await rendered.findByText('test-project')).toBeInTheDocument();
});
});
@@ -0,0 +1,178 @@
/*
* Copyright 2022 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 {
Content,
ContentHeader,
Progress,
ItemCardHeader,
} from '@backstage/core-components';
import { useRouteRef, useApi } from '@backstage/core-plugin-api';
import Alert from '@material-ui/lab/Alert';
import useAsync from 'react-use/lib/useAsync';
import { Link as RouterLink } from 'react-router-dom';
import { rootRouteRef } from '../../routes';
import { codesceneApiRef } from '../../api/api';
import { Project, Analysis } from '../../api/types';
import {
Grid,
Card,
CardActionArea,
Input,
makeStyles,
CardContent,
Chip,
CardActions,
} from '@material-ui/core';
const useStyles = makeStyles(() => ({
overflowXScroll: {
overflowX: 'scroll',
},
}));
function matchFilter(filter?: string): (entry: Project) => boolean {
const terms = filter
?.toLocaleLowerCase('en-US')
.split(/\s/)
.map(e => e.trim())
.filter(Boolean);
if (!terms?.length) {
return () => true;
}
return entry => {
const text = `${entry.name} ${entry.description || ''}`.toLocaleLowerCase(
'en-US',
);
return terms.every(term => text.includes(term));
};
}
function topLanguages(analysis: Analysis, coerceAtMost: number): string[] {
return analysis.file_summary
.sort((a, b) => b.code - a.code)
.map(fileSummary => fileSummary.language)
.slice(0, coerceAtMost);
}
type ProjectAndAnalysis = {
project: Project;
analysis?: Analysis;
};
export const ProjectsComponent = () => {
const routeRef = useRouteRef(rootRouteRef);
const codesceneApi = useApi(codesceneApiRef);
const classes = useStyles();
const [searchText, setSearchText] = React.useState('');
const {
value: projectsWithAnalyses,
loading,
error,
} = useAsync(async (): Promise<Record<number, ProjectAndAnalysis>> => {
const projects = (await codesceneApi.fetchProjects()).projects;
const promises = projects.map(project =>
codesceneApi.fetchLatestAnalysis(project.id),
);
// analyses associates project IDs with their latests analysis
const analyses: Record<number, Analysis> = await Promise.allSettled(
promises,
).then(results => {
return results
.filter(result => result.status === 'fulfilled')
.map(result => result as PromiseFulfilledResult<Analysis>)
.map(result => result.value)
.reduce(
(acc, analysis) => ({ ...acc, [analysis.project_id]: analysis }),
{},
);
});
return projects.reduce(
(acc, project) => ({
...acc,
[project.id]: {
project: project,
analysis: analyses[project.id],
},
}),
{},
);
}, []);
if (loading) {
return <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
} else if (
!projectsWithAnalyses ||
Object.keys(projectsWithAnalyses).length === 0
) {
return <Alert severity="error">No projects found!</Alert>;
}
const projects = Object.values(projectsWithAnalyses)
.map(p => p.project)
.sort((a, b) => a.name.localeCompare(b.name));
const cards = projects.filter(matchFilter(searchText)).map(project => {
const analysis = projectsWithAnalyses[project.id].analysis;
const subtitle = analysis
? `Last analysis: ${analysis.readable_analysis_time} · Score: ${analysis.high_level_metrics.current_score} · Active authors: ${analysis.high_level_metrics.active_developers}`
: undefined;
const chips = analysis
? topLanguages(analysis, 3).map(lang => (
<Chip label={lang} key={lang} size="small" />
))
: undefined;
return (
<Grid key={project.id} item xs={3}>
<Card>
<CardActionArea
style={{
height: '100%',
overflow: 'hidden',
width: '100%',
}}
component={RouterLink}
to={`${routeRef()}/${project.id}`}
>
<ItemCardHeader title={project.name} />
<CardContent>{subtitle}</CardContent>
<CardActions disableSpacing>{chips}</CardActions>
</CardActionArea>
</Card>
</Grid>
);
});
return (
<Content className={classes.overflowXScroll}>
<ContentHeader title="Projects">
<Input
id="projects-filter"
type="search"
placeholder="Filter"
onChange={e => setSearchText(e.target.value)}
/>
</ContentHeader>
<Grid container>{cards}</Grid>
</Content>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2022 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 { ProjectsComponent } from './ProjectsComponent';
+28
View File
@@ -0,0 +1,28 @@
/*
* Copyright 2022 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 {
codescenePlugin,
CodescenePage,
CodesceneProjectDetailsPage,
} from './plugin';
import CodeSceneIconComponent from './assets/codescene.icon.svg';
import { IconComponent } from '@backstage/core-plugin-api';
/**
* @public
*/
export const CodeSceneIcon: IconComponent =
CodeSceneIconComponent as IconComponent;
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2022 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 { codescenePlugin } from './plugin';
describe('codescene', () => {
it('should export plugin', () => {
expect(codescenePlugin).toBeDefined();
});
});
+69
View File
@@ -0,0 +1,69 @@
/*
* Copyright 2022 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 {
createPlugin,
createRoutableExtension,
createApiFactory,
discoveryApiRef,
} from '@backstage/core-plugin-api';
import { codesceneApiRef, CodeSceneClient } from './api/api';
import { rootRouteRef, projectDetailsRouteRef } from './routes';
/**
* @public
*/
export const codescenePlugin = createPlugin({
id: 'codescene',
apis: [
createApiFactory({
api: codesceneApiRef,
deps: { discoveryApi: discoveryApiRef },
factory: ({ discoveryApi }) => new CodeSceneClient({ discoveryApi }),
}),
],
routes: {
root: rootRouteRef,
projectPage: projectDetailsRouteRef,
},
});
/**
* @public
*/
export const CodescenePage = codescenePlugin.provide(
createRoutableExtension({
name: 'CodescenePage',
component: () =>
import('./components/CodeScenePageComponent').then(
m => m.CodeScenePageComponent,
),
mountPoint: rootRouteRef,
}),
);
/**
* @public
*/
export const CodesceneProjectDetailsPage = codescenePlugin.provide(
createRoutableExtension({
name: 'CodesceneProjectDetailsPage',
component: () =>
import('./components/CodeSceneProjectDetailsPage').then(
m => m.CodeSceneProjectDetailsPage,
),
mountPoint: projectDetailsRouteRef,
}),
);
+25
View File
@@ -0,0 +1,25 @@
/*
* Copyright 2022 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 { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
id: 'codescene',
});
export const projectDetailsRouteRef = createRouteRef({
id: 'codescene-project-page',
params: ['projectId'],
});
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2022 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 '@testing-library/jest-dom';
import 'cross-fetch/polyfill';
+1
View File
@@ -243,6 +243,7 @@ const NO_WARNING_PACKAGES = [
'plugins/catalog-common',
'plugins/catalog-graph',
'plugins/catalog-react',
'plugins/codescene',
'plugins/graphiql',
'plugins/org',
'plugins/periskop',