diff --git a/plugins/xcmetrics/src/api/XcmetricsClient.ts b/plugins/xcmetrics/src/api/XcmetricsClient.ts index 5eec12d285..2fcdaa3150 100644 --- a/plugins/xcmetrics/src/api/XcmetricsClient.ts +++ b/plugins/xcmetrics/src/api/XcmetricsClient.ts @@ -16,7 +16,12 @@ import { DiscoveryApi } from '@backstage/core-plugin-api'; import { ResponseError } from '@backstage/errors'; -import { BuildItem, BuildsResult, XcmetricsApi } from './types'; +import { + Build, + BuildStatusResult, + PaginationResult, + XcmetricsApi, +} from './types'; interface Options { discoveryApi: DiscoveryApi; @@ -29,7 +34,18 @@ export class XcmetricsClient implements XcmetricsApi { this.discoveryApi = options.discoveryApi; } - async getBuilds(limit: number = 10): Promise { + 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); + } + + 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}`); @@ -37,6 +53,20 @@ export class XcmetricsClient implements XcmetricsApi { throw await ResponseError.fromResponse(response); } - return ((await response.json()) as BuildsResult).items; + return ((await response.json()) as PaginationResult).items; + } + + 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; } } diff --git a/plugins/xcmetrics/src/api/types.ts b/plugins/xcmetrics/src/api/types.ts index add59b1a86..d995c53724 100644 --- a/plugins/xcmetrics/src/api/types.ts +++ b/plugins/xcmetrics/src/api/types.ts @@ -18,7 +18,7 @@ import { createApiRef } from '@backstage/core-plugin-api'; export type BuildStatus = 'succeeded' | 'failed' | 'stopped'; -export type BuildItem = { +export type Build = { userid: string; warningCount: number; duration: number; @@ -44,8 +44,10 @@ export type BuildItem = { wasSuspended: boolean; }; -export type BuildsResult = { - items: BuildItem[]; +export type BuildStatusResult = Pick; + +export type PaginationResult = { + items: T[]; metadata: { per: number; total: number; @@ -54,7 +56,9 @@ export type BuildsResult = { }; export interface XcmetricsApi { - getBuilds(): Promise; + getBuild(id: string): Promise; + getBuilds(): Promise; + getBuildStatuses(limit: number): Promise; } export const xcmetricsApiRef = createApiRef({ diff --git a/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.tsx b/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.tsx index da2aac144b..613a7a031a 100644 --- a/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.tsx +++ b/plugins/xcmetrics/src/components/OverviewComponent/OverviewComponent.tsx @@ -26,7 +26,7 @@ import { EmptyState, } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; -import { BuildItem, BuildStatus, xcmetricsApiRef } from '../../api'; +import { Build, BuildStatus, xcmetricsApiRef } from '../../api'; import { useAsync } from 'react-use'; import { Alert } from '@material-ui/lab'; import { Chip } from '@material-ui/core'; @@ -55,7 +55,7 @@ const Status = ({ ); }; -const columns: TableColumn[] = [ +const columns: TableColumn[] = [ { title: 'Project', field: 'projectName', @@ -93,7 +93,7 @@ const columns: TableColumn[] = [ export const OverviewComponent = () => { const client = useApi(xcmetricsApiRef); const { value: builds, loading, error } = useAsync( - async (): Promise => client.getBuilds(), + async () => client.getBuilds(), [], ); diff --git a/plugins/xcmetrics/src/components/StatusCellComponent/StatusCellComponent.tsx b/plugins/xcmetrics/src/components/StatusCellComponent/StatusCellComponent.tsx new file mode 100644 index 0000000000..f8c05612a6 --- /dev/null +++ b/plugins/xcmetrics/src/components/StatusCellComponent/StatusCellComponent.tsx @@ -0,0 +1,109 @@ +/* + * 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 { makeStyles, Tooltip } from '@material-ui/core'; +import React from 'react'; +import { BackstageTheme } from '@backstage/theme'; +import { BuildStatusResult, xcmetricsApiRef } from '../../api'; +import { cn, formatDuration, formatStatus } from '../../utils'; +import { useAsync } from 'react-use'; +import { useApi } from '@backstage/core-plugin-api'; +import { Progress } from '@backstage/core-components'; + +interface TooltipContentProps { + buildId: string; +} + +const TooltipContent = ({ buildId }: TooltipContentProps) => { + const client = useApi(xcmetricsApiRef); + const { value: build, loading, error } = useAsync( + async () => client.getBuild(buildId), + [], + ); + + if (error) { + return
{error.message}
; + } else if (loading || !build) { + return ; + } + + return ( + + + + + + + + + + + + + + + +
Started{new Date(build.startTimestamp).toLocaleString()}
Duration{formatDuration(build.duration)}
Status{formatStatus(build.buildStatus)}
+ ); +}; + +interface StatusCellProps { + buildStatus: BuildStatusResult; // TODO: Rename this + size: number; + spacing: number; +} + +const useStyles = makeStyles(theme => ({ + root: { + width: ({ size }) => size, + height: ({ size }) => size, + marginRight: ({ spacing }) => spacing, + marginBottom: ({ spacing }) => spacing, + backgroundColor: theme.palette.grey[600], + '&:hover': { + transform: 'scale(1.2)', + }, + }, + succeeded: { + backgroundColor: + theme.palette.type === 'light' + ? theme.palette.success.light + : theme.palette.success.main, + }, + failed: { + backgroundColor: theme.palette.error[theme.palette.type], + }, + stopped: { + backgroundColor: theme.palette.warning[theme.palette.type], + }, +})); + +export const StatusCellComponent = (props: StatusCellProps) => { + const classes = useStyles(props); + const { buildStatus: buildStatusItem } = props; + + return ( + } + enterNextDelay={500} + arrow + > +
+ + ); +}; diff --git a/plugins/xcmetrics/src/components/StatusCellComponent/index.ts b/plugins/xcmetrics/src/components/StatusCellComponent/index.ts new file mode 100644 index 0000000000..e1d4f81e34 --- /dev/null +++ b/plugins/xcmetrics/src/components/StatusCellComponent/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 './StatusCellComponent'; diff --git a/plugins/xcmetrics/src/components/StatusMatrixComponent/StatusMatrixComponent.test.tsx b/plugins/xcmetrics/src/components/StatusMatrixComponent/StatusMatrixComponent.test.tsx index c5217b4fa9..3a84bda0e3 100644 --- a/plugins/xcmetrics/src/components/StatusMatrixComponent/StatusMatrixComponent.test.tsx +++ b/plugins/xcmetrics/src/components/StatusMatrixComponent/StatusMatrixComponent.test.tsx @@ -18,21 +18,24 @@ import { renderInTestApp } from '@backstage/test-utils'; import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import userEvent from '@testing-library/user-event'; import { StatusMatrixComponent } from './StatusMatrixComponent'; -import { XCMetricsApi, xcmetricsApiRef } from '../../api'; +import { XcmetricsApi, xcmetricsApiRef } from '../../api'; import { formatStatus } from '../../utils'; describe('StatusMatrixComponent', () => { it('should render', async () => { + const mockId = 'mockId'; const mockStatus = 'succeeded'; - const mockApi: jest.Mocked = { - getBuilds: jest.fn().mockResolvedValue([ - { - id: 1, - startTimestamp: '2020-11-02T16:38:40Z', - duration: 123.45, - buildStatus: mockStatus, - }, - ]), + const mockApi: jest.Mocked = { + getBuildStatuses: jest + .fn() + .mockResolvedValue([{ id: mockId, buildStatus: mockStatus }]), + getBuild: jest.fn().mockResolvedValue({ + id: mockId, + buildStatus: mockStatus, + duration: 10.0, + startTimestamp: new Date().getTime().toString(), + }), + getBuilds: jest.fn().mockResolvedValue([]), }; const rendered = await renderInTestApp( @@ -41,7 +44,7 @@ describe('StatusMatrixComponent', () => { , ); - const cell = rendered.getByTestId(1); + const cell = rendered.getByTestId(mockId); expect(cell).toBeInTheDocument(); userEvent.hover(cell); diff --git a/plugins/xcmetrics/src/components/StatusMatrixComponent/StatusMatrixComponent.tsx b/plugins/xcmetrics/src/components/StatusMatrixComponent/StatusMatrixComponent.tsx index f25469d292..92c605e8fb 100644 --- a/plugins/xcmetrics/src/components/StatusMatrixComponent/StatusMatrixComponent.tsx +++ b/plugins/xcmetrics/src/components/StatusMatrixComponent/StatusMatrixComponent.tsx @@ -14,13 +14,14 @@ * limitations under the License. */ import React from 'react'; -import { makeStyles, Tooltip } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core'; import { BackstageTheme } from '@backstage/theme'; -import { BuildItem, xcmetricsApiRef } from '../../api'; +import { xcmetricsApiRef } from '../../api'; import { useAsync, useMeasure } from 'react-use'; -import { cn, formatDuration, formatStatus } from '../../utils'; +import { cn } from '../../utils'; import { useApi } from '@backstage/core-plugin-api'; import { Alert } from '@material-ui/lab'; +import { StatusCellComponent } from '../StatusCellComponent'; const CELL_SIZE = 12; const CELL_MARGIN = 4; @@ -33,28 +34,6 @@ const useStyles = makeStyles(theme => ({ flexWrap: 'wrap', width: '100%', }, - cell: { - width: CELL_SIZE, - height: CELL_SIZE, - marginRight: CELL_MARGIN, - marginBottom: CELL_MARGIN, - backgroundColor: theme.palette.grey[600], - '&:hover': { - transform: 'scale(1.2)', - }, - }, - succeeded: { - backgroundColor: - theme.palette.type === 'light' - ? theme.palette.success.light - : theme.palette.success.main, - }, - failed: { - backgroundColor: theme.palette.error[theme.palette.type], - }, - stopped: { - backgroundColor: theme.palette.warning[theme.palette.type], - }, loading: { animation: `$loadingOpacity 900ms ${theme.transitions.easing.easeInOut}`, animationIterationCount: 'infinite', @@ -65,31 +44,12 @@ const useStyles = makeStyles(theme => ({ }, })); -const TooltipContent = ({ build }: { build: BuildItem }) => ( - - - - - - - - - - - - - - - -
Started{new Date(build.startTimestamp).toLocaleString()}
Duration{formatDuration(build.duration)}
Status{formatStatus(build.buildStatus)}
-); - export const StatusMatrixComponent = () => { const classes = useStyles(); const [measureRef, { width: rootWidth }] = useMeasure(); const client = useApi(xcmetricsApiRef); const { value: builds, loading, error } = useAsync( - async (): Promise => client.getBuilds(300), + async () => client.getBuildStatuses(300), [], ); @@ -110,18 +70,16 @@ export const StatusMatrixComponent = () => { })} {builds && - builds.slice(0, cols * MAX_ROWS).map((build, index) => { - const trimmedBuildStatus = build.buildStatus.split(' ').pop()!; - return ( - } arrow> -
- - ); - })} + builds + .slice(0, cols * MAX_ROWS) + .map((buildStatus, index) => ( + + ))}
); };