Add DORA metrics insights to GoCD builds page
Signed-off-by: Julio Zynger <julio.zynger@soundcloud.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-gocd': patch
|
||||
---
|
||||
|
||||
Add DORA metrics insights to GoCD builds page
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 192 KiB |
@@ -101,6 +101,23 @@ export enum GoCdBuildResultStatus {
|
||||
pending,
|
||||
}
|
||||
|
||||
export const toBuildResultStatus = (status: string): GoCdBuildResultStatus => {
|
||||
switch (status.toLocaleLowerCase('en-US')) {
|
||||
case 'passed':
|
||||
return GoCdBuildResultStatus.successful;
|
||||
case 'failed':
|
||||
return GoCdBuildResultStatus.error;
|
||||
case 'aborted':
|
||||
return GoCdBuildResultStatus.aborted;
|
||||
case 'building':
|
||||
return GoCdBuildResultStatus.running;
|
||||
case 'pending':
|
||||
return GoCdBuildResultStatus.pending;
|
||||
default:
|
||||
return GoCdBuildResultStatus.aborted;
|
||||
}
|
||||
};
|
||||
|
||||
export interface GoCdBuildStageResult {
|
||||
status: GoCdBuildResultStatus;
|
||||
text: string;
|
||||
|
||||
@@ -27,6 +27,7 @@ import { useApi, configApiRef } from '@backstage/core-plugin-api';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { gocdApiRef } from '../../plugin';
|
||||
import { GoCdBuildsTable } from '../GoCdBuildsTable/GoCdBuildsTable';
|
||||
import { GoCdBuildsInsights } from '../GoCdBuildsInsights/GoCdBuildsInsights';
|
||||
import { GoCdApiError, PipelineHistory } from '../../api/gocdApi.model';
|
||||
import { Item, Select } from '../Select';
|
||||
|
||||
@@ -120,6 +121,11 @@ export const GoCdBuildsComponent = (): JSX.Element => {
|
||||
items={getSelectionItems(rawPipelines)}
|
||||
/>
|
||||
</ContentHeader>
|
||||
<GoCdBuildsInsights
|
||||
pipelineHistory={pipelineHistory}
|
||||
loading={loading}
|
||||
error={error}
|
||||
/>
|
||||
<GoCdBuildsTable
|
||||
goCdBaseUrl={config.getString('gocd.baseUrl')}
|
||||
pipelineHistory={pipelineHistory}
|
||||
|
||||
@@ -0,0 +1,417 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import {
|
||||
GoCdBuildsInsights,
|
||||
GoCdBuildsInsightsProps,
|
||||
} from './GoCdBuildsInsights';
|
||||
|
||||
const minProps: GoCdBuildsInsightsProps = {
|
||||
loading: false,
|
||||
error: undefined,
|
||||
pipelineHistory: {
|
||||
_links: { next: { href: 'some-href' } },
|
||||
pipelines: [
|
||||
{
|
||||
name: 'fortune',
|
||||
counter: 278,
|
||||
label: '278-8ab3',
|
||||
natural_order: 278.0,
|
||||
can_run: true,
|
||||
preparing_to_schedule: false,
|
||||
comment: null,
|
||||
scheduled_date: 1643019617380,
|
||||
build_cause: {
|
||||
trigger_message: 'Forced by api',
|
||||
trigger_forced: true,
|
||||
approver: 'api',
|
||||
material_revisions: [
|
||||
{
|
||||
changed: false,
|
||||
material: {
|
||||
name: 'git',
|
||||
fingerprint: 'print',
|
||||
type: 'Git',
|
||||
description:
|
||||
'URL: git@github.com:fake-org/fortune.git, Branch: master',
|
||||
},
|
||||
modifications: [
|
||||
{
|
||||
revision: '85d5debb5a93e5d43c7a695',
|
||||
modified_time: 1642177395000,
|
||||
user_name: 'User Name',
|
||||
comment: 'This is a fake build',
|
||||
email_address: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
stages: [
|
||||
{
|
||||
result: 'Passed',
|
||||
status: 'Passed',
|
||||
rerun_of_counter: null,
|
||||
name: 'init',
|
||||
counter: '1',
|
||||
scheduled: true,
|
||||
approval_type: 'success',
|
||||
approved_by: 'api',
|
||||
operate_permission: true,
|
||||
can_run: true,
|
||||
jobs: [
|
||||
{
|
||||
name: 'package-publish',
|
||||
scheduled_date: 1643019617380,
|
||||
state: 'Completed',
|
||||
result: 'Passed',
|
||||
},
|
||||
{
|
||||
name: 'publish-deploy-replicas',
|
||||
scheduled_date: 1643019617380,
|
||||
state: 'Completed',
|
||||
result: 'Passed',
|
||||
},
|
||||
{
|
||||
name: 'lint',
|
||||
scheduled_date: 1643019617380,
|
||||
state: 'Completed',
|
||||
result: 'Passed',
|
||||
},
|
||||
{
|
||||
name: 'test-all',
|
||||
scheduled_date: 1643019617380,
|
||||
state: 'Completed',
|
||||
result: 'Passed',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
result: 'Passed',
|
||||
status: 'Passed',
|
||||
rerun_of_counter: null,
|
||||
name: 'canary',
|
||||
counter: '1',
|
||||
scheduled: true,
|
||||
approval_type: 'success',
|
||||
approved_by: 'changes',
|
||||
operate_permission: true,
|
||||
can_run: true,
|
||||
jobs: [
|
||||
{
|
||||
name: 'canary',
|
||||
scheduled_date: 1643019690373,
|
||||
state: 'Completed',
|
||||
result: 'Passed',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
result: 'Passed',
|
||||
status: 'Passed',
|
||||
rerun_of_counter: null,
|
||||
name: 'deploy',
|
||||
counter: '1',
|
||||
scheduled: true,
|
||||
approval_type: 'success',
|
||||
approved_by: 'changes',
|
||||
operate_permission: true,
|
||||
can_run: true,
|
||||
jobs: [
|
||||
{
|
||||
name: 'promote-and-deploy',
|
||||
scheduled_date: 1643020668387,
|
||||
state: 'Completed',
|
||||
result: 'Passed',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'fortune',
|
||||
counter: 277,
|
||||
label: '277-8ab3',
|
||||
natural_order: 277.0,
|
||||
can_run: true,
|
||||
preparing_to_schedule: false,
|
||||
comment: null,
|
||||
scheduled_date: 1642177416721,
|
||||
build_cause: {
|
||||
trigger_message: 'modified by User Name',
|
||||
trigger_forced: false,
|
||||
approver: 'changes',
|
||||
material_revisions: [
|
||||
{
|
||||
changed: true,
|
||||
material: {
|
||||
name: 'git',
|
||||
fingerprint: '23feea5b59ef40cc7d43ac46bf08e3718',
|
||||
type: 'Git',
|
||||
description:
|
||||
'URL: git@github.com:fake-org/fortune.git, Branch: master',
|
||||
},
|
||||
modifications: [
|
||||
{
|
||||
revision: '85d53e5d43c7a695',
|
||||
modified_time: 1642177395000,
|
||||
user_name: 'User Name',
|
||||
comment: 'Update README',
|
||||
email_address: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
stages: [
|
||||
{
|
||||
result: 'Passed',
|
||||
status: 'Passed',
|
||||
rerun_of_counter: null,
|
||||
name: 'init',
|
||||
counter: '1',
|
||||
scheduled: true,
|
||||
approval_type: 'success',
|
||||
approved_by: 'changes',
|
||||
operate_permission: true,
|
||||
can_run: true,
|
||||
jobs: [
|
||||
{
|
||||
name: 'test-all',
|
||||
scheduled_date: 1642177416721,
|
||||
state: 'Completed',
|
||||
result: 'Passed',
|
||||
},
|
||||
{
|
||||
name: 'lint',
|
||||
scheduled_date: 1642177416721,
|
||||
state: 'Completed',
|
||||
result: 'Passed',
|
||||
},
|
||||
{
|
||||
name: 'publish-deploy-replicas',
|
||||
scheduled_date: 1642377416721,
|
||||
state: 'Completed',
|
||||
result: 'Failed',
|
||||
},
|
||||
{
|
||||
name: 'package-publish',
|
||||
scheduled_date: 1642177416721,
|
||||
state: 'Completed',
|
||||
result: 'Passed',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
result: 'Passed',
|
||||
status: 'Passed',
|
||||
rerun_of_counter: null,
|
||||
name: 'canary',
|
||||
counter: '1',
|
||||
scheduled: true,
|
||||
approval_type: 'success',
|
||||
approved_by: 'changes',
|
||||
operate_permission: true,
|
||||
can_run: true,
|
||||
jobs: [
|
||||
{
|
||||
name: 'canary',
|
||||
scheduled_date: 1642177485728,
|
||||
state: 'Completed',
|
||||
result: 'Passed',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
result: 'Passed',
|
||||
status: 'Passed',
|
||||
rerun_of_counter: null,
|
||||
name: 'deploy',
|
||||
counter: '1',
|
||||
scheduled: true,
|
||||
approval_type: 'success',
|
||||
approved_by: 'changes',
|
||||
operate_permission: true,
|
||||
can_run: true,
|
||||
jobs: [
|
||||
{
|
||||
name: 'promote-and-deploy',
|
||||
scheduled_date: 1642178467479,
|
||||
state: 'Completed',
|
||||
result: 'Passed',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'fortune',
|
||||
counter: 277,
|
||||
label: '277-8ab3',
|
||||
natural_order: 277.0,
|
||||
can_run: true,
|
||||
preparing_to_schedule: false,
|
||||
comment: null,
|
||||
scheduled_date: 1642177416721,
|
||||
build_cause: {
|
||||
trigger_message: 'modified by User Name',
|
||||
trigger_forced: false,
|
||||
approver: 'changes',
|
||||
material_revisions: [
|
||||
{
|
||||
changed: true,
|
||||
material: {
|
||||
name: 'git',
|
||||
fingerprint: '23feea5b59ef40cc7d43ac46bf08e3718',
|
||||
type: 'Git',
|
||||
description:
|
||||
'URL: git@github.com:fake-org/fortune.git, Branch: master',
|
||||
},
|
||||
modifications: [
|
||||
{
|
||||
revision: '85d53e5d43c7a695',
|
||||
modified_time: 1642177395000,
|
||||
user_name: 'User Name',
|
||||
comment: 'Update README',
|
||||
email_address: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
stages: [
|
||||
{
|
||||
result: 'Passed',
|
||||
status: 'Passed',
|
||||
rerun_of_counter: null,
|
||||
name: 'init',
|
||||
counter: '1',
|
||||
scheduled: true,
|
||||
approval_type: 'success',
|
||||
approved_by: 'changes',
|
||||
operate_permission: true,
|
||||
can_run: true,
|
||||
jobs: [
|
||||
{
|
||||
name: 'test-all',
|
||||
scheduled_date: 1642174416721,
|
||||
state: 'Completed',
|
||||
result: 'Failed',
|
||||
},
|
||||
{
|
||||
name: 'lint',
|
||||
scheduled_date: 1642177416721,
|
||||
state: 'Completed',
|
||||
result: 'Passed',
|
||||
},
|
||||
{
|
||||
name: 'publish-deploy-replicas',
|
||||
scheduled_date: 1642177416721,
|
||||
state: 'Completed',
|
||||
result: 'Passed',
|
||||
},
|
||||
{
|
||||
name: 'package-publish',
|
||||
scheduled_date: 1642177416721,
|
||||
state: 'Completed',
|
||||
result: 'Passed',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
result: 'Passed',
|
||||
status: 'Passed',
|
||||
rerun_of_counter: null,
|
||||
name: 'canary',
|
||||
counter: '1',
|
||||
scheduled: true,
|
||||
approval_type: 'success',
|
||||
approved_by: 'changes',
|
||||
operate_permission: true,
|
||||
can_run: true,
|
||||
jobs: [
|
||||
{
|
||||
name: 'canary',
|
||||
scheduled_date: 1642177485728,
|
||||
state: 'Completed',
|
||||
result: 'Passed',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
result: 'Passed',
|
||||
status: 'Passed',
|
||||
rerun_of_counter: null,
|
||||
name: 'deploy',
|
||||
counter: '1',
|
||||
scheduled: true,
|
||||
approval_type: 'success',
|
||||
approved_by: 'changes',
|
||||
operate_permission: true,
|
||||
can_run: true,
|
||||
jobs: [
|
||||
{
|
||||
name: 'promote-and-deploy',
|
||||
scheduled_date: 1642178467479,
|
||||
state: 'Completed',
|
||||
result: 'Passed',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe('<GoCdBuildsInsights />', () => {
|
||||
it('renders without exploding', () => {
|
||||
const { getByText } = render(<GoCdBuildsInsights {...minProps} />);
|
||||
expect(getByText('Run Frequency')).toBeInTheDocument();
|
||||
expect(getByText('Mean Time to Recovery')).toBeInTheDocument();
|
||||
expect(getByText('Mean Time Between Failures')).toBeInTheDocument();
|
||||
expect(getByText('Failure Rate')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders mean time to recovery', () => {
|
||||
const { getByText } = render(<GoCdBuildsInsights {...minProps} />);
|
||||
expect(getByText('-1d -3h -21m -40s')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders mean time between failures', () => {
|
||||
const { getByText } = render(<GoCdBuildsInsights {...minProps} />);
|
||||
expect(getByText('2d 8h 23m 20s')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders failure rate', () => {
|
||||
const { getByText } = render(<GoCdBuildsInsights {...minProps} />);
|
||||
expect(getByText('11.11%')).toBeInTheDocument();
|
||||
expect(getByText('(2 out of 18 jobs)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders nothing when loading', () => {
|
||||
const props = {
|
||||
loading: true,
|
||||
pipelineHistory: undefined,
|
||||
error: undefined,
|
||||
};
|
||||
const { getByTestId } = render(<GoCdBuildsInsights {...props} />);
|
||||
|
||||
expect(() => getByTestId('GoCdBuildsInsightsBox')).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* Copyright 2021 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 { groupBy, mean } from 'lodash';
|
||||
import {
|
||||
PipelineHistory,
|
||||
Job,
|
||||
toBuildResultStatus,
|
||||
GoCdBuildResultStatus,
|
||||
} from '../../api/gocdApi.model';
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
|
||||
export type GoCdBuildsInsightsProps = {
|
||||
pipelineHistory: PipelineHistory | undefined;
|
||||
loading: boolean;
|
||||
error: Error | undefined;
|
||||
};
|
||||
|
||||
function runFrequency(pipelineHistory: PipelineHistory): string {
|
||||
const lastMonth = DateTime.now().minus({ month: 1 });
|
||||
const buildLastMonth = pipelineHistory.pipelines
|
||||
.map(p => p.scheduled_date)
|
||||
.filter((d): d is number => !!d)
|
||||
.map(d => DateTime.fromMillis(d))
|
||||
.filter(d => d > lastMonth).length;
|
||||
return `${buildLastMonth} last month`;
|
||||
}
|
||||
|
||||
function meanTimeBetweenFailures(jobs: Job[]): string {
|
||||
const timeBetweenFailures: Duration[] = [];
|
||||
for (let index = 1; index < jobs.length; index++) {
|
||||
const job = jobs[index];
|
||||
if (!job.result) {
|
||||
continue;
|
||||
}
|
||||
if (toBuildResultStatus(job.result) === GoCdBuildResultStatus.error) {
|
||||
let previousFailedJob: Job | null = null;
|
||||
for (let j = index - 1; j >= 0; j--) {
|
||||
const candidateJob = jobs[j];
|
||||
if (!candidateJob.result) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
toBuildResultStatus(candidateJob.result) ===
|
||||
GoCdBuildResultStatus.error
|
||||
) {
|
||||
previousFailedJob = candidateJob;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (
|
||||
!previousFailedJob ||
|
||||
!job.scheduled_date ||
|
||||
!previousFailedJob.scheduled_date
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const failedJobDate = DateTime.fromMillis(job.scheduled_date);
|
||||
const previousFailedJobDate = DateTime.fromMillis(
|
||||
previousFailedJob.scheduled_date,
|
||||
);
|
||||
const timeBetweenFailure = failedJobDate.diff(previousFailedJobDate);
|
||||
timeBetweenFailures.push(timeBetweenFailure);
|
||||
}
|
||||
}
|
||||
return formatMean(timeBetweenFailures);
|
||||
}
|
||||
|
||||
/**
|
||||
* Imagine a sequence like:
|
||||
* S - S - S - F - S - F - F - S - S
|
||||
*
|
||||
* We iterate until finding a failure, and then iterate forward
|
||||
* until we find the first immediate success to then
|
||||
* calculate the time difference between the scheduling of the jobs.
|
||||
*/
|
||||
function meanTimeToRecovery(jobs: Job[]): string {
|
||||
const timeToRecoverIntervals: Duration[] = [];
|
||||
for (let index = 0; index < jobs.length; index++) {
|
||||
const job = jobs[index];
|
||||
if (!job.result) {
|
||||
continue;
|
||||
}
|
||||
if (toBuildResultStatus(job.result) === GoCdBuildResultStatus.error) {
|
||||
let nextSuccessfulJob: Job | null = null;
|
||||
for (let j = index + 1; j < jobs.length; j++) {
|
||||
const candidateJob = jobs[j];
|
||||
if (!candidateJob.result) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
toBuildResultStatus(candidateJob.result) ===
|
||||
GoCdBuildResultStatus.successful
|
||||
) {
|
||||
nextSuccessfulJob = candidateJob;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (
|
||||
!nextSuccessfulJob ||
|
||||
!job.scheduled_date ||
|
||||
!nextSuccessfulJob.scheduled_date
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const failedJobDate = DateTime.fromMillis(job.scheduled_date);
|
||||
const successfulJobDate = DateTime.fromMillis(
|
||||
nextSuccessfulJob.scheduled_date,
|
||||
);
|
||||
const timeToRecovery = successfulJobDate.diff(failedJobDate);
|
||||
timeToRecoverIntervals.push(timeToRecovery);
|
||||
}
|
||||
}
|
||||
|
||||
return formatMean(timeToRecoverIntervals);
|
||||
}
|
||||
|
||||
function formatMean(durations: Duration[]): string {
|
||||
if (durations.length === 0) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
const mttr: Duration = Duration.fromMillis(
|
||||
mean(durations.map(i => i.milliseconds)),
|
||||
);
|
||||
return mttr.toFormat("d'd' h'h' m'm' s's'");
|
||||
}
|
||||
|
||||
function failureRate(jobs: Job[]): {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
} {
|
||||
const resultGroups = new Map(
|
||||
Object.entries(groupBy(jobs, 'result')).map(([key, value]) => [
|
||||
toBuildResultStatus(key),
|
||||
value.flat(),
|
||||
]),
|
||||
);
|
||||
const failedJobs = resultGroups.get(GoCdBuildResultStatus.error);
|
||||
if (!failedJobs) {
|
||||
return {
|
||||
title: '0',
|
||||
subtitle: '(no failed jobs found)',
|
||||
};
|
||||
}
|
||||
|
||||
resultGroups.delete(GoCdBuildResultStatus.error);
|
||||
const nonFailedJobs = Array.from(resultGroups.values()).flat();
|
||||
|
||||
const totalJobs = failedJobs.length + nonFailedJobs.length;
|
||||
const percentage = (failedJobs.length / totalJobs) * 100;
|
||||
const decimalPercentage = (Math.round(percentage * 100) / 100).toFixed(2);
|
||||
return {
|
||||
title: `${decimalPercentage}%`,
|
||||
subtitle: `(${failedJobs.length} out of ${totalJobs} jobs)`,
|
||||
};
|
||||
}
|
||||
|
||||
export const GoCdBuildsInsights = (
|
||||
props: GoCdBuildsInsightsProps,
|
||||
): JSX.Element => {
|
||||
const { pipelineHistory, loading, error } = props;
|
||||
|
||||
if (loading || error || !pipelineHistory) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
// We reverse the array to calculate insights to make sure jobs are ordered
|
||||
// by their schedule date.
|
||||
const stages = pipelineHistory.pipelines
|
||||
.slice()
|
||||
.reverse()
|
||||
.map(p => p.stages)
|
||||
.flat();
|
||||
const jobs = stages.map(s => s.jobs).flat();
|
||||
|
||||
const failureRateObj: { title: string; subtitle: string } = failureRate(jobs);
|
||||
|
||||
return (
|
||||
<Box data-testid="GoCdBuildsInsightsBox" sx={{ mb: 1 }}>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<Tooltip title="What is your deployment frequency?">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="body2">Run Frequency</Typography>
|
||||
<Typography variant="h4">
|
||||
{runFrequency(pipelineHistory)}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<Tooltip title="How long does it take to fix a failure?">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="body2">Mean Time to Recovery</Typography>
|
||||
<Typography variant="h4">{meanTimeToRecovery(jobs)}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<Tooltip title="How often do changes fail?">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="body2">
|
||||
Mean Time Between Failures
|
||||
</Typography>
|
||||
<Typography variant="h4">
|
||||
{meanTimeBetweenFailures(jobs)}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<Tooltip title="What percentage of changes result in a failure?">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="body2">Failure Rate</Typography>
|
||||
<Typography variant="h4">{failureRateObj.title}</Typography>
|
||||
<Typography variant="body2">
|
||||
{failureRateObj.subtitle}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -22,6 +22,7 @@ import GitHubIcon from '@material-ui/icons/GitHub';
|
||||
import {
|
||||
GoCdBuildResult,
|
||||
GoCdBuildResultStatus,
|
||||
toBuildResultStatus,
|
||||
PipelineHistory,
|
||||
Pipeline,
|
||||
} from '../../api/gocdApi.model';
|
||||
@@ -157,23 +158,6 @@ const renderError = (error: Error): JSX.Element => {
|
||||
return <Alert severity="error">{error.message}</Alert>;
|
||||
};
|
||||
|
||||
const toStageStatus = (status: string): GoCdBuildResultStatus => {
|
||||
switch (status.toLocaleLowerCase('en-US')) {
|
||||
case 'passed':
|
||||
return GoCdBuildResultStatus.successful;
|
||||
case 'failed':
|
||||
return GoCdBuildResultStatus.error;
|
||||
case 'aborted':
|
||||
return GoCdBuildResultStatus.aborted;
|
||||
case 'building':
|
||||
return GoCdBuildResultStatus.running;
|
||||
case 'pending':
|
||||
return GoCdBuildResultStatus.pending;
|
||||
default:
|
||||
return GoCdBuildResultStatus.aborted;
|
||||
}
|
||||
};
|
||||
|
||||
const toBuildResults = (
|
||||
entity: Entity,
|
||||
builds: Pipeline[],
|
||||
@@ -185,7 +169,7 @@ const toBuildResults = (
|
||||
id: build.counter,
|
||||
source: `${entitySourceLocation}/commit/${build.build_cause?.material_revisions[0]?.modifications[0].revision}`,
|
||||
stages: build.stages.map(s => ({
|
||||
status: toStageStatus(s.status),
|
||||
status: toBuildResultStatus(s.status),
|
||||
text: s.name,
|
||||
})),
|
||||
buildSlug: `${build.name}/${build.counter}`,
|
||||
|
||||
Reference in New Issue
Block a user