Replacing SortableTable and a few MUI tables with new Table component (#722)
* Befind work of replacing tables with new Table component * Removed test plugins and fixed test * Fixed lint issue * Removed toolbar from lighthouse audit page * Moved column out of auditstables render function, no no interation with useInterval
This commit is contained in:
committed by
GitHub
parent
5b0e808204
commit
425cf93c08
+1
-3
@@ -47,9 +47,7 @@ const ExampleComponent: FC<{}> = () => (
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<InfoCard title="Example User List (fetching data from randomuser.me)">
|
||||
<ExampleFetchComponent />
|
||||
</InfoCard>
|
||||
<ExampleFetchComponent />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
|
||||
+29
-40
@@ -16,20 +16,11 @@
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Table from '@material-ui/core/Table';
|
||||
import TableBody from '@material-ui/core/TableBody';
|
||||
import TableCell from '@material-ui/core/TableCell';
|
||||
import TableContainer from '@material-ui/core/TableContainer';
|
||||
import TableHead from '@material-ui/core/TableHead';
|
||||
import TableRow from '@material-ui/core/TableRow';
|
||||
import { Table, TableColumn, Progress } from '@backstage/core';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import { useAsync } from 'react-use';
|
||||
import { Progress } from '@backstage/core';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
table: {
|
||||
minWidth: 650,
|
||||
},
|
||||
avatar: {
|
||||
height: 32,
|
||||
width: 32,
|
||||
@@ -66,37 +57,35 @@ type DenseTableProps = {
|
||||
export const DenseTable: FC<DenseTableProps> = ({ users }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{ title: 'Avatar', field: 'avatar' },
|
||||
{ title: 'Name', field: 'name' },
|
||||
{ title: 'Email', field: 'email' },
|
||||
{ title: 'Nationality', field: 'nationality' },
|
||||
];
|
||||
|
||||
const data = users.map((user) => {
|
||||
return {
|
||||
avatar: (
|
||||
<img
|
||||
src={user.picture.medium}
|
||||
className={classes.avatar}
|
||||
alt={user.name.first}
|
||||
/>
|
||||
),
|
||||
name: `${user.name.first} ${user.name.last}`,
|
||||
email: user.email,
|
||||
nationality: user.nat,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table className={classes.table} size="small" aria-label="a dense table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Avatar</TableCell>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Email</TableCell>
|
||||
<TableCell>Nationality</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{users.map(user => (
|
||||
<TableRow key={user.email}>
|
||||
<TableCell>
|
||||
<img
|
||||
src={user.picture.medium}
|
||||
className={classes.avatar}
|
||||
alt={user.name.first}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{user.name.first} {user.name.last}
|
||||
</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell>{user.nat}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Table
|
||||
title="Example User List (fetching data from randomuser.me)"
|
||||
options=\{{ search: false, paging: false }}
|
||||
columns={columns}
|
||||
data={data}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { StatusError, StatusOK, StatusWarning } from 'components/Status';
|
||||
import SortableTable from './SortableTable';
|
||||
|
||||
export default {
|
||||
title: 'Sortable Table',
|
||||
component: SortableTable,
|
||||
};
|
||||
const containerStyle = { width: 600, padding: 20 };
|
||||
|
||||
const data = [
|
||||
{ id: 'buffalos', amount: 1, status: <StatusError />, statusValue: 2 },
|
||||
{ id: 'milk', amount: 3, status: <StatusWarning />, statusValue: 1 },
|
||||
{ id: 'cheese', amount: 8, status: <StatusWarning />, statusValue: 1 },
|
||||
{ id: 'bread', amount: 2, status: <StatusOK />, statusValue: 0 },
|
||||
];
|
||||
const columns = [
|
||||
{ id: 'id', label: 'ID' },
|
||||
{ id: 'amount', disablePadding: false, numeric: true, label: 'AMOUNT' },
|
||||
{
|
||||
id: 'status',
|
||||
label: 'STATUS',
|
||||
sortValue: (row: typeof data[0]) => row.statusValue,
|
||||
},
|
||||
];
|
||||
const footerData = [
|
||||
{ id: 'total', amount: 4, statusValue: 2, status: <StatusError /> },
|
||||
];
|
||||
|
||||
export const Default = () => (
|
||||
<div style={containerStyle}>
|
||||
<SortableTable orderBy="desc" data={data} columns={columns} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const WithFooter = () => (
|
||||
<div style={containerStyle}>
|
||||
<SortableTable
|
||||
orderBy="asc"
|
||||
data={data}
|
||||
columns={columns}
|
||||
footerData={footerData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -1,335 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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, { FC, CSSProperties, memo, useState, useEffect } from 'react';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableSortLabel,
|
||||
Tooltip,
|
||||
} from '@material-ui/core';
|
||||
|
||||
type Column = {
|
||||
id: string;
|
||||
label: string;
|
||||
numeric?: boolean;
|
||||
disablePadding: boolean;
|
||||
style: CSSProperties;
|
||||
// FIXME (@Koroeskohr)
|
||||
sortValue: (obj: object) => any;
|
||||
};
|
||||
|
||||
// XXX (@Koroeskohr): no idea what I did but this typechecks. Need answer for CellContentProps
|
||||
type Row = { [name in string]: any };
|
||||
|
||||
type SortHandler = (
|
||||
event: React.MouseEvent<Element, MouseEvent>,
|
||||
property: string,
|
||||
) => void;
|
||||
|
||||
type Order = 'asc' | 'desc';
|
||||
|
||||
type EnhancedTableHeadProps = {
|
||||
columns: Column[];
|
||||
onRequestSort: SortHandler;
|
||||
order: Order;
|
||||
orderBy: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Table header which supports sorting ascending and desc
|
||||
*/
|
||||
const EnhancedTableHead: FC<EnhancedTableHeadProps> = ({
|
||||
columns,
|
||||
onRequestSort,
|
||||
order,
|
||||
orderBy,
|
||||
}) => {
|
||||
const createSortHandler = (property: string) => (
|
||||
event: React.MouseEvent<HTMLElement, MouseEvent>,
|
||||
) => {
|
||||
onRequestSort(event, property);
|
||||
};
|
||||
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map(column => (
|
||||
<TableCell
|
||||
key={column.id}
|
||||
align={column.numeric ? 'right' : 'left'}
|
||||
size={column.disablePadding ? 'small' : 'medium'}
|
||||
sortDirection={orderBy === column.id ? order : false}
|
||||
style={column.style}
|
||||
>
|
||||
<Tooltip
|
||||
title="Sort"
|
||||
placement={column.numeric ? 'bottom-end' : 'bottom-start'}
|
||||
enterDelay={300}
|
||||
>
|
||||
<TableSortLabel
|
||||
active={orderBy === column.id}
|
||||
direction={order}
|
||||
onClick={createSortHandler(column.id)}
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{column.label}
|
||||
</TableSortLabel>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
);
|
||||
};
|
||||
|
||||
type CellContentProps = {
|
||||
// XXX (@Koroeskohr): what am I supposed to use here
|
||||
data: any | any[];
|
||||
};
|
||||
|
||||
/**
|
||||
* CellContent can be an array or a string
|
||||
*/
|
||||
const CellContent: FC<CellContentProps> = ({ data }) => {
|
||||
if (Array.isArray(data)) {
|
||||
return data.map((item, index) => (
|
||||
<span key={index}>
|
||||
{item}
|
||||
<br />
|
||||
</span>
|
||||
));
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
type DataTableCellProps = {
|
||||
column: Column;
|
||||
row: Row;
|
||||
};
|
||||
|
||||
const DataTableCell: FC<DataTableCellProps> = ({ column, row }) => {
|
||||
return (
|
||||
<TableCell
|
||||
align={column.numeric ? 'right' : 'left'}
|
||||
key={column.id}
|
||||
style={{ verticalAlign: 'top' }}
|
||||
>
|
||||
<CellContent data={row[column.id] || ''} />
|
||||
</TableCell>
|
||||
);
|
||||
};
|
||||
|
||||
const noop = () => {};
|
||||
type DataTableRowProps = {
|
||||
row: Row;
|
||||
columns: Column[];
|
||||
handleRowClick?: (event: React.MouseEvent, rowId: string) => void;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
const _DataTableRow: FC<DataTableRowProps> = ({
|
||||
row,
|
||||
columns,
|
||||
handleRowClick,
|
||||
style,
|
||||
}) => {
|
||||
const onClick: React.MouseEventHandler = event =>
|
||||
(handleRowClick || noop)(event, row.id);
|
||||
return (
|
||||
<TableRow onClick={onClick} key={row.id} style={style}>
|
||||
{columns.map(column => (
|
||||
<DataTableCell key={column.id} column={column} row={row} />
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
const DataTableRow = memo(_DataTableRow);
|
||||
|
||||
/**
|
||||
* Table with sorting capabilites automatic rendering of cells
|
||||
* Note that the objects in props.data needs have an id property
|
||||
* The columns array defines which columns from the data to show.
|
||||
*
|
||||
* @param data A list of data entries, where object properties must
|
||||
* be strictly equal to column ids.
|
||||
*
|
||||
* @param columns A list of columns with the following shape:
|
||||
* {
|
||||
* // The column identifier must be strictly equal the relevant data entry
|
||||
* // key:
|
||||
* id: String,
|
||||
*
|
||||
* // The display label for the column:
|
||||
* label: String,
|
||||
*
|
||||
* // If true, the column contents will be right-aligned:
|
||||
* numeric: Boolean,
|
||||
*
|
||||
* // If true, padding will be disabled for table cells:
|
||||
* disablePadding: Boolean,
|
||||
*
|
||||
* // A function taking a data row and returning a suitable primitive for
|
||||
* // sorting:
|
||||
* sortValue: (Object) => Any
|
||||
* }
|
||||
*
|
||||
* @param orderBy The column ID initially used for sorting
|
||||
*
|
||||
* @param dataVersion A version identifier for the data which *must*
|
||||
* be updated when the contents of the data changes. This can be used for
|
||||
* components where the same SortableTable element will be used to display
|
||||
* variable sets of data.
|
||||
*
|
||||
* @param footerData A list of data entries to be placed in
|
||||
* the table footer, which will not be sorted.
|
||||
*
|
||||
* @param onRowClicked Get notified when a user clicks
|
||||
* on the row. The handler will receive the row id as the first argument, and
|
||||
* the synthetic click event as the second argument.
|
||||
*
|
||||
* @example
|
||||
* render {
|
||||
* const data = [
|
||||
* { id: 'buffalos', amount: 1, status: <Error />, statusValue: 2 },
|
||||
* { id: 'milk', amount: 3, status: <Warning />, statusValue: 1 }
|
||||
* ];
|
||||
* const columns = [
|
||||
* { id: 'id', label: 'ID' },
|
||||
* { id: 'amount', disablePadding: false, numeric: true, label: 'AMOUNT' },
|
||||
* { id: 'status', label: 'STATUS', sortValue: row => row.statusValue },
|
||||
* ];
|
||||
* const footerData = [
|
||||
* { id: 'total', amount: 4, statusValue: 2, status: <Error /> },
|
||||
* ];
|
||||
* return (
|
||||
* <SortableTable data={data} footerData={footerData} orderBy={'id'} columns={columns}
|
||||
* onRowClicked={(id, ev) => {console.log('Row:' + id + ' clicked');
|
||||
* ev.preventDefault();}}/>)
|
||||
* }
|
||||
*
|
||||
* // XXX (@koroeskohr): supposedly this is leftover from your internal doc
|
||||
* @deprecated use shared/components/DataGrid
|
||||
*/
|
||||
|
||||
type SortableTableProps = {
|
||||
data: Row[];
|
||||
footerData: Row[];
|
||||
orderBy: string;
|
||||
columns: Column[];
|
||||
onRowClicked?: (
|
||||
id: string,
|
||||
event: React.MouseEvent<Element, MouseEvent>,
|
||||
) => void;
|
||||
dataVersion: string;
|
||||
};
|
||||
|
||||
type TableState = {
|
||||
orderBy: string;
|
||||
order: Order;
|
||||
data: Row[];
|
||||
};
|
||||
|
||||
const SortableTable: FC<SortableTableProps> = props => {
|
||||
const [state, setState] = useState<TableState>({
|
||||
orderBy: props.orderBy,
|
||||
order: 'asc',
|
||||
data: props.data,
|
||||
});
|
||||
|
||||
const updateData = (data: Row[], orderBy: string, order: Order) => {
|
||||
const sortValueFn = (props.columns.find(col => col.id === orderBy) || {})
|
||||
.sortValue;
|
||||
|
||||
const sortedData = data.slice().sort((a, b) => {
|
||||
const valueA = sortValueFn ? sortValueFn(a) : a[orderBy];
|
||||
const valueB = sortValueFn ? sortValueFn(b) : b[orderBy];
|
||||
const inc = order === 'desc' ? -1 : 1;
|
||||
if (valueA === valueB) return 0;
|
||||
if (valueA === '' || valueA === null) return inc;
|
||||
if (valueB === '' || valueB === null) return -inc;
|
||||
return valueA < valueB ? -inc : inc;
|
||||
});
|
||||
setState({ data: sortedData, order, orderBy });
|
||||
};
|
||||
|
||||
const handleRequestSort: SortHandler = (event, property) => {
|
||||
event.preventDefault();
|
||||
const orderBy = property;
|
||||
let order: Order = 'desc';
|
||||
if (state.orderBy === property && state.order === 'desc') {
|
||||
order = 'asc';
|
||||
}
|
||||
updateData(state.data, orderBy, order);
|
||||
};
|
||||
|
||||
const handleRowClick = (event: React.MouseEvent, id: string): void => {
|
||||
if (props.onRowClicked) {
|
||||
props.onRowClicked(id, event);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const { data } = props;
|
||||
const { orderBy, order } = state;
|
||||
updateData(data, orderBy, order);
|
||||
}, [props.dataVersion]);
|
||||
|
||||
const { data, order, orderBy } = state;
|
||||
const { columns, dataVersion, footerData } = props;
|
||||
|
||||
let tableFoot = null;
|
||||
if (footerData && footerData.length > 0) {
|
||||
tableFoot = (
|
||||
<TableFooter>
|
||||
{footerData.map(row => (
|
||||
<DataTableRow
|
||||
key={row.id}
|
||||
columns={columns}
|
||||
row={row}
|
||||
style={{ height: 'auto' }}
|
||||
/>
|
||||
))}
|
||||
</TableFooter>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Table>
|
||||
<EnhancedTableHead
|
||||
columns={columns}
|
||||
onRequestSort={handleRequestSort}
|
||||
order={order}
|
||||
orderBy={orderBy}
|
||||
/>
|
||||
<TableBody key={dataVersion}>
|
||||
{data.map(row => (
|
||||
<DataTableRow
|
||||
key={row.id}
|
||||
columns={columns}
|
||||
row={row}
|
||||
handleRowClick={handleRowClick}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
{tableFoot}
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export default SortableTable;
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { default } from './SortableTable';
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
StatusRunning,
|
||||
StatusWarning,
|
||||
} from './Status';
|
||||
import SortableTable from 'components/SortableTable';
|
||||
import Table from 'components/Table';
|
||||
import InfoCard from 'layout/InfoCard';
|
||||
|
||||
export default {
|
||||
@@ -71,9 +71,9 @@ const data = [
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{ id: 'status', label: 'Status' },
|
||||
{ id: 'label', label: 'Label' },
|
||||
{ id: 'usage', label: 'Example usage' },
|
||||
{ field: 'status', title: 'Status' },
|
||||
{ field: 'label', title: 'Label' },
|
||||
{ field: 'usage', title: 'Example usage' },
|
||||
];
|
||||
|
||||
const containerStyle = { width: 600 };
|
||||
@@ -81,7 +81,15 @@ const containerStyle = { width: 600 };
|
||||
export const Default = () => (
|
||||
<div style={containerStyle}>
|
||||
<InfoCard title="Available status types">
|
||||
<SortableTable data={data} columns={columns} />
|
||||
<Table
|
||||
options={{
|
||||
search: false,
|
||||
paging: false,
|
||||
toolbar: false,
|
||||
}}
|
||||
data={data}
|
||||
columns={columns}
|
||||
/>
|
||||
</InfoCard>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import TrendLine from '.';
|
||||
import SortableTable from 'components/SortableTable';
|
||||
import Table from 'components/Table';
|
||||
import InfoCard from 'layout/InfoCard';
|
||||
|
||||
export default {
|
||||
@@ -58,17 +58,25 @@ const data = [
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{ id: 'stock', label: 'Stock' },
|
||||
{ id: 'day', label: 'Day' },
|
||||
{ id: 'week', label: 'Week' },
|
||||
{ id: 'month', label: 'Month' },
|
||||
{ id: 'year', label: 'Year' },
|
||||
{ field: 'stock', title: 'Stock' },
|
||||
{ field: 'day', title: 'Day' },
|
||||
{ field: 'week', title: 'Week' },
|
||||
{ field: 'month', title: 'Month' },
|
||||
{ field: 'year', title: 'Year' },
|
||||
];
|
||||
|
||||
export const Default = () => (
|
||||
<div style={containerStyle}>
|
||||
<InfoCard title="Trends over time">
|
||||
<SortableTable data={data} columns={columns} />
|
||||
<Table
|
||||
options={{
|
||||
search: false,
|
||||
paging: false,
|
||||
toolbar: false,
|
||||
}}
|
||||
data={data}
|
||||
columns={columns}
|
||||
/>
|
||||
</InfoCard>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -35,7 +35,8 @@ export { default as Progress } from './components/Progress';
|
||||
export * from './components/SimpleStepper';
|
||||
export { AlphaLabel, BetaLabel } from './components/Lifecycle';
|
||||
export { default as SupportButton } from './components/SupportButton';
|
||||
export { default as SortableTable } from './components/SortableTable';
|
||||
export { default as Table, SubvalueCell } from './components/Table';
|
||||
export type { TableColumn } from './components/Table/Table';
|
||||
export { default as StructuredMetadataTable } from './components/StructuredMetadataTable';
|
||||
export { default as TrendLine } from './components/TrendLine';
|
||||
export { FeatureCalloutCircular } from './components/FeatureDiscovery/FeatureCalloutCircular';
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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, { FC } from 'react';
|
||||
import { Link, TableCell, TableRow } from '@material-ui/core';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { TrendLine } from '@backstage/core';
|
||||
import { Website } from '../../api';
|
||||
import {
|
||||
formatTime,
|
||||
CATEGORIES,
|
||||
CATEGORY_LABELS,
|
||||
SparklinesDataByCategory,
|
||||
} from '../../utils';
|
||||
import AuditStatusIcon from '../AuditStatusIcon';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
table: {
|
||||
minWidth: 650,
|
||||
},
|
||||
status: {
|
||||
textTransform: 'capitalize',
|
||||
},
|
||||
link: {
|
||||
paddingTop: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(2),
|
||||
display: 'inline-block',
|
||||
},
|
||||
statusCell: { whiteSpace: 'nowrap' },
|
||||
sparklinesCell: { minWidth: 120 },
|
||||
}));
|
||||
|
||||
export const AuditRow: FC<{
|
||||
website: Website;
|
||||
categorySparkline: SparklinesDataByCategory;
|
||||
}> = ({ website, categorySparkline }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<TableRow key={website.url}>
|
||||
<TableCell>
|
||||
<Link
|
||||
className={classes.link}
|
||||
href={`/lighthouse/audit/${website.lastAudit.id}`}
|
||||
>
|
||||
{website.url}
|
||||
</Link>
|
||||
</TableCell>
|
||||
{CATEGORIES.map(category => (
|
||||
<TableCell
|
||||
key={`${website.url}|${category}`}
|
||||
className={classes.sparklinesCell}
|
||||
>
|
||||
<TrendLine
|
||||
title={`trendline for ${CATEGORY_LABELS[category]} category of ${website.url}`}
|
||||
data={categorySparkline[category] || []}
|
||||
/>
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell className={classes.statusCell}>
|
||||
<AuditStatusIcon audit={website.lastAudit} />{' '}
|
||||
<span className={classes.status}>
|
||||
{website.lastAudit.status.toLowerCase()}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>{formatTime(website.lastAudit.timeCreated)}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuditRow;
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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, { FC, useState } from 'react';
|
||||
import { useInterval } from 'react-use';
|
||||
import { Website, lighthouseApiRef } from '../../api';
|
||||
import { useApi } from '@backstage/core';
|
||||
import {
|
||||
SparklinesDataByCategory,
|
||||
buildSparklinesDataForItem,
|
||||
} from '../../utils';
|
||||
import { AuditRow } from './AuditRow';
|
||||
export const LIMIT = 10;
|
||||
|
||||
export const Audit: FC<{
|
||||
website: Website;
|
||||
categorySparkline: SparklinesDataByCategory;
|
||||
}> = ({ website, categorySparkline }) => {
|
||||
const lighthouseApi = useApi(lighthouseApiRef);
|
||||
const [websiteState, setWebsiteState] = useState(website);
|
||||
const [sparklineState, setSparklineState] = useState(categorySparkline);
|
||||
|
||||
const runRefresh = async () => {
|
||||
const response = await lighthouseApi.getWebsiteForAuditId(
|
||||
websiteState.lastAudit.id,
|
||||
);
|
||||
const auditStatus = response.lastAudit.status;
|
||||
if (auditStatus === 'COMPLETED' || auditStatus === 'FAILED') {
|
||||
setSparklineState(buildSparklinesDataForItem(response));
|
||||
setWebsiteState(response);
|
||||
}
|
||||
};
|
||||
|
||||
useInterval(
|
||||
runRefresh,
|
||||
websiteState?.lastAudit.status === 'RUNNING' ? 5000 : null,
|
||||
);
|
||||
|
||||
return <AuditRow website={websiteState} categorySparkline={sparklineState} />;
|
||||
};
|
||||
|
||||
export default Audit;
|
||||
@@ -55,7 +55,7 @@ describe('AuditListTable', () => {
|
||||
);
|
||||
const link = rendered.queryByText('https://anchor.fm');
|
||||
const website = websiteListResponse.items.find(
|
||||
w => w.url === 'https://anchor.fm',
|
||||
(w) => w.url === 'https://anchor.fm',
|
||||
);
|
||||
if (!website)
|
||||
throw new Error('https://anchor.fm must be present in fixture');
|
||||
@@ -71,7 +71,7 @@ describe('AuditListTable', () => {
|
||||
wrapInThemedTestApp(auditList(websiteListResponse)),
|
||||
);
|
||||
const website = websiteListResponse.items.find(
|
||||
w => w.url === 'https://anchor.fm',
|
||||
(w) => w.url === 'https://anchor.fm',
|
||||
);
|
||||
if (!website)
|
||||
throw new Error('https://anchor.fm must be present in fixture');
|
||||
@@ -85,21 +85,22 @@ describe('AuditListTable', () => {
|
||||
wrapInThemedTestApp(auditList(websiteListResponse)),
|
||||
);
|
||||
|
||||
const completed = await rendered.findAllByText('completed');
|
||||
const completed = await rendered.findAllByText('COMPLETED');
|
||||
expect(completed).toHaveLength(
|
||||
websiteListResponse.items.filter(w => w.lastAudit.status === 'COMPLETED')
|
||||
.length,
|
||||
websiteListResponse.items.filter(
|
||||
(w) => w.lastAudit.status === 'COMPLETED',
|
||||
).length,
|
||||
);
|
||||
|
||||
const failed = await rendered.findAllByText('failed');
|
||||
const failed = await rendered.findAllByText('FAILED');
|
||||
expect(failed).toHaveLength(
|
||||
websiteListResponse.items.filter(w => w.lastAudit.status === 'FAILED')
|
||||
websiteListResponse.items.filter((w) => w.lastAudit.status === 'FAILED')
|
||||
.length,
|
||||
);
|
||||
|
||||
const running = await rendered.findAllByText('failed');
|
||||
const running = await rendered.findAllByText('FAILED');
|
||||
expect(running).toHaveLength(
|
||||
websiteListResponse.items.filter(w => w.lastAudit.status === 'RUNNING')
|
||||
websiteListResponse.items.filter((w) => w.lastAudit.status === 'RUNNING')
|
||||
.length,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -13,81 +13,111 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from '@material-ui/core';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { Website } from '../../api';
|
||||
import React, { FC, useState } from 'react';
|
||||
import { Table, TableColumn, TrendLine, useApi } from '@backstage/core';
|
||||
import { Website, lighthouseApiRef } from '../../api';
|
||||
import { useInterval } from 'react-use';
|
||||
import {
|
||||
formatTime,
|
||||
CATEGORIES,
|
||||
CATEGORY_LABELS,
|
||||
SparklinesDataByCategory,
|
||||
buildSparklinesDataForItem,
|
||||
} from '../../utils';
|
||||
import Audit from '../Audit';
|
||||
import { Link } from '@material-ui/core';
|
||||
import AuditStatusIcon from '../AuditStatusIcon';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
table: {
|
||||
minWidth: 650,
|
||||
const columns: TableColumn[] = [
|
||||
{
|
||||
title: 'Website URL',
|
||||
field: 'websiteUrl',
|
||||
},
|
||||
status: {
|
||||
textTransform: 'capitalize',
|
||||
...CATEGORIES.map((category) => ({
|
||||
title: CATEGORY_LABELS[category],
|
||||
field: category,
|
||||
})),
|
||||
{
|
||||
title: 'Last Report',
|
||||
field: 'lastReport',
|
||||
cellStyle: {
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
},
|
||||
link: {
|
||||
paddingTop: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(2),
|
||||
display: 'inline-block',
|
||||
{
|
||||
title: 'Last Audit Triggered',
|
||||
field: 'lastAuditTriggered',
|
||||
cellStyle: {
|
||||
minWidth: 120,
|
||||
},
|
||||
},
|
||||
statusCell: { whiteSpace: 'nowrap' },
|
||||
sparklinesCell: { minWidth: 120 },
|
||||
}));
|
||||
];
|
||||
|
||||
export const AuditListTable: FC<{ items: Website[] }> = ({ items }) => {
|
||||
const classes = useStyles();
|
||||
const categorySparklines: Record<string, SparklinesDataByCategory> = useMemo(
|
||||
() =>
|
||||
items.reduce(
|
||||
(res, item) => ({
|
||||
...res,
|
||||
[item.url]: buildSparklinesDataForItem(item),
|
||||
}),
|
||||
{},
|
||||
),
|
||||
[items],
|
||||
const [websiteState, setWebsiteState] = useState(items);
|
||||
const lighthouseApi = useApi(lighthouseApiRef);
|
||||
|
||||
const runRefresh = (websites: Website[]) => {
|
||||
websites.forEach(async (website) => {
|
||||
const response = await lighthouseApi.getWebsiteForAuditId(
|
||||
website.lastAudit.id,
|
||||
);
|
||||
const auditStatus = response.lastAudit.status;
|
||||
if (auditStatus === 'COMPLETED' || auditStatus === 'FAILED') {
|
||||
const newWebsiteData = websiteState.slice(0);
|
||||
newWebsiteData[
|
||||
newWebsiteData.findIndex((w) => w.url === response.url)
|
||||
] = response;
|
||||
setWebsiteState(newWebsiteData);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const runningWebsiteAudits = websiteState
|
||||
? websiteState.filter((website) => website.lastAudit.status === 'RUNNING')
|
||||
: [];
|
||||
|
||||
useInterval(
|
||||
() => runRefresh(runningWebsiteAudits),
|
||||
runningWebsiteAudits.length > 0 ? 5000 : null,
|
||||
);
|
||||
|
||||
const data = websiteState.map((website) => {
|
||||
const trendlineData = buildSparklinesDataForItem(website);
|
||||
const trendlines: any = {};
|
||||
CATEGORIES.forEach((category) => {
|
||||
trendlines[category] = (
|
||||
<TrendLine
|
||||
title={`trendline for ${CATEGORY_LABELS[category]} category of ${website.url}`}
|
||||
data={trendlineData[category] || []}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
websiteUrl: (
|
||||
<Link href={`/lighthouse/audit/${website.lastAudit.id}`}>
|
||||
{website.url}
|
||||
</Link>
|
||||
),
|
||||
...trendlines,
|
||||
lastReport: (
|
||||
<>
|
||||
<AuditStatusIcon audit={website.lastAudit} />{' '}
|
||||
<span>{website.lastAudit.status.toUpperCase()}</span>
|
||||
</>
|
||||
),
|
||||
lastAuditTriggered: formatTime(website.lastAudit.timeCreated),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table className={classes.table} size="small" aria-label="a dense table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Website URL</TableCell>
|
||||
{CATEGORIES.map(category => (
|
||||
<TableCell key={`${category}-label`}>
|
||||
{CATEGORY_LABELS[category]}
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell>Last Report</TableCell>
|
||||
<TableCell>Last Audit Triggered</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{items.map(website => (
|
||||
<Audit
|
||||
key={website.url}
|
||||
website={website}
|
||||
categorySparkline={categorySparklines[website.url]}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Table
|
||||
options={{
|
||||
paging: false,
|
||||
toolbar: false,
|
||||
}}
|
||||
columns={columns}
|
||||
data={data}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user