Use statistics end point for status matrix

This enables fetching less data up front and only
fetching more when needed.

Signed-off-by: Niklas Granander <ngranander@spotify.com>
This commit is contained in:
Niklas Granander
2021-07-15 14:48:04 +02:00
parent 502ed21c51
commit f3a66ccaf1
7 changed files with 198 additions and 78 deletions
+33 -3
View File
@@ -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<BuildItem[]> {
async getBuild(id: string): Promise<Build> {
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<Build[]> {
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<Build>).items;
}
async getBuildStatuses(limit: number): Promise<BuildStatusResult[]> {
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<BuildStatusResult>)
.items;
}
}
+8 -4
View File
@@ -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<Build, 'id' | 'buildStatus'>;
export type PaginationResult<T> = {
items: T[];
metadata: {
per: number;
total: number;
@@ -54,7 +56,9 @@ export type BuildsResult = {
};
export interface XcmetricsApi {
getBuilds(): Promise<BuildItem[]>;
getBuild(id: string): Promise<Build>;
getBuilds(): Promise<Build[]>;
getBuildStatuses(limit: number): Promise<BuildStatusResult[]>;
}
export const xcmetricsApiRef = createApiRef<XcmetricsApi>({
@@ -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<BuildItem>[] = [
const columns: TableColumn<Build>[] = [
{
title: 'Project',
field: 'projectName',
@@ -93,7 +93,7 @@ const columns: TableColumn<BuildItem>[] = [
export const OverviewComponent = () => {
const client = useApi(xcmetricsApiRef);
const { value: builds, loading, error } = useAsync(
async (): Promise<BuildItem[]> => client.getBuilds(),
async () => client.getBuilds(),
[],
);
@@ -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 <div>{error.message}</div>;
} else if (loading || !build) {
return <Progress />;
}
return (
<table>
<tbody>
<tr>
<td>Started</td>
<td>{new Date(build.startTimestamp).toLocaleString()}</td>
</tr>
<tr>
<td>Duration</td>
<td>{formatDuration(build.duration)}</td>
</tr>
<tr>
<td>Status</td>
<td>{formatStatus(build.buildStatus)}</td>
</tr>
</tbody>
</table>
);
};
interface StatusCellProps {
buildStatus: BuildStatusResult; // TODO: Rename this
size: number;
spacing: number;
}
const useStyles = makeStyles<BackstageTheme, StatusCellProps>(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 (
<Tooltip
title={<TooltipContent buildId={buildStatusItem.id} />}
enterNextDelay={500}
arrow
>
<div
data-testid={buildStatusItem.id}
className={cn(classes.root, classes[buildStatusItem.buildStatus])}
/>
</Tooltip>
);
};
@@ -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';
@@ -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<XCMetricsApi> = {
getBuilds: jest.fn().mockResolvedValue([
{
id: 1,
startTimestamp: '2020-11-02T16:38:40Z',
duration: 123.45,
buildStatus: mockStatus,
},
]),
const mockApi: jest.Mocked<XcmetricsApi> = {
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', () => {
</ApiProvider>,
);
const cell = rendered.getByTestId(1);
const cell = rendered.getByTestId(mockId);
expect(cell).toBeInTheDocument();
userEvent.hover(cell);
@@ -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<BackstageTheme>(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<BackstageTheme>(theme => ({
},
}));
const TooltipContent = ({ build }: { build: BuildItem }) => (
<table>
<tbody>
<tr>
<td>Started</td>
<td>{new Date(build.startTimestamp).toLocaleString()}</td>
</tr>
<tr>
<td>Duration</td>
<td>{formatDuration(build.duration)}</td>
</tr>
<tr>
<td>Status</td>
<td>{formatStatus(build.buildStatus)}</td>
</tr>
</tbody>
</table>
);
export const StatusMatrixComponent = () => {
const classes = useStyles();
const [measureRef, { width: rootWidth }] = useMeasure<HTMLDivElement>();
const client = useApi(xcmetricsApiRef);
const { value: builds, loading, error } = useAsync(
async (): Promise<BuildItem[]> => 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 (
<Tooltip key={index} title={<TooltipContent build={build} />} arrow>
<div
data-testid={build.id}
key={build.id}
className={cn(classes.cell, classes[trimmedBuildStatus])}
/>
</Tooltip>
);
})}
builds
.slice(0, cols * MAX_ROWS)
.map((buildStatus, index) => (
<StatusCellComponent
key={index}
buildStatus={buildStatus}
size={CELL_SIZE}
spacing={CELL_MARGIN}
/>
))}
</div>
);
};