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:
Sebastian Qvarfordt
2020-05-06 12:58:34 +02:00
committed by GitHub
parent 5b0e808204
commit 425cf93c08
12 changed files with 161 additions and 676 deletions
@@ -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>
@@ -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>
);
+2 -1
View File
@@ -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}
/>
);
};