Merge pull request #10777 from julioz/codescene-plugin
Add CodeScene plugin
This commit is contained in:
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,64 @@
|
||||
# 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.
|
||||
|
||||

|
||||
|
||||
The CodeScene Backstage Plugin exposes a page component that will list the existing projects and their analysis data on your CodeScene instance.
|
||||
|
||||

|
||||
|
||||
## 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}
|
||||
```
|
||||
|
||||
```yaml
|
||||
codescene:
|
||||
baseUrl: https://codescene.my-company.net # replace with your own URL
|
||||
```
|
||||
@@ -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)
|
||||
```
|
||||
Vendored
+27
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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 |
@@ -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.1-next.0",
|
||||
"@backstage/core-app-api": "^1.0.1-next.0",
|
||||
"@backstage/dev-utils": "^1.0.2-next.0",
|
||||
"@backstage/test-utils": "^1.0.2-next.0",
|
||||
"@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"
|
||||
}
|
||||
@@ -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`;
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
};
|
||||
+69
@@ -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';
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
+158
@@ -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,137 @@
|
||||
/*
|
||||
* 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' },
|
||||
);
|
||||
|
||||
export type GaugeProps = {
|
||||
value: number;
|
||||
max: number;
|
||||
tooltipDelay?: number;
|
||||
tooltipText: string;
|
||||
};
|
||||
|
||||
export type GaugePropsGetColorOptions = {
|
||||
palette: BackstagePalette;
|
||||
value: number;
|
||||
max: number;
|
||||
};
|
||||
|
||||
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
|
||||
*
|
||||
*/
|
||||
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';
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
@@ -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'],
|
||||
});
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user