From 5a4d10c04ee71e5ae95e6b63e7c36c9e8dc8e7b2 Mon Sep 17 00:00:00 2001 From: Niklas Granander Date: Tue, 3 Aug 2021 15:30:56 +0200 Subject: [PATCH 1/9] Add details page for builds Signed-off-by: Niklas Granander --- plugins/xcmetrics/src/api/XcmetricsClient.ts | 160 +++++++-------- .../src/api/__mocks__/XcmetricsClient.ts | 114 ++++++++++- plugins/xcmetrics/src/api/types.ts | 68 +++++++ .../AccordionComponent.test.tsx | 51 +++++ .../AccordionComponent/AccordionComponent.tsx | 67 +++++++ .../components/AccordionComponent/index.ts | 16 ++ .../BuildDetailsComponent.test.tsx | 101 ++++++++++ .../BuildDetailsComponent.tsx | 184 ++++++++++++++++++ .../components/BuildDetailsComponent/index.ts | 16 ++ .../BuildListComponent.test.tsx | 22 +++ .../BuildListComponent/BuildListComponent.tsx | 19 +- .../src/components/BuildTableColumns.tsx | 20 +- .../components/BuildsPage/BuildsPage.test.tsx | 43 ++++ .../src/components/BuildsPage/BuildsPage.tsx | 48 +++++ .../src/components/BuildsPage/index.ts | 16 ++ .../StatusIconComponent.test.tsx | 38 ++++ .../StatusIconComponent.tsx | 35 ++++ .../components/StatusIconComponent/index.ts | 16 ++ .../XcmetricsLayout/XcmetricsLayout.test.tsx | 3 + .../XcmetricsLayout/XcmetricsLayout.tsx | 4 +- plugins/xcmetrics/src/utils/format.ts | 8 +- 21 files changed, 942 insertions(+), 107 deletions(-) create mode 100644 plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.test.tsx create mode 100644 plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.tsx create mode 100644 plugins/xcmetrics/src/components/AccordionComponent/index.ts create mode 100644 plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.test.tsx create mode 100644 plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx create mode 100644 plugins/xcmetrics/src/components/BuildDetailsComponent/index.ts create mode 100644 plugins/xcmetrics/src/components/BuildsPage/BuildsPage.test.tsx create mode 100644 plugins/xcmetrics/src/components/BuildsPage/BuildsPage.tsx create mode 100644 plugins/xcmetrics/src/components/BuildsPage/index.ts create mode 100644 plugins/xcmetrics/src/components/StatusIconComponent/StatusIconComponent.test.tsx create mode 100644 plugins/xcmetrics/src/components/StatusIconComponent/StatusIconComponent.tsx create mode 100644 plugins/xcmetrics/src/components/StatusIconComponent/index.ts diff --git a/plugins/xcmetrics/src/api/XcmetricsClient.ts b/plugins/xcmetrics/src/api/XcmetricsClient.ts index 81b3e78a31..7aee5453bd 100644 --- a/plugins/xcmetrics/src/api/XcmetricsClient.ts +++ b/plugins/xcmetrics/src/api/XcmetricsClient.ts @@ -20,9 +20,13 @@ import { DateTime } from 'luxon'; import { Build, BuildCount, + BuildError, BuildFilters, + BuildHost, + BuildMetadata, BuildStatusResult, BuildTime, + BuildWarning, PaginationResult, XcmetricsApi, } from './types'; @@ -39,24 +43,12 @@ export class XcmetricsClient implements XcmetricsApi { } async getBuild(id: string): Promise { - const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`; - const response = await fetch(`${baseUrl}/build/${id}`); - - if (!response.ok) { - throw await ResponseError.fromResponse(response); - } - + const response = await this.get(`/build/${id}`); return ((await response.json()) as Record<'build', Build>).build; } async getBuilds(limit: number = 10): Promise { - const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`; - const response = await fetch(`${baseUrl}/build?per=${limit}`); - - if (!response.ok) { - throw await ResponseError.fromResponse(response); - } - + const response = await this.get(`/build?per=${limit}`); return ((await response.json()) as PaginationResult).items; } @@ -65,80 +57,88 @@ export class XcmetricsClient implements XcmetricsApi { page?: number, perPage?: number, ): Promise> { + const response = await this.post('/build/filter', { + from: DateTime.fromISO(filters.from) + .startOf('day') + .toISO({ suppressMilliseconds: true }), + to: DateTime.fromISO(filters.to) + .endOf('day') + .startOf('second') + .toISO({ suppressMilliseconds: true }), + status: filters.buildStatus, + projectName: filters.project, + page, + per: perPage, + }); + + return (await response.json()) as PaginationResult; + } + + async getBuildCounts(days: number): Promise { + const response = await this.get(`/statistics/build/count?days=${days}`); + return (await response.json()) as BuildCount[]; + } + + async getBuildErrors(buildId: string): Promise { + const response = await this.get(`/build/error/${buildId}`); + return (await response.json()) as BuildError[]; + } + + async getBuildHost(buildId: string): Promise { + const response = await this.get(`/build/host/${buildId}`); + return (await response.json()) as BuildHost; + } + + async getBuildMetadata(buildId: string): Promise { + const response = await this.get(`/build/metadata/${buildId}`); + return ((await response.json()) as Record<'metadata', BuildMetadata>) + .metadata; + } + + async getBuildTimes(days: number): Promise { + const response = await this.get(`/statistics/build/time?days=${days}`); + return (await response.json()) as BuildTime[]; + } + + async getBuildStatuses(limit: number): Promise { + const response = await this.get(`/statistics/build/status?per=${limit}`); + return ((await response.json()) as PaginationResult) + .items; + } + + async getBuildWarnings(buildId: string): Promise { + const response = await this.get(`/build/warning/${buildId}`); + return (await response.json()) as BuildWarning[]; + } + + async getProjects(): Promise { + const response = await this.get('/build/project'); + return (await response.json()) as string[]; + } + + private async get(path: string): Promise { const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`; - const response = await fetch(`${baseUrl}/build/filter`, { + const response = await fetch(`${baseUrl}${path}`); + + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + + return response; + } + + private async post(path: string, body: Object): Promise { + const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`; + const response = await fetch(`${baseUrl}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - from: DateTime.fromISO(filters.from) - .startOf('day') - .toISO({ suppressMilliseconds: true }), - to: DateTime.fromISO(filters.to) - .endOf('day') - .startOf('second') - .toISO({ suppressMilliseconds: true }), - status: filters.buildStatus, - projectName: filters.project, - page, - per: perPage, - }), + body: JSON.stringify(body), }); if (!response.ok) { throw await ResponseError.fromResponse(response); } - return (await response.json()) as PaginationResult; - } - - async getBuildCounts(days: number): Promise { - const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`; - const response = await fetch( - `${baseUrl}/statistics/build/count?days=${days}`, - ); - - if (!response.ok) { - throw await ResponseError.fromResponse(response); - } - - return (await response.json()) as BuildCount[]; - } - - async getBuildTimes(days: number): Promise { - const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`; - const response = await fetch( - `${baseUrl}/statistics/build/time?days=${days}`, - ); - - if (!response.ok) { - throw await ResponseError.fromResponse(response); - } - - return (await response.json()) as BuildTime[]; - } - - async getBuildStatuses(limit: number): Promise { - const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`; - const response = await fetch( - `${baseUrl}/statistics/build/status?per=${limit}`, - ); - - if (!response.ok) { - throw await ResponseError.fromResponse(response); - } - - return ((await response.json()) as PaginationResult) - .items; - } - - async getProjects(): Promise { - const baseUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/xcmetrics`; - const response = await fetch(`${baseUrl}/build/project`); - - if (!response.ok) { - throw await ResponseError.fromResponse(response); - } - - return (await response.json()) as string[]; + return response; } } diff --git a/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts b/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts index 430fbb1006..5258215175 100644 --- a/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts +++ b/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts @@ -14,9 +14,20 @@ * limitations under the License. */ -import { Build, BuildFilters, XcmetricsApi } from '../types'; +import { + Build, + BuildCount, + BuildError, + BuildFilters, + BuildHost, + BuildMetadata, + BuildStatusResult, + BuildTime, + BuildWarning, + XcmetricsApi, +} from '../types'; -export const mockBuild = { +export const mockBuild: Build = { userid: 'userid1', warningCount: 1, duration: 1, @@ -34,22 +45,99 @@ export const mockBuild = { id: 'buildId', buildStatus: 'succeeded', compilationDuration: 1, - schema: 'Schema', + schema: 'SchemaName', compiledCount: 1, endTimestamp: '2021-01-01T00:00:01Z', userid256: 'userId256', machineName: 'Example_Machine', wasSuspended: true, -} as Build; +}; -export const mockBuildCount = { day: '2021-07-10', builds: 10, errors: 1 }; -export const mockBuildTime = { +export const mockBuildCount: BuildCount = { + day: '2021-07-10', + builds: 10, + errors: 1, +}; + +export const mockBuildError: BuildError = { + detail: `\/Users\/\/myproject\/Sources\/MyClass.m:241:97:// / error: instance method 'fetch' not found ; did you mean 'fetchIt'?\r + myclass:[self.myService fetch]\r ^~~~~~~~~~~~~~\r fetch\r + 1 error generated.\r"`, + characterRangeEnd: 13815, + id: '3E6EF185-6AC1-4E95-87E8-E305F41916E9', + endingColumn: 97, + parentIdentifier: 'MyMac_34580469-5792-40F3-BEFB-7C5925996F23_8860', + day: '2020-11-02T00:00:00Z', + type: 'clangError', + title: 'Instance method "fetch" not found ; did you mean "fetchIt"?', + endingLine: 241, + severity: 2, + startingLine: 241, + parentType: 'step', + buildIdentifier: 'buildId', + startingColumn: 97, + characterRangeStart: 0, + documentURL: 'file:///Users//myproject/Sources/MyClass.m', +}; + +export const mockBuildHost: BuildHost = { + id: '9DD5508D-4AD9-4C1C-AB7C-45BC2183EC51', + swapFreeMb: 1615.25, + hostOsFamily: 'Darwin', + isVirtual: false, + uptimeSeconds: 1602055187, + hostModel: 'MacBookPro14,2', + hostOsVersion: '10.15.7', + day: '2020-10-26T00:00:00Z', + cpuCount: 4, + swapTotalMb: 7168, + hostOs: 'Mac OS X', + hostArchitecture: 'x86_64', + memoryTotalMb: 16384, + timezone: 'CET', + cpuModel: 'Intel(R) Core(TM) i7-7567U CPU @ 3.50GHz', + buildIdentifier: 'buildId', + memoryFreeMb: 24.5234375, + cpuSpeedGhz: 3.5, +}; + +export const mockBuildMetadata: BuildMetadata = { + anotherKey: '42', + thirdKey: 'Third value', + aKey: 'value1', +}; + +export const mockBuildTime: BuildTime = { day: '2021-07-10', durationP50: 1.1, durationP95: 2.1, totalDuration: 3.1, }; -export const mockBuildStatus = { id: 'build_id', status: 'succeeded' }; +export const mockBuildStatus: BuildStatusResult = { + id: 'build_id', + buildStatus: 'succeeded', +}; + +export const mockBuildWarning: BuildWarning = { + detail: null, + characterRangeEnd: 9817, + documentURL: 'file:///Users//myproject/Sources/MyViewController.m', + endingColumn: 22, + id: '5F2011AC-F87F-4EDC-BBC6-2BBA3D789EB3', + parentIdentifier: 'MyMac_34580469-5792-40F3-BEFB-7C5925996F23_1845', + day: '2020-11-02T00:00:00Z', + type: 'deprecatedWarning', + title: + "'dimsBackgroundDuringPresentation' is deprecated: first deprecated in iOS 12.0", + endingLine: 235, + severity: 1, + startingLine: 235, + parentType: 'step', + clangFlag: '[-Wdeprecated-declarations]', + startingColumn: 22, + buildIdentifier: 'MyMac_34580469-5792-40F3-BEFB-7C5925996F23_1', + characterRangeStart: 0, +}; export const XcmetricsClient: XcmetricsApi = { getBuild: (id: string) => { @@ -78,15 +166,27 @@ export const XcmetricsClient: XcmetricsApi = { }, }); }, + getBuildErrors: (buildId: string) => { + return Promise.resolve([{ ...mockBuildError, buildIdentifier: buildId }]); + }, getBuildCounts: () => { return Promise.resolve([mockBuildCount, mockBuildCount]); }, + getBuildHost: (buildId: string) => { + return Promise.resolve({ ...mockBuildHost, buildIdentifier: buildId }); + }, + getBuildMetadata: (_buildId: string) => { + return Promise.resolve(mockBuildMetadata); + }, getBuildStatuses: (limit: number) => { return Promise.resolve([mockBuild].slice(0, limit)); }, getBuildTimes: (days: number) => { return Promise.resolve([mockBuildTime, mockBuildTime].slice(0, days)); }, + getBuildWarnings: (buildId: string) => { + return Promise.resolve([{ ...mockBuildWarning, buildIdentifier: buildId }]); + }, getProjects: () => { return Promise.resolve([mockBuild.projectName]); }, diff --git a/plugins/xcmetrics/src/api/types.ts b/plugins/xcmetrics/src/api/types.ts index c018a67056..8f6ae9f0ae 100644 --- a/plugins/xcmetrics/src/api/types.ts +++ b/plugins/xcmetrics/src/api/types.ts @@ -52,6 +52,50 @@ export type BuildCount = { builds: number; }; +export type BuildError = { + detail: string; + characterRangeEnd: number; + id: string; + endingColumn: number; + parentIdentifier: string; + day: string; + type: string; + title: string; + endingLine: number; + severity: number; + startingLine: number; + parentType: string; + buildIdentifier: string; + startingColumn: number; + characterRangeStart: number; + documentURL: string; +}; + +export type BuildHost = { + id: string; + swapFreeMb: number; + hostOsFamily: string; + isVirtual: boolean; + uptimeSeconds: number; + hostModel: string; + hostOsVersion: string; + day: string; + cpuCount: number; + swapTotalMb: number; + hostOs: string; + hostArchitecture: string; + memoryTotalMb: number; + timezone: string; + cpuModel: string; + buildIdentifier: string; + memoryFreeMb: number; + cpuSpeedGhz: number; +}; + +export type BuildMetadata = { + [key: string]: string; +}; + export type BuildTime = { day: string; durationP50: number; @@ -59,6 +103,26 @@ export type BuildTime = { totalDuration: number; }; +export type BuildWarning = { + detail: string | null; + characterRangeEnd: number; + documentURL: string; + endingColumn: number; + id: string; + parentIdentifier: string; + day: string; + type: string; + title: string; + endingLine: number; + severity: number; + startingLine: number; + parentType: string; + clangFlag: string; + startingColumn: number; + buildIdentifier: string; + characterRangeStart: number; +}; + export type PaginationResult = { items: T[]; metadata: { @@ -83,9 +147,13 @@ export interface XcmetricsApi { page?: number, perPage?: number, ): Promise>; + getBuildErrors(buildId: string): Promise; getBuildCounts(days: number): Promise; + getBuildHost(buildId: string): Promise; + getBuildMetadata(buildId: string): Promise; getBuildTimes(days: number): Promise; getBuildStatuses(limit: number): Promise; + getBuildWarnings(buildId: string): Promise; getProjects(): Promise; } diff --git a/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.test.tsx b/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.test.tsx new file mode 100644 index 0000000000..baa8c0e5f2 --- /dev/null +++ b/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.test.tsx @@ -0,0 +1,51 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { AccordionComponent } from './AccordionComponent'; +import userEvent from '@testing-library/user-event'; + +describe('AccordionComponent', () => { + it('should render', async () => { + const rendered = await renderInTestApp( + , + ); + + expect(rendered.getByText('heading')).toBeInTheDocument(); + expect(rendered.getByText('secondaryHeading')).toBeInTheDocument(); + }); + + it('should show content when clicked', async () => { + const rendered = await renderInTestApp( + + Content + , + ); + + expect(rendered.getByText('Content')).not.toBeVisible(); + + userEvent.click(rendered.getByRole('button')); + expect(await rendered.findByText('Content')).toBeVisible(); + }); +}); diff --git a/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.tsx b/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.tsx new file mode 100644 index 0000000000..d8c4e4f81d --- /dev/null +++ b/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.tsx @@ -0,0 +1,67 @@ +/* + * 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 { + Accordion, + AccordionSummary, + Typography, + AccordionDetails, + makeStyles, + createStyles, +} from '@material-ui/core'; +import React, { PropsWithChildren } from 'react'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { BackstageTheme } from '@backstage/theme'; + +const useStyles = makeStyles((theme: BackstageTheme) => + createStyles({ + heading: { + flexBasis: '33.33%', + flexShrink: 0, + }, + secondaryHeading: { + color: theme.palette.text.secondary, + }, + }), +); + +interface AccordionProps { + id: string; + heading: string; + secondaryHeading?: string | number; + disabled?: boolean; +} + +export const AccordionComponent = ( + props: PropsWithChildren, +) => { + const classes = useStyles(); + + return ( + + } + aria-controls={`${props.id}-content`} + id={`${props.id}-header`} + > + {props.heading} + + {props.secondaryHeading} + + + {props.children} + + ); +}; diff --git a/plugins/xcmetrics/src/components/AccordionComponent/index.ts b/plugins/xcmetrics/src/components/AccordionComponent/index.ts new file mode 100644 index 0000000000..1e71dc81ca --- /dev/null +++ b/plugins/xcmetrics/src/components/AccordionComponent/index.ts @@ -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 './AccordionComponent'; diff --git a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.test.tsx b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.test.tsx new file mode 100644 index 0000000000..99c5ec20e5 --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.test.tsx @@ -0,0 +1,101 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { BuildDetailsComponent, withRequest } from './BuildDetailsComponent'; +import { xcmetricsApiRef } from '../../api'; + +jest.mock('../../api/XcmetricsClient'); +const client = require('../../api/XcmetricsClient'); + +jest.mock('../AccordionComponent', () => ({ + AccordionComponent: ({ heading }: { heading: string }) => ( +
accordion-{heading}
+ ), +})); + +describe('BuildDetailsComponent', () => { + it('should render', async () => { + const rendered = await renderInTestApp( + + + , + ); + + expect(rendered.getByText('accordion-Host')).toBeInTheDocument(); + expect(rendered.getByText('accordion-Errors')).toBeInTheDocument(); + expect(rendered.getByText('accordion-Warnings')).toBeInTheDocument(); + expect(rendered.getByText('accordion-Metadata')).toBeInTheDocument(); + + expect(rendered.getByText(client.mockBuild.id)).toBeInTheDocument(); + expect( + rendered.getByText(client.mockBuild.projectName), + ).toBeInTheDocument(); + expect(rendered.getByText(client.mockBuild.schema)).toBeInTheDocument(); + }); +}); + +describe('BuildDetailsComponent with request', () => { + const BuildDetails = withRequest(BuildDetailsComponent); + + it('should fetch the build and render', async () => { + const rendered = await renderInTestApp( + + + , + ); + + expect(rendered.getByText(client.mockBuild.id)).toBeInTheDocument(); + }); + + it('should show an error when API not responding', async () => { + const errorMessage = 'MockErrorMessage'; + client.XcmetricsClient.getBuild = jest + .fn() + .mockRejectedValue({ message: errorMessage }); + + const rendered = await renderInTestApp( + + + , + ); + + expect(rendered.getByText(errorMessage)).toBeInTheDocument(); + }); + + it('should show a message when no build is returned from the API', async () => { + client.XcmetricsClient.getBuild = jest.fn().mockReturnValue(undefined); + + const rendered = await renderInTestApp( + + + , + ); + + expect( + rendered.getByText(`Could not load build ${client.mockBuild.id}`), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx new file mode 100644 index 0000000000..99b4c83210 --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx @@ -0,0 +1,184 @@ +/* + * 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 { createStyles, Divider, Grid, makeStyles } from '@material-ui/core'; +import React from 'react'; +import { Build, xcmetricsApiRef } from '../../api'; +import { + OverflowTooltip, + Progress, + StructuredMetadataTable, +} from '@backstage/core-components'; +import { Alert } from '@material-ui/lab'; +import { useAsync } from 'react-use'; +import { useApi } from '@backstage/core-plugin-api'; +import { formatDuration, formatStatus, formatTime } from '../../utils'; +import { StatusIconComponent as StatusIcon } from '../StatusIconComponent'; +import { BackstageTheme } from '@backstage/theme'; +import { AccordionComponent } from '../AccordionComponent'; + +const useStyles = makeStyles((theme: BackstageTheme) => + createStyles({ + divider: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + }, + }), +); + +interface BuildDetailsProps { + build: Build; +} + +export const BuildDetailsComponent = ({ build }: BuildDetailsProps) => { + const classes = useStyles(); + const client = useApi(xcmetricsApiRef); + const hostResult = useAsync(async () => client.getBuildHost(build.id), []); + const errorsResult = useAsync( + async () => client.getBuildErrors(build.id), + [], + ); + const warningsResult = useAsync( + async () => client.getBuildWarnings(build.id), + [], + ); + const metadataResult = useAsync( + async () => client.getBuildMetadata(build.id), + [], + ); + + const buildDetails = { + id: build.id, + project: build.projectName, + schema: build.schema, + category: build.category, + userId: build.userid, + 'started at': formatTime(build.startTimestamp), + 'ended at': formatTime(build.endTimestamp), + duration: formatDuration(build.duration), + status: ( + <> + + {formatStatus(build.buildStatus)} + + ), + CI: build.isCi, + }; + + return ( + + + + + + + + {hostResult.loading && } + {!hostResult.loading && hostResult.value && ( + + )} + + + +
+ {errorsResult.loading && } + {!errorsResult.loading && + errorsResult.value && + errorsResult.value.map((error, idx) => ( +
+ + {idx !== errorsResult.value.length - 1 && ( + + )} +
+ ))} +
+
+ + +
+ {warningsResult.loading && } + {!warningsResult.loading && + warningsResult.value && + warningsResult.value.map((warning, idx) => ( +
+ + {idx !== warningsResult.value.length - 1 && ( + + )} +
+ ))} +
+
+ + + {metadataResult.loading && } + {!metadataResult.loading && metadataResult.value && ( + + )} + +
+
+ {/* TODO: Sequnce chart */} +
+ ); +}; + +export const withRequest = (Component: typeof BuildDetailsComponent) => ({ + buildId, +}: { + buildId: string; +}) => { + const client = useApi(xcmetricsApiRef); + const { value: build, loading, error } = useAsync( + async () => client.getBuild(buildId), + [], + ); + + if (loading) { + return ; + } + + if (error) { + return {error.message}; + } + + if (!build) { + return Could not load build {buildId}; + } + + return ; +}; diff --git a/plugins/xcmetrics/src/components/BuildDetailsComponent/index.ts b/plugins/xcmetrics/src/components/BuildDetailsComponent/index.ts new file mode 100644 index 0000000000..574b29f32e --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildDetailsComponent/index.ts @@ -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 './BuildDetailsComponent'; diff --git a/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.test.tsx b/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.test.tsx index 3818d51a16..7d7d1ff585 100644 --- a/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.test.tsx +++ b/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.test.tsx @@ -18,6 +18,7 @@ import { renderInTestApp } from '@backstage/test-utils'; import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { BuildListComponent } from './BuildListComponent'; import { xcmetricsApiRef } from '../../api'; +import userEvent from '@testing-library/user-event'; jest.mock('../../api/XcmetricsClient'); const client = require('../../api/XcmetricsClient'); @@ -26,6 +27,10 @@ jest.mock('../BuildListFilterComponent', () => ({ BuildListFilterComponent: () => 'BuildListFilterComponent', })); +jest.mock('../BuildDetailsComponent', () => ({ + BuildDetailsComponent: () => 'BuildDetailsComponent', +})); + describe('BuildListComponent', () => { it('should render', async () => { const rendered = await renderInTestApp( @@ -42,6 +47,23 @@ describe('BuildListComponent', () => { ).toBeInTheDocument(); }); + it('should show build details', async () => { + const rendered = await renderInTestApp( + + + , + ); + + userEvent.click( + (await rendered.findAllByLabelText('Detail panel visiblity toggle'))[0], + ); + expect( + await rendered.findByText('BuildDetailsComponent'), + ).toBeInTheDocument(); + }); + it('should show errors', async () => { const message = 'error'; client.XcmetricsClient.getFilteredBuilds = jest diff --git a/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.tsx b/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.tsx index c9ab028359..6cc44eee54 100644 --- a/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.tsx +++ b/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.tsx @@ -17,12 +17,24 @@ import React, { useRef, useState } from 'react'; import { Table } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; import { BuildFilters, xcmetricsApiRef } from '../../api'; -import { Grid } from '@material-ui/core'; +import { createStyles, Grid, makeStyles } from '@material-ui/core'; import { BuildListFilterComponent as Filters } from '../BuildListFilterComponent'; import { DateTime } from 'luxon'; import { buildPageColumns } from '../BuildTableColumns'; +import { BuildDetailsComponent } from '../BuildDetailsComponent'; +import { BackstageTheme } from '@backstage/theme'; + +const useStyles = makeStyles((theme: BackstageTheme) => + createStyles({ + detailPanel: { + padding: theme.spacing(2), + backgroundColor: theme.palette.background.paper, + }, + }), +); export const BuildListComponent = () => { + const classes = useStyles(); const client = useApi(xcmetricsApiRef); const tableRef = useRef(); @@ -68,6 +80,11 @@ export const BuildListComponent = () => { .catch(reason => reject(reason)); }); }} + detailPanel={rowData => ( +
+ +
+ )} /> ); diff --git a/plugins/xcmetrics/src/components/BuildTableColumns.tsx b/plugins/xcmetrics/src/components/BuildTableColumns.tsx index 95c82ef372..a8bfd0166e 100644 --- a/plugins/xcmetrics/src/components/BuildTableColumns.tsx +++ b/plugins/xcmetrics/src/components/BuildTableColumns.tsx @@ -15,26 +15,16 @@ */ import { Chip } from '@material-ui/core'; -import React, { ReactChild } from 'react'; -import { - StatusOK, - StatusError, - StatusWarning, - TableColumn, -} from '@backstage/core-components'; -import { Build, BuildStatus } from '../api'; +import React from 'react'; +import { TableColumn } from '@backstage/core-components'; +import { Build } from '../api'; import { formatTime, formatDuration } from '../utils'; - -const STATUS_ICONS: { [key in BuildStatus]: ReactChild } = { - succeeded: , - failed: , - stopped: , -}; +import { StatusIconComponent as StatusIcon } from './StatusIconComponent'; const baseColumns: TableColumn[] = [ { field: 'buildStatus', - render: data => STATUS_ICONS[data.buildStatus], + render: data => , }, { title: 'Project', diff --git a/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.test.tsx b/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.test.tsx new file mode 100644 index 0000000000..601f3ea6a2 --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.test.tsx @@ -0,0 +1,43 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { BuildsPage } from './BuildsPage'; + +jest.mock('../BuildDetailsComponent', () => ({ + withRequest: (component: any) => component, + BuildDetailsComponent: () => 'BuildDetailsComponent', +})); + +jest.mock('../BuildListComponent', () => ({ + BuildListComponent: () => 'BuildListComponent', +})); + +describe('BuildPageComponent', () => { + it('should render BuildDetailsWithDataComponent if build id is provided in path', async () => { + const rendered = await renderInTestApp(, { + routeEntries: [`/buildId`], + }); + + expect(rendered.getByText('BuildDetailsComponent')).toBeInTheDocument(); + }); + + it('should render BuildListComponent if no build id is provided in path', async () => { + const rendered = await renderInTestApp(); + + expect(rendered.getByText('BuildListComponent')).toBeInTheDocument(); + }); +}); diff --git a/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.tsx b/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.tsx new file mode 100644 index 0000000000..6d7c8cf20b --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.tsx @@ -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 { useRouteRefParams } from '@backstage/core-plugin-api'; +import { buildsRouteRef } from '../../routes'; +import { BuildListComponent } from '../BuildListComponent'; +import { BuildDetailsComponent, withRequest } from '../BuildDetailsComponent'; +import { CopyTextButton, InfoCard } from '@backstage/core-components'; + +export const BuildsPage = () => { + const { '*': buildId } = useRouteRefParams(buildsRouteRef) ?? { '*': '' }; + + if (buildId) { + const BuildDetails = withRequest(BuildDetailsComponent); + + return ( + + {buildId}{' '} + + + } + > + + + ); + } + + return ; +}; diff --git a/plugins/xcmetrics/src/components/BuildsPage/index.ts b/plugins/xcmetrics/src/components/BuildsPage/index.ts new file mode 100644 index 0000000000..d04503499c --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildsPage/index.ts @@ -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 './BuildsPage'; diff --git a/plugins/xcmetrics/src/components/StatusIconComponent/StatusIconComponent.test.tsx b/plugins/xcmetrics/src/components/StatusIconComponent/StatusIconComponent.test.tsx new file mode 100644 index 0000000000..8833d828f0 --- /dev/null +++ b/plugins/xcmetrics/src/components/StatusIconComponent/StatusIconComponent.test.tsx @@ -0,0 +1,38 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { StatusIconComponent } from './StatusIconComponent'; + +describe('StatusIconComponent', () => { + it('should render', async () => { + let rendered = await renderInTestApp( + , + ); + expect(rendered.getByLabelText('Status ok')).toBeInTheDocument(); + + rendered = await renderInTestApp( + , + ); + expect(rendered.getByLabelText('Status error')).toBeInTheDocument(); + + rendered = await renderInTestApp( + , + ); + expect(rendered.getByLabelText('Status warning')).toBeInTheDocument(); + }); +}); diff --git a/plugins/xcmetrics/src/components/StatusIconComponent/StatusIconComponent.tsx b/plugins/xcmetrics/src/components/StatusIconComponent/StatusIconComponent.tsx new file mode 100644 index 0000000000..f680003ff4 --- /dev/null +++ b/plugins/xcmetrics/src/components/StatusIconComponent/StatusIconComponent.tsx @@ -0,0 +1,35 @@ +/* + * 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 { + StatusError, + StatusOK, + StatusWarning, +} from '@backstage/core-components'; +import { BuildStatus } from '../../api'; + +const STATUS_ICONS: { [key in BuildStatus]: JSX.Element } = { + succeeded: , + failed: , + stopped: , +}; + +interface StatusIconProps { + buildStatus: BuildStatus; +} + +export const StatusIconComponent = ({ buildStatus }: StatusIconProps) => + STATUS_ICONS[buildStatus]; diff --git a/plugins/xcmetrics/src/components/StatusIconComponent/index.ts b/plugins/xcmetrics/src/components/StatusIconComponent/index.ts new file mode 100644 index 0000000000..e680e6177c --- /dev/null +++ b/plugins/xcmetrics/src/components/StatusIconComponent/index.ts @@ -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 './StatusIconComponent'; diff --git a/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.test.tsx b/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.test.tsx index 2ce634dc11..b0d81757a9 100644 --- a/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.test.tsx +++ b/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.test.tsx @@ -18,6 +18,7 @@ import { renderInTestApp } from '@backstage/test-utils'; import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { XcmetricsLayout } from './XcmetricsLayout'; import { xcmetricsApiRef } from '../../api'; +import userEvent from '@testing-library/user-event'; jest.mock('../../api/XcmetricsClient'); const client = require('../../api/XcmetricsClient'); @@ -44,5 +45,7 @@ describe('XcmetricsLayout', () => { expect(rendered.getByText('Builds')).toBeInTheDocument(); expect(rendered.getByText('OverviewComponent')).toBeInTheDocument(); + userEvent.click(rendered.getByText('Builds')); + expect(await rendered.findByText('BuildListComponent')).toBeInTheDocument(); }); }); diff --git a/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.tsx b/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.tsx index 48faaaa3a6..33e9d64c6b 100644 --- a/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.tsx +++ b/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.tsx @@ -24,7 +24,7 @@ import { import { OverviewComponent } from '../OverviewComponent'; import { buildsRouteRef, rootRouteRef } from '../../routes'; import { RouteRef, SubRouteRef } from '@backstage/core-plugin-api'; -import { BuildListComponent } from '../BuildListComponent'; +import { BuildsPage } from '../BuildsPage'; export interface TabConfig { routeRef: RouteRef | SubRouteRef; @@ -41,7 +41,7 @@ const TABS: TabConfig[] = [ { routeRef: buildsRouteRef, title: 'Builds', - component: , + component: , }, ]; diff --git a/plugins/xcmetrics/src/utils/format.ts b/plugins/xcmetrics/src/utils/format.ts index 8c6dadb768..67a0ed4c43 100644 --- a/plugins/xcmetrics/src/utils/format.ts +++ b/plugins/xcmetrics/src/utils/format.ts @@ -18,8 +18,12 @@ import { BuildStatus } from '../api'; export const formatDuration = (seconds: number) => { const duration = Duration.fromObject({ - seconds: Math.round(seconds), - }).shiftTo('hours', 'minutes', 'seconds'); + seconds: seconds, + }).shiftTo('hours', 'minutes', 'seconds', 'milliseconds'); + + if (duration.hours + duration.minutes + duration.seconds === 0) { + return `${Math.round(duration.milliseconds)} ms`; + } const h = duration.hours ? `${duration.hours} h` : ''; const m = duration.minutes ? `${duration.minutes} m` : ''; From 2daf2635a4392b8b7f7f455e3131bf2f03194fc9 Mon Sep 17 00:00:00 2001 From: Niklas Granander Date: Tue, 3 Aug 2021 15:31:50 +0200 Subject: [PATCH 2/9] Make sure datepicker label is above input Signed-off-by: Niklas Granander --- .../src/components/DatePickerComponent/DatePickerComponent.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/xcmetrics/src/components/DatePickerComponent/DatePickerComponent.tsx b/plugins/xcmetrics/src/components/DatePickerComponent/DatePickerComponent.tsx index 6877340a5b..c6cc2eded5 100644 --- a/plugins/xcmetrics/src/components/DatePickerComponent/DatePickerComponent.tsx +++ b/plugins/xcmetrics/src/components/DatePickerComponent/DatePickerComponent.tsx @@ -62,6 +62,7 @@ export const DatePickerComponent = ({ }: InputProps & DatePickerProps) => ( <> {label} +
Date: Wed, 4 Aug 2021 18:11:06 +0200 Subject: [PATCH 3/9] Add timeline and better handling of warning/error message Signed-off-by: Niklas Granander --- plugins/xcmetrics/package.json | 3 +- plugins/xcmetrics/src/api/XcmetricsClient.ts | 5 +- .../src/api/__mocks__/XcmetricsClient.ts | 52 ++++- plugins/xcmetrics/src/api/types.ts | 36 +++- .../AccordionComponent/AccordionComponent.tsx | 6 +- .../BuildDetailsComponent.test.tsx | 7 +- .../BuildDetailsComponent.tsx | 193 ++++++++++-------- .../BuildListComponent.test.tsx | 1 + .../BuildListComponent/BuildListComponent.tsx | 15 +- .../BuildTimelineComponent.test.tsx | 43 ++++ .../BuildTimelineComponent.tsx | 119 +++++++++++ .../BuildTimelineComponent/index.ts | 16 ++ .../src/components/BuildsPage/BuildsPage.tsx | 17 +- .../PreformattedTextComponent.tsx | 128 ++++++++++++ .../PreformattedTextComponent/index.ts | 16 ++ .../StatusCellComponent.tsx | 19 +- plugins/xcmetrics/src/utils/format.ts | 6 + 17 files changed, 551 insertions(+), 131 deletions(-) create mode 100644 plugins/xcmetrics/src/components/BuildTimelineComponent/BuildTimelineComponent.test.tsx create mode 100644 plugins/xcmetrics/src/components/BuildTimelineComponent/BuildTimelineComponent.tsx create mode 100644 plugins/xcmetrics/src/components/BuildTimelineComponent/index.ts create mode 100644 plugins/xcmetrics/src/components/PreformattedTextComponent/PreformattedTextComponent.tsx create mode 100644 plugins/xcmetrics/src/components/PreformattedTextComponent/index.ts diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index 8ebcf9a85e..50eaaf5b94 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -30,7 +30,8 @@ "luxon": "^2.0.2", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-use": "^17.2.4" + "react-use": "^17.2.4", + "recharts": "^1.8.5" }, "devDependencies": { "@backstage/cli": "^0.7.7", diff --git a/plugins/xcmetrics/src/api/XcmetricsClient.ts b/plugins/xcmetrics/src/api/XcmetricsClient.ts index 7aee5453bd..37baa949d4 100644 --- a/plugins/xcmetrics/src/api/XcmetricsClient.ts +++ b/plugins/xcmetrics/src/api/XcmetricsClient.ts @@ -24,6 +24,7 @@ import { BuildFilters, BuildHost, BuildMetadata, + BuildResponse, BuildStatusResult, BuildTime, BuildWarning, @@ -42,9 +43,9 @@ export class XcmetricsClient implements XcmetricsApi { this.discoveryApi = options.discoveryApi; } - async getBuild(id: string): Promise { + async getBuild(id: string): Promise { const response = await this.get(`/build/${id}`); - return ((await response.json()) as Record<'build', Build>).build; + return (await response.json()) as BuildResponse; } async getBuilds(limit: number = 10): Promise { diff --git a/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts b/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts index 5258215175..765be721c6 100644 --- a/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts +++ b/plugins/xcmetrics/src/api/__mocks__/XcmetricsClient.ts @@ -24,9 +24,13 @@ import { BuildStatusResult, BuildTime, BuildWarning, + Target, XcmetricsApi, + Xcode, } from '../types'; +const MOCK_BUILD_ID = 'buildId'; + export const mockBuild: Build = { userid: 'userid1', warningCount: 1, @@ -42,7 +46,7 @@ export const mockBuild: Build = { projectName: 'ProjectName', compilationEndTimestampMicroseconds: 1, errorCount: 1, - id: 'buildId', + id: MOCK_BUILD_ID, buildStatus: 'succeeded', compilationDuration: 1, schema: 'SchemaName', @@ -74,7 +78,7 @@ export const mockBuildError: BuildError = { severity: 2, startingLine: 241, parentType: 'step', - buildIdentifier: 'buildId', + buildIdentifier: MOCK_BUILD_ID, startingColumn: 97, characterRangeStart: 0, documentURL: 'file:///Users//myproject/Sources/MyClass.m', @@ -96,7 +100,7 @@ export const mockBuildHost: BuildHost = { memoryTotalMb: 16384, timezone: 'CET', cpuModel: 'Intel(R) Core(TM) i7-7567U CPU @ 3.50GHz', - buildIdentifier: 'buildId', + buildIdentifier: MOCK_BUILD_ID, memoryFreeMb: 24.5234375, cpuSpeedGhz: 3.5, }; @@ -114,7 +118,7 @@ export const mockBuildTime: BuildTime = { totalDuration: 3.1, }; export const mockBuildStatus: BuildStatusResult = { - id: 'build_id', + id: MOCK_BUILD_ID, buildStatus: 'succeeded', }; @@ -135,13 +139,47 @@ export const mockBuildWarning: BuildWarning = { parentType: 'step', clangFlag: '[-Wdeprecated-declarations]', startingColumn: 22, - buildIdentifier: 'MyMac_34580469-5792-40F3-BEFB-7C5925996F23_1', + buildIdentifier: MOCK_BUILD_ID, characterRangeStart: 0, }; +export const mockTarget: Target = { + id: 'MyMac_34580469-5792-40F3-BEFB-7C5925996F23_1992', + category: 'noop', + startTimestamp: '2020-11-02T10:59:09Z', + compilationEndTimestampMicroseconds: 1604314749.2909288, + endTimestampMicroseconds: 1604314982.298002, + endTimestamp: '2020-11-02T11:03:02Z', + fetchedFromCache: false, + errorCount: 0, + day: '2020-11-02T00:00:00Z', + warningCount: 0, + compilationEndTimestamp: '2020-11-02T10:59:09Z', + compilationDuration: 0, + compiledCount: 0, + duration: 0.000233007, + buildIdentifier: MOCK_BUILD_ID, + name: 'Model', + startTimestampMicroseconds: 1604314749.2909288, +}; + +export const mockXcode: Xcode = { + buildNumber: '12A7209', + id: '6354C87F-0ADC-4354-929C-02EBE545E099', + buildIdentifier: MOCK_BUILD_ID, + day: '2020-11-02T00:00:00Z', + version: '1200', +}; + +export const mockBuildResponse = { + build: mockBuild, + targets: [mockTarget], + xcode: mockXcode, +}; + export const XcmetricsClient: XcmetricsApi = { - getBuild: (id: string) => { - return Promise.resolve({ ...mockBuild, id }); + getBuild: (_id: string) => { + return Promise.resolve(mockBuildResponse); }, getBuilds: () => { return Promise.resolve([ diff --git a/plugins/xcmetrics/src/api/types.ts b/plugins/xcmetrics/src/api/types.ts index 8f6ae9f0ae..44f27872ca 100644 --- a/plugins/xcmetrics/src/api/types.ts +++ b/plugins/xcmetrics/src/api/types.ts @@ -132,6 +132,40 @@ export type PaginationResult = { }; }; +export type Target = { + id: string; + category: string; + startTimestamp: string; + compilationEndTimestampMicroseconds: number; + endTimestampMicroseconds: number; + endTimestamp: string; + fetchedFromCache: boolean; + errorCount: number; + day: string; + warningCount: number; + compilationEndTimestamp: string; + compilationDuration: number; + compiledCount: number; + duration: number; + buildIdentifier: string; + name: string; + startTimestampMicroseconds: number; +}; + +export type Xcode = { + buildNumber: string; + id: string; + buildIdentifier: string; + day: string; + version: string; +}; + +export type BuildResponse = { + build: Build; + targets: Target[]; + xcode: Xcode; +}; + export type BuildFilters = { from: string; // ISO Date (e.g. "2021-01-01") to: string; // ISO Date (e.g. "2021-01-02") @@ -140,7 +174,7 @@ export type BuildFilters = { }; export interface XcmetricsApi { - getBuild(id: string): Promise; + getBuild(id: string): Promise; getBuilds(limit?: number): Promise; getFilteredBuilds( filters: BuildFilters, diff --git a/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.tsx b/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.tsx index d8c4e4f81d..9afe736a2a 100644 --- a/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.tsx +++ b/plugins/xcmetrics/src/components/AccordionComponent/AccordionComponent.tsx @@ -42,6 +42,7 @@ interface AccordionProps { heading: string; secondaryHeading?: string | number; disabled?: boolean; + unmountOnExit?: boolean; } export const AccordionComponent = ( @@ -50,7 +51,10 @@ export const AccordionComponent = ( const classes = useStyles(); return ( - + } aria-controls={`${props.id}-content`} diff --git a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.test.tsx b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.test.tsx index 99c5ec20e5..15b8720a1d 100644 --- a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.test.tsx +++ b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.test.tsx @@ -28,13 +28,17 @@ jest.mock('../AccordionComponent', () => ({ ), })); +jest.mock('../BuildTimelineComponent', () => ({ + BuildTimelineComponent: () => 'BuildTimelineComponent', +})); + describe('BuildDetailsComponent', () => { it('should render', async () => { const rendered = await renderInTestApp( - + , ); @@ -42,6 +46,7 @@ describe('BuildDetailsComponent', () => { expect(rendered.getByText('accordion-Errors')).toBeInTheDocument(); expect(rendered.getByText('accordion-Warnings')).toBeInTheDocument(); expect(rendered.getByText('accordion-Metadata')).toBeInTheDocument(); + expect(rendered.getByText('accordion-Timeline')).toBeInTheDocument(); expect(rendered.getByText(client.mockBuild.id)).toBeInTheDocument(); expect( diff --git a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx index 99b4c83210..6696a2083d 100644 --- a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx +++ b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx @@ -15,12 +15,8 @@ */ import { createStyles, Divider, Grid, makeStyles } from '@material-ui/core'; import React from 'react'; -import { Build, xcmetricsApiRef } from '../../api'; -import { - OverflowTooltip, - Progress, - StructuredMetadataTable, -} from '@backstage/core-components'; +import { BuildResponse, xcmetricsApiRef } from '../../api'; +import { Progress, StructuredMetadataTable } from '@backstage/core-components'; import { Alert } from '@material-ui/lab'; import { useAsync } from 'react-use'; import { useApi } from '@backstage/core-plugin-api'; @@ -28,6 +24,8 @@ import { formatDuration, formatStatus, formatTime } from '../../utils'; import { StatusIconComponent as StatusIcon } from '../StatusIconComponent'; import { BackstageTheme } from '@backstage/theme'; import { AccordionComponent } from '../AccordionComponent'; +import { BuildTimelineComponent } from '../BuildTimelineComponent'; +import { PreformattedTextComponent } from '../PreformattedTextComponent'; const useStyles = makeStyles((theme: BackstageTheme) => createStyles({ @@ -39,10 +37,14 @@ const useStyles = makeStyles((theme: BackstageTheme) => ); interface BuildDetailsProps { - build: Build; + buildData: BuildResponse; + showId?: boolean; } -export const BuildDetailsComponent = ({ build }: BuildDetailsProps) => { +export const BuildDetailsComponent = ({ + buildData: { build, targets, xcode }, + showId, +}: BuildDetailsProps) => { const classes = useStyles(); const client = useApi(xcmetricsApiRef); const hostResult = useAsync(async () => client.getBuildHost(build.id), []); @@ -60,7 +62,6 @@ export const BuildDetailsComponent = ({ build }: BuildDetailsProps) => { ); const buildDetails = { - id: build.id, project: build.projectName, schema: build.schema, category: build.category, @@ -74,96 +75,110 @@ export const BuildDetailsComponent = ({ build }: BuildDetailsProps) => { {formatStatus(build.buildStatus)} ), + xcode: `${xcode.version} (${xcode.buildNumber})`, CI: build.isCi, }; return ( - - - - - - - - {hostResult.loading && } - {!hostResult.loading && hostResult.value && ( - - )} - - - -
- {errorsResult.loading && } - {!errorsResult.loading && - errorsResult.value && - errorsResult.value.map((error, idx) => ( -
- - {idx !== errorsResult.value.length - 1 && ( - - )} -
- ))} -
-
- - -
- {warningsResult.loading && } - {!warningsResult.loading && - warningsResult.value && - warningsResult.value.map((warning, idx) => ( -
- - {idx !== warningsResult.value.length - 1 && ( - - )} -
- ))} -
-
- - - {metadataResult.loading && } - {!metadataResult.loading && metadataResult.value && ( - - )} - -
+ + + + + + + {hostResult.loading && } + {!hostResult.loading && hostResult.value && ( + + )} + + + +
+ {errorsResult.loading && } + {!errorsResult.loading && + errorsResult.value?.map((error, idx) => ( +
+ + {idx !== errorsResult.value.length - 1 && ( + + )} +
+ ))} +
+
+ + +
+ {warningsResult.loading && } + {!warningsResult.loading && + warningsResult.value?.map((warning, idx) => ( +
+ + {idx !== warningsResult.value.length - 1 && ( + + )} +
+ ))} +
+
+ + + {metadataResult.loading && } + {!metadataResult.loading && metadataResult.value && ( + + )} + + + + +
- {/* TODO: Sequnce chart */}
); }; +type WithRequestProps = Omit & { + buildId: string; +}; + export const withRequest = (Component: typeof BuildDetailsComponent) => ({ buildId, -}: { - buildId: string; -}) => { + ...props +}: WithRequestProps) => { const client = useApi(xcmetricsApiRef); - const { value: build, loading, error } = useAsync( + const { value: buildResponse, loading, error } = useAsync( async () => client.getBuild(buildId), [], ); @@ -176,9 +191,9 @@ export const withRequest = (Component: typeof BuildDetailsComponent) => ({ return {error.message}; } - if (!build) { + if (!buildResponse) { return Could not load build {buildId}; } - return ; + return ; }; diff --git a/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.test.tsx b/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.test.tsx index 7d7d1ff585..761c08a711 100644 --- a/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.test.tsx +++ b/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.test.tsx @@ -28,6 +28,7 @@ jest.mock('../BuildListFilterComponent', () => ({ })); jest.mock('../BuildDetailsComponent', () => ({ + withRequest: (component: any) => component, BuildDetailsComponent: () => 'BuildDetailsComponent', })); diff --git a/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.tsx b/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.tsx index 6cc44eee54..42f6bf4414 100644 --- a/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.tsx +++ b/plugins/xcmetrics/src/components/BuildListComponent/BuildListComponent.tsx @@ -21,7 +21,7 @@ import { createStyles, Grid, makeStyles } from '@material-ui/core'; import { BuildListFilterComponent as Filters } from '../BuildListFilterComponent'; import { DateTime } from 'luxon'; import { buildPageColumns } from '../BuildTableColumns'; -import { BuildDetailsComponent } from '../BuildDetailsComponent'; +import { BuildDetailsComponent, withRequest } from '../BuildDetailsComponent'; import { BackstageTheme } from '@backstage/theme'; const useStyles = makeStyles((theme: BackstageTheme) => @@ -80,11 +80,14 @@ export const BuildListComponent = () => { .catch(reason => reject(reason)); }); }} - detailPanel={rowData => ( -
- -
- )} + detailPanel={rowData => { + const BuildDetails = withRequest(BuildDetailsComponent); + return ( +
+ +
+ ); + }} />
); diff --git a/plugins/xcmetrics/src/components/BuildTimelineComponent/BuildTimelineComponent.test.tsx b/plugins/xcmetrics/src/components/BuildTimelineComponent/BuildTimelineComponent.test.tsx new file mode 100644 index 0000000000..3adb1ede58 --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildTimelineComponent/BuildTimelineComponent.test.tsx @@ -0,0 +1,43 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { BuildTimelineComponent } from './BuildTimelineComponent'; + +jest.mock('../../api/XcmetricsClient'); +const client = require('../../api/XcmetricsClient'); + +describe('BuildTimelineComponent', () => { + it('should render', async () => { + const rendered = await renderInTestApp( + , + ); + expect( + await rendered.findByText(client.mockTarget.name), + ).toBeInTheDocument(); + }); + + it('should render a message if no targets are provided', async () => { + const rendered = await renderInTestApp( + , + ); + expect(rendered.getByText('No Targets')).toBeInTheDocument(); + }); +}); diff --git a/plugins/xcmetrics/src/components/BuildTimelineComponent/BuildTimelineComponent.tsx b/plugins/xcmetrics/src/components/BuildTimelineComponent/BuildTimelineComponent.tsx new file mode 100644 index 0000000000..0d0f249a95 --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildTimelineComponent/BuildTimelineComponent.tsx @@ -0,0 +1,119 @@ +/* + * 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 { createStyles, makeStyles, useTheme } from '@material-ui/core'; +import React from 'react'; +import { + Bar, + BarChart, + CartesianGrid, + Legend, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import { BackstageTheme } from '@backstage/theme'; +import { Target } from '../../api'; +import { formatSecondsInterval, formatPercentage } from '../../utils'; + +const EMPTY_HEIGHT = 100; + +const useStyles = makeStyles((theme: BackstageTheme) => + createStyles({ + toolTip: { + backgroundColor: theme.palette.background.paper, + opacity: 0.8, + padding: 8, + }, + }), +); + +const TargetToolTip = ({ active, payload, label }: any) => { + const classes = useStyles(); + + if (active && payload && payload.length === 2) { + const buildTime = payload[0].value[1] - payload[0].value[0]; + const compileTime = payload[1].value[1] - payload[1].value[0]; + return ( +
+ {`${label}: ${formatSecondsInterval(payload[0].value)}`} +
+ {buildTime > 0 && + `Compile time: ${formatPercentage(compileTime / buildTime)}`} +
+ ); + } + + return null; +}; + +const getTimelineData = (targets: Target[]) => { + const min = targets[0].startTimestampMicroseconds; + + return targets + .filter(target => target.fetchedFromCache === false) + .map(target => ({ + name: target.name, + buildTime: [ + target.startTimestampMicroseconds - min, + target.endTimestampMicroseconds - min, + ], + compileTime: [ + target.startTimestampMicroseconds - min, + target.compilationEndTimestampMicroseconds - min, + ], + })); +}; + +export interface BuildTimelineProps { + targets: Target[]; + height?: number; + width?: number; +} + +export const BuildTimelineComponent = ({ + targets, + height, + width, +}: BuildTimelineProps) => { + const theme = useTheme(); + if (!targets.length) return

No Targets

; + + const data = getTimelineData(targets); + + return ( + + + + + + } /> + + + + + + ); +}; diff --git a/plugins/xcmetrics/src/components/BuildTimelineComponent/index.ts b/plugins/xcmetrics/src/components/BuildTimelineComponent/index.ts new file mode 100644 index 0000000000..2a6a612acf --- /dev/null +++ b/plugins/xcmetrics/src/components/BuildTimelineComponent/index.ts @@ -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 './BuildTimelineComponent'; diff --git a/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.tsx b/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.tsx index 6d7c8cf20b..ca3f023ff5 100644 --- a/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.tsx +++ b/plugins/xcmetrics/src/components/BuildsPage/BuildsPage.tsx @@ -18,7 +18,7 @@ import { useRouteRefParams } from '@backstage/core-plugin-api'; import { buildsRouteRef } from '../../routes'; import { BuildListComponent } from '../BuildListComponent'; import { BuildDetailsComponent, withRequest } from '../BuildDetailsComponent'; -import { CopyTextButton, InfoCard } from '@backstage/core-components'; +import { InfoCard } from '@backstage/core-components'; export const BuildsPage = () => { const { '*': buildId } = useRouteRefParams(buildsRouteRef) ?? { '*': '' }; @@ -27,19 +27,8 @@ export const BuildsPage = () => { const BuildDetails = withRequest(BuildDetailsComponent); return ( - - {buildId}{' '} - - - } - > - + + ); } diff --git a/plugins/xcmetrics/src/components/PreformattedTextComponent/PreformattedTextComponent.tsx b/plugins/xcmetrics/src/components/PreformattedTextComponent/PreformattedTextComponent.tsx new file mode 100644 index 0000000000..a5ad997ee8 --- /dev/null +++ b/plugins/xcmetrics/src/components/PreformattedTextComponent/PreformattedTextComponent.tsx @@ -0,0 +1,128 @@ +/* + * 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 { + Button, + createStyles, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + makeStyles, +} from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; +import React, { useState } from 'react'; +import { BackstageTheme } from '@backstage/theme'; +import { cn } from '../../utils'; + +const useStyles = makeStyles((theme: BackstageTheme) => + createStyles({ + pre: { + whiteSpace: 'pre-line', + wordBreak: 'break-all', + }, + expandable: { + cursor: 'pointer', + }, + fullPre: { + whiteSpace: 'pre-wrap', + }, + closeButton: { + position: 'absolute', + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, + }), +); + +interface PreformattedTextProps { + text: string; + maxChars: number; +} + +interface ExpandableProps extends PreformattedTextProps { + expandable: boolean; + title: string; +} + +interface NonExpandableProps extends PreformattedTextProps { + expandable?: never; + title?: string; +} + +export const PreformattedTextComponent = ({ + text, + maxChars, + expandable, + title, +}: NonExpandableProps | ExpandableProps) => { + const [open, setOpen] = useState(false); + const classes = useStyles(); + + const handleKeyUp = (event: React.KeyboardEvent) => { + if (expandable && event.key === 'Enter') { + setOpen(true); + } + }; + + return ( + <> +
expandable && setOpen(true)} + onKeyUp={handleKeyUp} + tabIndex={expandable ? 0 : undefined} + > +
+          {text.slice(0, maxChars - 3).trim()}
+          {text.length > maxChars && '...'}
+        
+
+ + {expandable && ( + setOpen(false)} + aria-labelledby="dialog-title" + aria-describedby="dialog-description" + maxWidth="xl" + fullWidth + > + + {title} + setOpen(false)} + > + + + + +
{text}
+
+ + + +
+ )} + + ); +}; diff --git a/plugins/xcmetrics/src/components/PreformattedTextComponent/index.ts b/plugins/xcmetrics/src/components/PreformattedTextComponent/index.ts new file mode 100644 index 0000000000..e923eeb224 --- /dev/null +++ b/plugins/xcmetrics/src/components/PreformattedTextComponent/index.ts @@ -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 './PreformattedTextComponent'; diff --git a/plugins/xcmetrics/src/components/StatusCellComponent/StatusCellComponent.tsx b/plugins/xcmetrics/src/components/StatusCellComponent/StatusCellComponent.tsx index 1ecc324683..201cb4febf 100644 --- a/plugins/xcmetrics/src/components/StatusCellComponent/StatusCellComponent.tsx +++ b/plugins/xcmetrics/src/components/StatusCellComponent/StatusCellComponent.tsx @@ -28,15 +28,16 @@ interface TooltipContentProps { const TooltipContent = ({ buildId }: TooltipContentProps) => { const client = useApi(xcmetricsApiRef); - const { - value: build, - loading, - error, - } = useAsync(async () => client.getBuild(buildId), []); + const { value, loading, error } = useAsync( + async () => client.getBuild(buildId), + [], + ); if (error) { return
{error.message}
; - } else if (loading || !build) { + } + + if (loading || !value?.build) { return ; } @@ -45,15 +46,15 @@ const TooltipContent = ({ buildId }: TooltipContentProps) => { Started - {new Date(build.startTimestamp).toLocaleString()} + {new Date(value.build.startTimestamp).toLocaleString()} Duration - {formatDuration(build.duration)} + {formatDuration(value.build.duration)} Status - {formatStatus(build.buildStatus)} + {formatStatus(value.build.buildStatus)} diff --git a/plugins/xcmetrics/src/utils/format.ts b/plugins/xcmetrics/src/utils/format.ts index 67a0ed4c43..e21e95a7b2 100644 --- a/plugins/xcmetrics/src/utils/format.ts +++ b/plugins/xcmetrics/src/utils/format.ts @@ -32,6 +32,12 @@ export const formatDuration = (seconds: number) => { return `${h} ${m} ${s}`; }; +export const formatSecondsInterval = ([start, end]: [number, number]) => { + return `${Math.round(start * 100) / 100} s - ${ + Math.round(end * 100) / 100 + } s`; +}; + export const formatTime = (timestamp: string) => { return DateTime.fromISO(timestamp).toLocaleString( DateTime.DATETIME_SHORT_WITH_SECONDS, From 57bf1233de2a1530ca2a948357243689743864de Mon Sep 17 00:00:00 2001 From: Niklas Granander Date: Wed, 11 Aug 2021 20:35:40 +0200 Subject: [PATCH 4/9] Fix prettier error Signed-off-by: Niklas Granander --- .../BuildDetailsComponent.tsx | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx index 6696a2083d..0c636b1dcd 100644 --- a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx +++ b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx @@ -173,27 +173,27 @@ type WithRequestProps = Omit & { buildId: string; }; -export const withRequest = (Component: typeof BuildDetailsComponent) => ({ - buildId, - ...props -}: WithRequestProps) => { - const client = useApi(xcmetricsApiRef); - const { value: buildResponse, loading, error } = useAsync( - async () => client.getBuild(buildId), - [], - ); +export const withRequest = + (Component: typeof BuildDetailsComponent) => + ({ buildId, ...props }: WithRequestProps) => { + const client = useApi(xcmetricsApiRef); + const { + value: buildResponse, + loading, + error, + } = useAsync(async () => client.getBuild(buildId), []); - if (loading) { - return ; - } + if (loading) { + return ; + } - if (error) { - return {error.message}; - } + if (error) { + return {error.message}; + } - if (!buildResponse) { - return Could not load build {buildId}; - } + if (!buildResponse) { + return Could not load build {buildId}; + } - return ; -}; + return ; + }; From eed15251ed5747f53493dfb918d54ef2082d561e Mon Sep 17 00:00:00 2001 From: Niklas Granander Date: Wed, 11 Aug 2021 20:54:59 +0200 Subject: [PATCH 5/9] Add changelog Signed-off-by: Niklas Granander --- .changeset/new-ligers-drum.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/new-ligers-drum.md diff --git a/.changeset/new-ligers-drum.md b/.changeset/new-ligers-drum.md new file mode 100644 index 0000000000..0edee17b4d --- /dev/null +++ b/.changeset/new-ligers-drum.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-xcmetrics': patch +--- + +Enable browsing detailed build information such as host configuration, errors, warnings, metadata and a timeline for all targets From 918b535041bbc265a9f1af9769759a0c9f3e9f40 Mon Sep 17 00:00:00 2001 From: Niklas Granander Date: Thu, 12 Aug 2021 11:53:19 +0200 Subject: [PATCH 6/9] Add missing dependencies for requests in BuildDetailsComponent Signed-off-by: Niklas Granander --- .../BuildDetailsComponent/BuildDetailsComponent.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx index 0c636b1dcd..44f43a268b 100644 --- a/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx +++ b/plugins/xcmetrics/src/components/BuildDetailsComponent/BuildDetailsComponent.tsx @@ -47,18 +47,21 @@ export const BuildDetailsComponent = ({ }: BuildDetailsProps) => { const classes = useStyles(); const client = useApi(xcmetricsApiRef); - const hostResult = useAsync(async () => client.getBuildHost(build.id), []); + const hostResult = useAsync( + async () => client.getBuildHost(build.id), + [build.id], + ); const errorsResult = useAsync( async () => client.getBuildErrors(build.id), - [], + [build.id], ); const warningsResult = useAsync( async () => client.getBuildWarnings(build.id), - [], + [build.id], ); const metadataResult = useAsync( async () => client.getBuildMetadata(build.id), - [], + [build.id], ); const buildDetails = { From 3bbe05bb0286579e06fdda6faeaee3d3579ccc25 Mon Sep 17 00:00:00 2001 From: Niklas Granander Date: Thu, 12 Aug 2021 11:54:52 +0200 Subject: [PATCH 7/9] Use named exports Signed-off-by: Niklas Granander --- plugins/xcmetrics/src/components/AccordionComponent/index.ts | 2 +- plugins/xcmetrics/src/components/BuildDetailsComponent/index.ts | 2 +- .../xcmetrics/src/components/BuildTimelineComponent/index.ts | 2 +- plugins/xcmetrics/src/components/BuildsPage/index.ts | 2 +- .../xcmetrics/src/components/PreformattedTextComponent/index.ts | 2 +- plugins/xcmetrics/src/components/StatusIconComponent/index.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/xcmetrics/src/components/AccordionComponent/index.ts b/plugins/xcmetrics/src/components/AccordionComponent/index.ts index 1e71dc81ca..33632d6828 100644 --- a/plugins/xcmetrics/src/components/AccordionComponent/index.ts +++ b/plugins/xcmetrics/src/components/AccordionComponent/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './AccordionComponent'; +export { AccordionComponent } from './AccordionComponent'; diff --git a/plugins/xcmetrics/src/components/BuildDetailsComponent/index.ts b/plugins/xcmetrics/src/components/BuildDetailsComponent/index.ts index 574b29f32e..4e50cf23a0 100644 --- a/plugins/xcmetrics/src/components/BuildDetailsComponent/index.ts +++ b/plugins/xcmetrics/src/components/BuildDetailsComponent/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './BuildDetailsComponent'; +export { BuildDetailsComponent, withRequest } from './BuildDetailsComponent'; diff --git a/plugins/xcmetrics/src/components/BuildTimelineComponent/index.ts b/plugins/xcmetrics/src/components/BuildTimelineComponent/index.ts index 2a6a612acf..fe294f8467 100644 --- a/plugins/xcmetrics/src/components/BuildTimelineComponent/index.ts +++ b/plugins/xcmetrics/src/components/BuildTimelineComponent/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './BuildTimelineComponent'; +export { BuildTimelineComponent } from './BuildTimelineComponent'; diff --git a/plugins/xcmetrics/src/components/BuildsPage/index.ts b/plugins/xcmetrics/src/components/BuildsPage/index.ts index d04503499c..47fe9166df 100644 --- a/plugins/xcmetrics/src/components/BuildsPage/index.ts +++ b/plugins/xcmetrics/src/components/BuildsPage/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './BuildsPage'; +export { BuildsPage } from './BuildsPage'; diff --git a/plugins/xcmetrics/src/components/PreformattedTextComponent/index.ts b/plugins/xcmetrics/src/components/PreformattedTextComponent/index.ts index e923eeb224..dbc27dd869 100644 --- a/plugins/xcmetrics/src/components/PreformattedTextComponent/index.ts +++ b/plugins/xcmetrics/src/components/PreformattedTextComponent/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './PreformattedTextComponent'; +export { PreformattedTextComponent } from './PreformattedTextComponent'; diff --git a/plugins/xcmetrics/src/components/StatusIconComponent/index.ts b/plugins/xcmetrics/src/components/StatusIconComponent/index.ts index e680e6177c..b69415bd78 100644 --- a/plugins/xcmetrics/src/components/StatusIconComponent/index.ts +++ b/plugins/xcmetrics/src/components/StatusIconComponent/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './StatusIconComponent'; +export { StatusIconComponent } from './StatusIconComponent'; From 8aed3a5beb5444ec83135f7945439e0822b59e87 Mon Sep 17 00:00:00 2001 From: Niklas Granander Date: Thu, 12 Aug 2021 11:55:47 +0200 Subject: [PATCH 8/9] Use ellipsis instead of dots Signed-off-by: Niklas Granander --- .../PreformattedTextComponent/PreformattedTextComponent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/xcmetrics/src/components/PreformattedTextComponent/PreformattedTextComponent.tsx b/plugins/xcmetrics/src/components/PreformattedTextComponent/PreformattedTextComponent.tsx index a5ad997ee8..b73e2cead5 100644 --- a/plugins/xcmetrics/src/components/PreformattedTextComponent/PreformattedTextComponent.tsx +++ b/plugins/xcmetrics/src/components/PreformattedTextComponent/PreformattedTextComponent.tsx @@ -89,8 +89,8 @@ export const PreformattedTextComponent = ({ tabIndex={expandable ? 0 : undefined} >
-          {text.slice(0, maxChars - 3).trim()}
-          {text.length > maxChars && '...'}
+          {text.slice(0, maxChars - 1).trim()}
+          {text.length > maxChars - 1 && '…'}
         
From 9dd49cfd3b298584c408e233918f6f93047992c3 Mon Sep 17 00:00:00 2001 From: Niklas Granander Date: Thu, 12 Aug 2021 11:56:34 +0200 Subject: [PATCH 9/9] Match styles of Select for date picker Signed-off-by: Niklas Granander --- .../DatePickerComponent.tsx | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/plugins/xcmetrics/src/components/DatePickerComponent/DatePickerComponent.tsx b/plugins/xcmetrics/src/components/DatePickerComponent/DatePickerComponent.tsx index c6cc2eded5..854fbbd87f 100644 --- a/plugins/xcmetrics/src/components/DatePickerComponent/DatePickerComponent.tsx +++ b/plugins/xcmetrics/src/components/DatePickerComponent/DatePickerComponent.tsx @@ -18,6 +18,7 @@ import { createStyles, InputBase, InputProps, + makeStyles, Theme, Typography, withStyles, @@ -50,6 +51,13 @@ const BootstrapInput = withStyles((theme: Theme) => }), )(InputBase); +const useStyles = makeStyles({ + root: { + display: 'flex', + flexDirection: 'column', + }, +}); + interface DatePickerProps { label: string; onDateChange?: (date: string) => void; @@ -59,16 +67,19 @@ export const DatePickerComponent = ({ label, onDateChange, ...inputProps -}: InputProps & DatePickerProps) => ( - <> - {label} -
- onDateChange?.(event.target.value)} - {...inputProps} - /> - -); +}: InputProps & DatePickerProps) => { + const classes = useStyles(); + + return ( +
+ {label} + onDateChange?.(event.target.value)} + {...inputProps} + /> +
+ ); +};