diff --git a/plugins/xcmetrics/src/components/StatusMatrixComponent/StatusMatrixComponent.test.tsx b/plugins/xcmetrics/src/components/StatusMatrixComponent/StatusMatrixComponent.test.tsx new file mode 100644 index 0000000000..17532489a8 --- /dev/null +++ b/plugins/xcmetrics/src/components/StatusMatrixComponent/StatusMatrixComponent.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 { StatusMatrixComponent } from './StatusMatrixComponent'; +import { renderInTestApp } from '@backstage/test-utils'; +import { XCMetricsApi, xcmetricsApiRef } from '../../api'; +import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; + +describe('StatusMatrixComponent', () => { + it('should render', async () => { + const mockApi: jest.Mocked = { + getBuilds: jest.fn().mockResolvedValue([ + { + id: 1, + startTimestamp: '2020-11-02T16:38:40Z', + duration: 123.45, + buildStatus: 'succeeded', + }, + ]), + }; + + const rendered = await renderInTestApp( + + + , + ); + + expect(rendered.getByTestId(1)).toBeInTheDocument(); + }); +}); diff --git a/plugins/xcmetrics/src/components/StatusMatrixComponent/StatusMatrixComponent.tsx b/plugins/xcmetrics/src/components/StatusMatrixComponent/StatusMatrixComponent.tsx index 65d8c7af7a..98423822fe 100644 --- a/plugins/xcmetrics/src/components/StatusMatrixComponent/StatusMatrixComponent.tsx +++ b/plugins/xcmetrics/src/components/StatusMatrixComponent/StatusMatrixComponent.tsx @@ -13,45 +13,115 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { makeStyles } from '@material-ui/core'; import React from 'react'; +import { makeStyles, Tooltip } from '@material-ui/core'; import { BackstageTheme } from '@backstage/theme'; -import { BuildStatus } from '../../api'; +import { BuildItem, xcmetricsApiRef } from '../../api'; +import { useAsync, useMeasure } from 'react-use'; +import { formatDuration, formatStatus } from '../../utils'; +import { useApi } from '@backstage/core-plugin-api'; +import { Alert } from '@material-ui/lab'; + +const CELL_SIZE = 12; +const CELL_MARGIN = 4; +const MAX_ROWS = 4; const useStyles = makeStyles(theme => ({ root: { marginTop: 8, + display: 'flex', + flexWrap: 'wrap', + width: '100%', }, - cell: (props: { status?: BuildStatus }) => { - const statusBackgrounds: { [key in BuildStatus]: string } = { - succeeded: theme.palette.success.main, - failed: theme.palette.error.main, - stopped: theme.palette.warning.main, - }; - - return { - width: 12, - height: 12, - margin: '0 4px 4px 0', - float: 'left', - backgroundColor: statusBackgrounds[props.status!], - }; + 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', + }, + '@keyframes loadingOpacity': { + '0%': { opacity: 0.3 }, + '100%': { opacity: 0.8 }, }, })); -const StatusCell = ({ status }: { status: BuildStatus }) => { - const classes = useStyles({ status }); - return
; -}; +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), + [], + ); + + if (error) { + return {error.message}; + } + + const cols = Math.trunc(rootWidth / (CELL_SIZE + CELL_MARGIN)) || 1; return ( -
- {[...Array(240).keys()].map(() => ( - - ))} +
+ {loading && + [...new Array(cols * MAX_ROWS)].map((_, index) => { + return
; + })} + + {builds && + builds.slice(0, cols * MAX_ROWS).map((build, index) => { + const trimmedBuildStatus = build.buildStatus.split(' ').pop()!; + return ( + } arrow> +
+ + ); + })}
); };