Add trendline for build count and a few data fields
Signed-off-by: Niklas Granander <ngranander@spotify.com>
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 { BuildTrendComponent } from './BuildTrendComponent';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { BuildCount } from '../../api';
|
||||
|
||||
describe('BuildTrendComponent', () => {
|
||||
it('should render', async () => {
|
||||
const buildCounts: BuildCount[] = [
|
||||
{ day: '2021-01-01', errors: 10, builds: 100 },
|
||||
];
|
||||
const rendered = await renderInTestApp(
|
||||
<BuildTrendComponent buildCounts={buildCounts} />,
|
||||
);
|
||||
expect(rendered.findAllByText('Build Count')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 { TrendLine } from '@backstage/core-components';
|
||||
import { BuildCount } from '../../api';
|
||||
import { Typography, useTheme } from '@material-ui/core';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
const TRENDLINE_TITLE = 'Build Count';
|
||||
|
||||
interface BuildTrendProps {
|
||||
buildCounts: BuildCount[];
|
||||
}
|
||||
|
||||
export const BuildTrendComponent = ({ buildCounts }: BuildTrendProps) => {
|
||||
const theme = useTheme<BackstageTheme>();
|
||||
|
||||
let max = 0;
|
||||
const builds = buildCounts.map(counts => {
|
||||
max = Math.max(max, counts.builds);
|
||||
return counts.builds;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="overline">{TRENDLINE_TITLE}</Typography>
|
||||
<TrendLine
|
||||
data={builds}
|
||||
title={TRENDLINE_TITLE}
|
||||
max={max}
|
||||
color={theme.palette.primary.main}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export * from './BuildTrendComponent';
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 { DataValueComponent, DataValueGridItem } from './DataValueComponent';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
|
||||
describe('DataValueComponent', () => {
|
||||
it('should render', async () => {
|
||||
const field = 'Field';
|
||||
const value = 'Value';
|
||||
const rendered = await renderInTestApp(
|
||||
<DataValueComponent field={field} value={value} />,
|
||||
);
|
||||
expect(rendered.getByText(field)).toBeInTheDocument();
|
||||
expect(rendered.getByText(value)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('grid item should render', async () => {
|
||||
const field = 'Field';
|
||||
const value = 'Value';
|
||||
const rendered = await renderInTestApp(
|
||||
<DataValueGridItem field={field} value={value} />,
|
||||
);
|
||||
expect(rendered.getByText(field)).toBeInTheDocument();
|
||||
expect(rendered.getByText(value)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 { Grid, GridSize, Typography } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
|
||||
interface DataValueProps {
|
||||
field: string;
|
||||
value: string | number | undefined;
|
||||
}
|
||||
|
||||
export const DataValueComponent = ({ field, value }: DataValueProps) => {
|
||||
return (
|
||||
<div>
|
||||
<Typography variant="caption">{field}</Typography>
|
||||
<Typography variant="subtitle1">{value}</Typography>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface GridProps {
|
||||
xs?: GridSize;
|
||||
md?: GridSize;
|
||||
}
|
||||
|
||||
export const DataValueGridItem = (props: DataValueProps & GridProps) => (
|
||||
<Grid item xs={props.xs ?? 6} md={props.md ?? 4}>
|
||||
<DataValueComponent {...props} />
|
||||
</Grid>
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export * from './DataValueComponent';
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 { ErrorTrendComponent } from './ErrorTrendComponent';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { BuildCount } from '../../api';
|
||||
|
||||
describe('ErrorTrendComponent', () => {
|
||||
it('should render', async () => {
|
||||
const buildCounts: BuildCount[] = [
|
||||
{ day: '2021-01-01', errors: 10, builds: 100 },
|
||||
];
|
||||
const rendered = await renderInTestApp(
|
||||
<ErrorTrendComponent buildCounts={buildCounts} />,
|
||||
);
|
||||
expect(rendered.findAllByText('Error Rate')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -14,32 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Progress, TrendLine } from '@backstage/core-components';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { BuildCount, xcmetricsApiRef } from '../../api';
|
||||
import { useAsync } from 'react-use';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { TrendLine } from '@backstage/core-components';
|
||||
import { BuildCount } from '../../api';
|
||||
import { Typography, useTheme } from '@material-ui/core';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
const TRENDLINE_TITLE = 'Error Rate';
|
||||
|
||||
interface ErrorTrendProps {
|
||||
days: number;
|
||||
buildCounts: BuildCount[];
|
||||
}
|
||||
|
||||
export const ErrorTrendComponent = ({ days }: ErrorTrendProps) => {
|
||||
const client = useApi(xcmetricsApiRef);
|
||||
const { value: buildCounts, loading, error } = useAsync(
|
||||
async (): Promise<BuildCount[]> => client.getBuildCounts(days),
|
||||
[],
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
} else if (error) {
|
||||
return <Alert severity="error">{error.message}</Alert>;
|
||||
} else if (!buildCounts) {
|
||||
return <TrendLine data={Array(days).fill(0)} title={TRENDLINE_TITLE} />;
|
||||
}
|
||||
export const ErrorTrendComponent = ({ buildCounts }: ErrorTrendProps) => {
|
||||
const theme = useTheme<BackstageTheme>();
|
||||
|
||||
let max = 0;
|
||||
const averageErrors = buildCounts.map(counts => {
|
||||
@@ -49,5 +36,15 @@ export const ErrorTrendComponent = ({ days }: ErrorTrendProps) => {
|
||||
return dayAverage;
|
||||
});
|
||||
|
||||
return <TrendLine data={averageErrors} title={TRENDLINE_TITLE} max={max} />;
|
||||
return (
|
||||
<>
|
||||
<Typography variant="overline">{TRENDLINE_TITLE}</Typography>
|
||||
<TrendLine
|
||||
data={averageErrors}
|
||||
title={TRENDLINE_TITLE}
|
||||
max={max}
|
||||
color={theme.palette.status.warning}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,47 +14,59 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { OverviewComponent } from './OverviewComponent';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { xcmetricsApiRef } from '../../api';
|
||||
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
|
||||
import { mockUserId, mockXcmetricsApi } from '../../test-utils';
|
||||
import { mockUserId, createMockXcmetricsApi } from '../../test-utils';
|
||||
import { OverviewComponent } from './OverviewComponent';
|
||||
|
||||
jest.mock('../OverviewTrendsComponent', () => ({
|
||||
OverviewTrendsComponent: () => 'OverviewTrendsComponent',
|
||||
}));
|
||||
|
||||
jest.mock('../StatusMatrixComponent', () => ({
|
||||
StatusMatrixComponent: () => 'StatusMatrixComponent',
|
||||
}));
|
||||
|
||||
describe('OverviewComponent', () => {
|
||||
it('should render', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, mockXcmetricsApi)}>
|
||||
<ApiProvider
|
||||
apis={ApiRegistry.with(xcmetricsApiRef, createMockXcmetricsApi())}
|
||||
>
|
||||
<OverviewComponent />
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(rendered.getByText('XCMetrics Dashboard')).toBeInTheDocument();
|
||||
expect(rendered.getByText(mockUserId)).toBeInTheDocument();
|
||||
expect(rendered.queryByText('CI')).toBeNull();
|
||||
});
|
||||
|
||||
it('should render an empty state when no builds exist', async () => {
|
||||
mockXcmetricsApi.getBuilds = jest.fn().mockResolvedValue([]);
|
||||
const api = createMockXcmetricsApi();
|
||||
api.getBuilds = jest.fn().mockResolvedValue([]);
|
||||
|
||||
const rendered = await renderInTestApp(
|
||||
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, mockXcmetricsApi)}>
|
||||
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, api)}>
|
||||
<OverviewComponent />
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(rendered.getByText('No builds to show')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show an error when API not responding', async () => {
|
||||
const api = createMockXcmetricsApi();
|
||||
const errorMessage = 'MockErrorMessage';
|
||||
|
||||
mockXcmetricsApi.getBuilds = jest
|
||||
.fn()
|
||||
.mockRejectedValue({ message: errorMessage });
|
||||
api.getBuilds = jest.fn().mockRejectedValue({ message: errorMessage });
|
||||
|
||||
const rendered = await renderInTestApp(
|
||||
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, mockXcmetricsApi)}>
|
||||
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, api)}>
|
||||
<OverviewComponent />
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
expect(rendered.getByText(errorMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, { ReactChild } from 'react';
|
||||
import {
|
||||
ContentHeader,
|
||||
SupportButton,
|
||||
@@ -31,33 +31,21 @@ import { Build, BuildStatus, xcmetricsApiRef } from '../../api';
|
||||
import { useAsync } from 'react-use';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { StatusMatrixComponent } from '../StatusMatrixComponent';
|
||||
import { formatDuration, formatStatus } from '../../utils';
|
||||
import { Chip, Grid, Typography } from '@material-ui/core';
|
||||
import { ErrorTrendComponent } from '../ErrorTrendComponent';
|
||||
import { formatTime } from '../../utils';
|
||||
import { Chip, Grid } from '@material-ui/core';
|
||||
import { OverviewTrendsComponent } from '../OverviewTrendsComponent';
|
||||
|
||||
const Status = ({
|
||||
status,
|
||||
warningCount,
|
||||
}: {
|
||||
status: BuildStatus;
|
||||
warningCount: number;
|
||||
}) => {
|
||||
const statusIcons = {
|
||||
succeeded: <StatusOK />,
|
||||
failed: <StatusError />,
|
||||
stopped: <StatusWarning />,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{statusIcons[status]} {formatStatus(status)}
|
||||
{warningCount > 0 && ` with ${warningCount} warning`}
|
||||
{warningCount > 1 && 's'}
|
||||
</>
|
||||
);
|
||||
const STATUS_ICONS: { [key in BuildStatus]: ReactChild } = {
|
||||
succeeded: <StatusOK />,
|
||||
failed: <StatusError />,
|
||||
stopped: <StatusWarning />,
|
||||
};
|
||||
|
||||
const columns: TableColumn<Build>[] = [
|
||||
{
|
||||
field: 'buildStatus',
|
||||
render: data => STATUS_ICONS[data.buildStatus],
|
||||
},
|
||||
{
|
||||
title: 'Project',
|
||||
field: 'projectName',
|
||||
@@ -67,23 +55,15 @@ const columns: TableColumn<Build>[] = [
|
||||
field: 'schema',
|
||||
},
|
||||
{
|
||||
title: 'Duration',
|
||||
field: 'duration',
|
||||
type: 'time',
|
||||
title: 'Started',
|
||||
field: 'startedAt',
|
||||
searchable: false,
|
||||
render: data => formatDuration(data.duration),
|
||||
render: data => formatTime(data.startTimestamp),
|
||||
},
|
||||
{
|
||||
title: 'User',
|
||||
field: 'userid',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
field: 'buildStatus',
|
||||
render: data => (
|
||||
<Status status={data.buildStatus} warningCount={data.warningCount} />
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'isCI',
|
||||
render: data => data.isCi && <Chip label="CI" size="small" />,
|
||||
@@ -121,7 +101,7 @@ export const OverviewComponent = () => {
|
||||
<SupportButton>Dashboard for XCMetrics</SupportButton>
|
||||
</ContentHeader>
|
||||
<Grid container spacing={3} direction="row">
|
||||
<Grid item xs={7}>
|
||||
<Grid item xs={12} md={8} lg={7} xl={9}>
|
||||
<Table
|
||||
options={{ paging: false, search: false }}
|
||||
data={builds}
|
||||
@@ -134,10 +114,9 @@ export const OverviewComponent = () => {
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={5}>
|
||||
<Grid item xs={12} md={4} lg={5} xl={3}>
|
||||
<InfoCard>
|
||||
<Typography variant="overline">Error Rate</Typography>
|
||||
<ErrorTrendComponent days={14} />
|
||||
<OverviewTrendsComponent days={14} />
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 { OverviewTrendsComponent } from './OverviewTrendsComponent';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { xcmetricsApiRef } from '../../api';
|
||||
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
|
||||
import { createMockXcmetricsApi } from '../../test-utils';
|
||||
|
||||
describe('OverviewTrendsComponent', () => {
|
||||
it('should render', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<ApiProvider
|
||||
apis={ApiRegistry.with(xcmetricsApiRef, createMockXcmetricsApi())}
|
||||
>
|
||||
<OverviewTrendsComponent days={14} />
|
||||
</ApiProvider>,
|
||||
);
|
||||
expect(rendered.getByText('Last 14 Days')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render empty state', async () => {
|
||||
const api = createMockXcmetricsApi();
|
||||
api.getBuildCounts = jest.fn().mockResolvedValue([]);
|
||||
|
||||
const rendered = await renderInTestApp(
|
||||
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, api)}>
|
||||
<OverviewTrendsComponent days={14} />
|
||||
</ApiProvider>,
|
||||
);
|
||||
expect(rendered.getByText('No Trends Available')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show an error when API not responding', async () => {
|
||||
const api = createMockXcmetricsApi();
|
||||
const errorMessage = 'MockErrorMessage';
|
||||
|
||||
api.getBuildCounts = jest.fn().mockRejectedValue({ message: errorMessage });
|
||||
|
||||
const rendered = await renderInTestApp(
|
||||
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, api)}>
|
||||
<OverviewTrendsComponent days={14} />
|
||||
</ApiProvider>,
|
||||
);
|
||||
expect(rendered.getByText(errorMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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 { Grid, makeStyles, Typography } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { Progress, TrendLine } from '@backstage/core-components';
|
||||
import { ErrorTrendComponent } from '../ErrorTrendComponent';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { BuildCount, xcmetricsApiRef } from '../../api';
|
||||
import { useAsync } from 'react-use';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { BuildTrendComponent } from '../BuildTrendComponent';
|
||||
import { DataValueGridItem } from '../DataValueComponent';
|
||||
import { formatPercentage } from '../../utils';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
spacingTop: {
|
||||
marginTop: 8,
|
||||
},
|
||||
});
|
||||
|
||||
export const OverviewTrendsComponent = ({ days }: { days: number }) => {
|
||||
const classes = useStyles();
|
||||
const client = useApi(xcmetricsApiRef);
|
||||
const { value: buildCounts, loading, error } = useAsync(
|
||||
async (): Promise<BuildCount[]> => client.getBuildCounts(days),
|
||||
[],
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
} else if (error) {
|
||||
return <Alert severity="error">{error.message}</Alert>;
|
||||
} else if (!buildCounts || buildCounts.length === 0) {
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h6">No Trends Available</Typography>
|
||||
<TrendLine data={[0, 0]} title="No data" color="#CECECE" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const sumCount = buildCounts.reduce(
|
||||
(sum, current) => sum + current.builds,
|
||||
0,
|
||||
);
|
||||
const sumErrors = buildCounts.reduce(
|
||||
(sum, current) => sum + current.errors,
|
||||
0,
|
||||
);
|
||||
const errorRate = sumCount > 0 ? sumErrors / sumCount : 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h6">Last {days} Days</Typography>
|
||||
<ErrorTrendComponent buildCounts={buildCounts} />
|
||||
<BuildTrendComponent buildCounts={buildCounts} />
|
||||
<Grid
|
||||
container
|
||||
spacing={3}
|
||||
direction="row"
|
||||
className={classes.spacingTop}
|
||||
>
|
||||
<DataValueGridItem field="Total Build Count" value={sumCount} />
|
||||
<DataValueGridItem field="Total Error Count" value={sumErrors} />
|
||||
<DataValueGridItem
|
||||
field="Error Rate"
|
||||
value={formatPercentage(errorRate)}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export * from './OverviewTrendsComponent';
|
||||
@@ -19,13 +19,19 @@ import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { StatusCellComponent } from './StatusCellComponent';
|
||||
import { xcmetricsApiRef } from '../../api';
|
||||
import { mockBuildId, mockStatus, mockXcmetricsApi } from '../../test-utils';
|
||||
import {
|
||||
mockBuildId,
|
||||
mockStatus,
|
||||
createMockXcmetricsApi,
|
||||
} from '../../test-utils';
|
||||
import { formatStatus } from '../../utils';
|
||||
|
||||
describe('StatusCellComponent', () => {
|
||||
it('should render', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, mockXcmetricsApi)}>
|
||||
<ApiProvider
|
||||
apis={ApiRegistry.with(xcmetricsApiRef, createMockXcmetricsApi())}
|
||||
>
|
||||
<StatusCellComponent
|
||||
buildStatus={{ id: mockBuildId, buildStatus: mockStatus }}
|
||||
size={10}
|
||||
|
||||
+4
-2
@@ -18,12 +18,14 @@ import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
|
||||
import { StatusMatrixComponent } from './StatusMatrixComponent';
|
||||
import { xcmetricsApiRef } from '../../api';
|
||||
import { mockBuildId, mockXcmetricsApi } from '../../test-utils';
|
||||
import { mockBuildId, createMockXcmetricsApi } from '../../test-utils';
|
||||
|
||||
describe('StatusMatrixComponent', () => {
|
||||
it('should render', async () => {
|
||||
const rendered = await renderInTestApp(
|
||||
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, mockXcmetricsApi)}>
|
||||
<ApiProvider
|
||||
apis={ApiRegistry.with(xcmetricsApiRef, createMockXcmetricsApi())}
|
||||
>
|
||||
<StatusMatrixComponent />
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ export const mockUserId = 'user_id';
|
||||
export const mockBuildId = 'build_id';
|
||||
export const mockStatus = 'succeeded';
|
||||
|
||||
export const mockXcmetricsApi: jest.Mocked<XcmetricsApi> = {
|
||||
export const createMockXcmetricsApi = (): jest.Mocked<XcmetricsApi> => ({
|
||||
getBuildStatuses: jest
|
||||
.fn()
|
||||
.mockResolvedValue([{ id: mockBuildId, status: mockStatus }]),
|
||||
@@ -40,5 +40,8 @@ export const mockXcmetricsApi: jest.Mocked<XcmetricsApi> = {
|
||||
schema: 'AppSchema',
|
||||
},
|
||||
]),
|
||||
getBuildCounts: jest.fn(),
|
||||
};
|
||||
getBuildCounts: jest.fn().mockResolvedValue([
|
||||
{ day: '2021-07-10', builds: 10, errors: 1 },
|
||||
{ day: '2021-07-09', builds: 11, errors: 2 },
|
||||
]),
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Duration } from 'luxon';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { BuildStatus } from '../api';
|
||||
|
||||
export const formatDuration = (seconds: number) =>
|
||||
@@ -21,5 +21,15 @@ export const formatDuration = (seconds: number) =>
|
||||
suppressMilliseconds: true,
|
||||
});
|
||||
|
||||
export const formatTime = (timestamp: string) => {
|
||||
return DateTime.fromISO(timestamp).toLocaleString(
|
||||
DateTime.DATETIME_SHORT_WITH_SECONDS,
|
||||
);
|
||||
};
|
||||
|
||||
export const formatPercentage = (number: number) => {
|
||||
return `${Math.round(number * 100)} %`;
|
||||
};
|
||||
|
||||
export const formatStatus = (status: BuildStatus) =>
|
||||
status[0].toUpperCase() + status.slice(1);
|
||||
|
||||
Reference in New Issue
Block a user