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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
+14
-11
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user