Extract cell of status matrix to a component

Signed-off-by: Niklas Granander <ngranander@spotify.com>
This commit is contained in:
Niklas Granander
2021-07-19 11:30:28 +02:00
parent 5e05056e61
commit 7a7680d950
7 changed files with 156 additions and 70 deletions
@@ -16,28 +16,14 @@
import React from 'react';
import { OverviewComponent } from './OverviewComponent';
import { renderInTestApp } from '@backstage/test-utils';
import { XcmetricsApi, xcmetricsApiRef } from '../../api';
import { xcmetricsApiRef } from '../../api';
import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
import { mockUserId, mockXcmetricsApi } from '../../test-utils';
describe('OverviewComponent', () => {
it('should render', async () => {
const mockUserId = 'mockUser';
const mockApi: jest.Mocked<XcmetricsApi> = {
getBuilds: jest.fn().mockResolvedValue([
{
userid: mockUserId,
warningCount: 1,
duration: 123.45,
isCi: false,
projectName: 'App',
buildStatus: 'succeeded',
schema: 'AppSchema',
},
]),
};
const rendered = await renderInTestApp(
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, mockApi)}>
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, mockXcmetricsApi)}>
<OverviewComponent />
</ApiProvider>,
);
@@ -47,12 +33,10 @@ describe('OverviewComponent', () => {
});
it('should render an empty state when no builds exist', async () => {
const mockApi: jest.Mocked<XcmetricsApi> = {
getBuilds: jest.fn().mockResolvedValue([]),
};
mockXcmetricsApi.getBuilds = jest.fn().mockResolvedValue([]);
const rendered = await renderInTestApp(
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, mockApi)}>
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, mockXcmetricsApi)}>
<OverviewComponent />
</ApiProvider>,
);
@@ -61,12 +45,13 @@ describe('OverviewComponent', () => {
it('should show an error when API not responding', async () => {
const errorMessage = 'MockErrorMessage';
const mockApi: jest.Mocked<XcmetricsApi> = {
getBuilds: jest.fn().mockRejectedValue({ message: errorMessage }),
};
mockXcmetricsApi.getBuilds = jest
.fn()
.mockRejectedValue({ message: errorMessage });
const rendered = await renderInTestApp(
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, mockApi)}>
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, mockXcmetricsApi)}>
<OverviewComponent />
</ApiProvider>,
);
@@ -0,0 +1,42 @@
/*
* 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 userEvent from '@testing-library/user-event';
import { StatusCellComponent } from './StatusCellComponent';
import { xcmetricsApiRef } from '../../api';
import { mockBuildId, mockStatus, mockXcmetricsApi } from '../../test-utils';
import { formatStatus } from '../../utils';
describe('StatusCellComponent', () => {
it('should render', async () => {
const rendered = await renderInTestApp(
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, mockXcmetricsApi)}>
<StatusCellComponent
buildStatus={{ id: mockBuildId, buildStatus: mockStatus }}
size={10}
spacing={10}
/>
</ApiProvider>,
);
userEvent.hover(rendered.getByTestId(mockBuildId));
expect(
await rendered.findByText(formatStatus(mockStatus)),
).toBeInTheDocument();
});
});
@@ -16,7 +16,7 @@
import { makeStyles, Tooltip } from '@material-ui/core';
import React from 'react';
import { BackstageTheme } from '@backstage/theme';
import { BuildStatusResult, xcmetricsApiRef } from '../../api';
import { BuildStatus, BuildStatusResult, xcmetricsApiRef } from '../../api';
import { cn, formatDuration, formatStatus } from '../../utils';
import { useAsync } from 'react-use';
import { useApi } from '@backstage/core-plugin-api';
@@ -36,7 +36,7 @@ const TooltipContent = ({ buildId }: TooltipContentProps) => {
if (error) {
return <div>{error.message}</div>;
} else if (loading || !build) {
return <Progress />;
return <Progress style={{ width: 100 }} />;
}
return (
@@ -60,11 +60,15 @@ const TooltipContent = ({ buildId }: TooltipContentProps) => {
};
interface StatusCellProps {
buildStatus: BuildStatusResult; // TODO: Rename this
buildStatus?: BuildStatusResult;
size: number;
spacing: number;
}
type StatusStyle = {
[key in BuildStatus]: any;
};
const useStyles = makeStyles<BackstageTheme, StatusCellProps>(theme => ({
root: {
width: ({ size }) => size,
@@ -76,33 +80,43 @@ const useStyles = makeStyles<BackstageTheme, StatusCellProps>(theme => ({
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],
},
...({
succeeded: {
backgroundColor:
theme.palette.type === 'light'
? theme.palette.success.light
: theme.palette.success.main,
},
} as StatusStyle), // Make sure that key matches a status
...({
failed: {
backgroundColor: theme.palette.error[theme.palette.type],
},
} as StatusStyle),
...({
stopped: {
backgroundColor: theme.palette.warning[theme.palette.type],
},
} as StatusStyle),
}));
export const StatusCellComponent = (props: StatusCellProps) => {
const classes = useStyles(props);
const { buildStatus: buildStatusItem } = props;
const { buildStatus } = props;
if (!buildStatus) {
return <div className={classes.root} />;
}
return (
<Tooltip
title={<TooltipContent buildId={buildStatusItem.id} />}
title={<TooltipContent buildId={buildStatus.id} />}
enterNextDelay={500}
arrow
>
<div
data-testid={buildStatusItem.id}
className={cn(classes.root, classes[buildStatusItem.buildStatus])}
data-testid={buildStatus.id}
className={cn(classes.root, classes[buildStatus.buildStatus])}
/>
</Tooltip>
);
@@ -16,40 +16,19 @@
import React from 'react';
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 { formatStatus } from '../../utils';
import { xcmetricsApiRef } from '../../api';
import { mockBuildId, mockXcmetricsApi } from '../../test-utils';
describe('StatusMatrixComponent', () => {
it('should render', async () => {
const mockId = 'mockId';
const mockStatus = 'succeeded';
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(
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, mockApi)}>
<ApiProvider apis={ApiRegistry.with(xcmetricsApiRef, mockXcmetricsApi)}>
<StatusMatrixComponent />
</ApiProvider>,
);
const cell = rendered.getByTestId(mockId);
const cell = rendered.getByTestId(mockBuildId);
expect(cell).toBeInTheDocument();
userEvent.hover(cell);
expect(
await rendered.findByText(formatStatus(mockStatus)),
).toBeInTheDocument();
});
});
@@ -66,7 +66,13 @@ export const StatusMatrixComponent = () => {
>
{loading &&
[...new Array(cols * MAX_ROWS)].map((_, index) => {
return <div key={index} className={classes.cell} />;
return (
<StatusCellComponent
key={index}
size={CELL_SIZE}
spacing={CELL_MARGIN}
/>
);
})}
{builds &&
+16
View File
@@ -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 './mockXcmetricsApi';
@@ -0,0 +1,44 @@
/*
* 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 { XcmetricsApi } from '../api';
export const mockUserId = 'user_id';
export const mockBuildId = 'build_id';
export const mockStatus = 'succeeded';
export const mockXcmetricsApi: jest.Mocked<XcmetricsApi> = {
getBuildStatuses: jest
.fn()
.mockResolvedValue([{ id: mockBuildId, status: mockStatus }]),
getBuild: jest.fn().mockResolvedValue({
id: mockBuildId,
buildStatus: 'succeeded',
duration: 10.0,
startTimestamp: '1626365026',
}),
getBuilds: jest.fn().mockResolvedValue([
{
userid: mockUserId,
warningCount: 1,
duration: 123.45,
isCi: false,
projectName: 'App',
buildStatus: mockStatus,
schema: 'AppSchema',
},
]),
getBuildCounts: jest.fn(),
};