diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index 6534442c95..6d74cd09a2 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -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" diff --git a/plugins/xcmetrics/src/api/XCMetricsClient.ts b/plugins/xcmetrics/src/api/XCMetricsClient.ts index 54107337f9..f89cf00123 100644 --- a/plugins/xcmetrics/src/api/XCMetricsClient.ts +++ b/plugins/xcmetrics/src/api/XCMetricsClient.ts @@ -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 { - 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 { + 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; } } diff --git a/plugins/xcmetrics/src/api/types.ts b/plugins/xcmetrics/src/api/types.ts index 1f7fd4ebc3..51af93cd43 100644 --- a/plugins/xcmetrics/src/api/types.ts +++ b/plugins/xcmetrics/src/api/types.ts @@ -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; + getBuilds(): Promise; } export const xcmetricsApiRef = createApiRef({ diff --git a/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.test.tsx b/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.test.tsx index 4f00ff311a..d92ec1cbc0 100644 --- a/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.test.tsx +++ b/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.test.tsx @@ -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 = { - 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', () => { , ); 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 = { + getBuilds: jest.fn().mockResolvedValue([]), + }; + + const rendered = await renderInTestApp( + + + , + ); + 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 = { + getBuilds: jest.fn().mockRejectedValue({ message: errorMessage }), + }; + + const rendered = await renderInTestApp( + + + , + ); + expect(rendered.getByText(errorMessage)).toBeInTheDocument(); }); }); diff --git a/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.tsx b/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.tsx index 8bdf370627..c582ddc943 100644 --- a/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.tsx +++ b/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.tsx @@ -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: , + failed: , + stopped: , + }; + + return ( + <> + {statusIcons[status]} {status[0].toUpperCase() + status.slice(1)} + {warningCount > 0 && ` with ${warningCount} warnings`} + + ); +}; + +const columns: TableColumn[] = [ + { + 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 && , + width: '10', + sorting: false, + }, +]; export const OverviewComponent = () => { const client = useApi(xcmetricsApiRef); - const { value, loading, error } = useAsync(async (): Promise => { - return client.getBuilds(); - }, []); + const { value: builds, loading, error } = useAsync( + async (): Promise => client.getBuilds(), + [], + ); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + if (!builds || !builds.length) { + return ( + + ); + } return ( <> Dashboard for XCMetrics - - - - {loading && } - {error && {error.message}} - {value &&
{value}
} -
-
- - - -
+ + options={{ paging: false, search: false }} + data={builds} + columns={columns} + title="Latest Builds" + /> ); };