Add table showing the last 10 builds

Signed-off-by: Niklas Granander <ngranander@spotify.com>
This commit is contained in:
Niklas Granander
2021-07-05 10:26:50 +02:00
parent 3aad657c73
commit de315573f8
5 changed files with 177 additions and 27 deletions
+3
View File
@@ -22,10 +22,12 @@
"dependencies": {
"@backstage/core-components": "^0.1.3",
"@backstage/core-plugin-api": "^0.1.3",
"@backstage/errors": "^0.1.1",
"@backstage/theme": "^0.2.8",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"luxon": "^1.27.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-use": "^17.2.4"
@@ -39,6 +41,7 @@
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^13.1.8",
"@types/jest": "^26.0.7",
"@types/luxon": "^1.27.0",
"@types/node": "^14.14.32",
"msw": "^0.29.0",
"cross-fetch": "^3.0.6"
+11 -7
View File
@@ -15,7 +15,8 @@
*/
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { XCMetricsApi } from './types';
import { ResponseError } from '@backstage/errors';
import { BuildItem, BuildsResult, XCMetricsApi } from './types';
interface Options {
discoveryApi: DiscoveryApi;
@@ -28,11 +29,14 @@ export class XCMetricsClient implements XCMetricsApi {
this.discoveryApi = options.discoveryApi;
}
async getBuilds(): Promise<string> {
const backendUrl = `${await this.discoveryApi.getBaseUrl(
'proxy',
)}/xcmetrics/build`;
const response = await fetch(backendUrl);
return JSON.stringify(await response.json(), null, 2);
async getBuilds(): Promise<BuildItem[]> {
const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`;
const response = await fetch(`${baseUrl}/build`);
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
return ((await response.json()) as BuildsResult).items;
}
}
+36 -1
View File
@@ -16,8 +16,43 @@
import { createApiRef } from '@backstage/core-plugin-api';
export type BuildItem = {
userid: string;
warningCount: number;
duration: number;
startTimestamp: string;
isCi: boolean;
startTimestampMicroseconds: number;
category: string;
endTimestampMicroseconds: number;
day: string;
compilationEndTimestamp: string;
tag: string;
projectName: string;
compilationEndTimestampMicroseconds: number;
errorCount: number;
id: string;
buildStatus: 'succeeded' | 'failed' | 'stopped';
compilationDuration: number;
schema: string;
compiledCount: number;
endTimestamp: string;
userid256: string;
machineName: string;
wasSuspended: boolean;
};
export type BuildsResult = {
items: BuildItem[];
metadata: {
per: number;
total: number;
page: number;
};
};
export interface XCMetricsApi {
getBuilds(): Promise<string>;
getBuilds(): Promise<BuildItem[]>;
}
export const xcmetricsApiRef = createApiRef<XCMetricsApi>({
@@ -21,8 +21,19 @@ import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
describe('OverviewComponent', () => {
it('should render', async () => {
const mockUserId = 'mockUser';
const mockApi: jest.Mocked<XCMetricsApi> = {
getBuilds: jest.fn().mockResolvedValue(''),
getBuilds: jest.fn().mockResolvedValue([
{
userid: mockUserId,
warningCount: 1,
duration: 123.45,
isCi: false,
projectName: 'App',
buildStatus: 'succeeded',
schema: 'AppSchema',
},
]),
};
const rendered = await renderInTestApp(
@@ -31,5 +42,34 @@ describe('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 () => {
const mockApi: jest.Mocked<XCMetricsApi> = {
getBuilds: jest.fn().mockResolvedValue([]),
};
const rendered = await renderInTestApp(
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, mockApi)}>
<OverviewComponent />
</ApiProvider>,
);
expect(rendered.getByText('No builds to show')).toBeInTheDocument();
});
it('should show an error when API not responding', async () => {
const errorMessage = 'MockErrorMessage';
const mockApi: jest.Mocked<XCMetricsApi> = {
getBuilds: jest.fn().mockRejectedValue({ message: errorMessage }),
};
const rendered = await renderInTestApp(
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, mockApi)}>
<OverviewComponent />
</ApiProvider>,
);
expect(rendered.getByText(errorMessage)).toBeInTheDocument();
});
});
@@ -14,41 +14,109 @@
* limitations under the License.
*/
import React from 'react';
import { Grid } from '@material-ui/core';
import {
InfoCard,
ContentHeader,
SupportButton,
Progress,
StatusOK,
StatusError,
StatusWarning,
Table,
TableColumn,
EmptyState,
} from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { xcmetricsApiRef } from '../../api';
import { BuildItem, xcmetricsApiRef } from '../../api';
import { useAsync } from 'react-use';
import { Alert } from '@material-ui/lab';
import { Duration } from 'luxon';
import { Chip } from '@material-ui/core';
const formatStatus = (
status: 'succeeded' | 'failed' | 'stopped',
warningCount: number,
) => {
const statusIcons = {
succeeded: <StatusOK />,
failed: <StatusError />,
stopped: <StatusWarning />,
};
return (
<>
{statusIcons[status]} {status[0].toUpperCase() + status.slice(1)}
{warningCount > 0 && ` with ${warningCount} warnings`}
</>
);
};
const columns: TableColumn<BuildItem>[] = [
{
title: 'Project',
field: 'projectName',
},
{
title: 'Schema',
field: 'schema',
},
{
title: 'Duration',
field: 'duration',
type: 'time',
searchable: false,
render: data => Duration.fromObject({ seconds: data.duration }).toISOTime(),
},
{
title: 'User',
field: 'userid',
},
{
title: 'Status',
field: 'buildStatus',
render: data => formatStatus(data.buildStatus, data.warningCount),
},
{
field: 'isCI',
render: data => data.isCi && <Chip label="CI" size="small" />,
width: '10',
sorting: false,
},
];
export const OverviewComponent = () => {
const client = useApi(xcmetricsApiRef);
const { value, loading, error } = useAsync(async (): Promise<string> => {
return client.getBuilds();
}, []);
const { value: builds, loading, error } = useAsync(
async (): Promise<BuildItem[]> => client.getBuilds(),
[],
);
if (loading) {
return <Progress />;
} else if (error) {
return <Alert severity="error">{error.message}</Alert>;
}
if (!builds || !builds.length) {
return (
<EmptyState
missing="data"
title="No builds to show"
description="There are no builds in XCMetrics yet"
/>
);
}
return (
<>
<ContentHeader title="XCMetrics Dashboard">
<SupportButton>Dashboard for XCMetrics</SupportButton>
</ContentHeader>
<Grid container spacing={3} direction="row">
<Grid item xs={7}>
<InfoCard title="Data fetched from XCMetrics through Backstage proxy">
{loading && <Progress />}
{error && <Alert severity="error">{error.message}</Alert>}
{value && <pre>{value}</pre>}
</InfoCard>
</Grid>
<Grid item xs={5}>
<InfoCard title="Placeholder Card" />
</Grid>
</Grid>
<Table<BuildItem>
options={{ paging: false, search: false }}
data={builds}
columns={columns}
title="Latest Builds"
/>
</>
);
};