Add table showing the last 10 builds
Signed-off-by: Niklas Granander <ngranander@spotify.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user