Use useInterval() hook to refetch audit status in lighthouse plugin WIP (#623)

* Fix error when using yarn lint

* Update logic for create app

* Add eslint-react-hooks

* Move common utils to utils.ts

* Add AuditListRow that refetches the audit

* Fixed Audit state using a react component

* Change AuditListRow to AuditRow

* Fixed some eslint-react-hooks warnings

* Change AuditListRow to AuditRow

* Merge

* Fixes tests in AuditListTable.test.tsx

* Refactored based on freben's review

* Remove eslint-plugin-react-hooks
This commit is contained in:
Jose Balanza Martinez
2020-04-27 09:59:52 -05:00
committed by GitHub
parent 3464ac7536
commit ea880db04a
8 changed files with 216 additions and 87 deletions
+1 -1
View File
@@ -31,8 +31,8 @@
"@material-ui/lab": "4.0.0-alpha.45",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router-dom": "^5.1.2",
"react-markdown": "^4.3.1",
"react-router-dom": "^5.1.2",
"react-use": "^13.24.0"
},
"files": [
@@ -0,0 +1,86 @@
/*
* 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;
@@ -0,0 +1,54 @@
/*
* 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;
@@ -17,19 +17,41 @@
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInThemedTestApp } from '@backstage/test-utils';
import { ApiRegistry, ApiProvider } from '@backstage/core';
import AuditListTable from './AuditListTable';
import { WebsiteListResponse } from '../../api';
import {
WebsiteListResponse,
lighthouseApiRef,
LighthouseRestApi,
} from '../../api';
import { formatTime } from '../../utils';
import mockFetch from 'jest-fetch-mock';
import * as data from '../../__fixtures__/website-list-response.json';
const websiteListResponse = data as WebsiteListResponse;
describe('AuditListTable', () => {
let apis: ApiRegistry;
beforeEach(() => {
apis = ApiRegistry.from([
[lighthouseApiRef, new LighthouseRestApi('http://lighthouse')],
]);
mockFetch.mockResponse(JSON.stringify(websiteListResponse));
});
const auditList = (websiteList: WebsiteListResponse) => {
return (
<ApiProvider apis={apis}>
<AuditListTable items={websiteList.items} />
</ApiProvider>
);
};
it('renders the link to each website', () => {
const rendered = render(
wrapInThemedTestApp(<AuditListTable items={websiteListResponse.items} />),
wrapInThemedTestApp(auditList(websiteListResponse)),
);
const link = rendered.queryByText('https://anchor.fm');
const website = websiteListResponse.items.find(
@@ -46,7 +68,7 @@ describe('AuditListTable', () => {
it('renders the dates that are available for a given row', () => {
const rendered = render(
wrapInThemedTestApp(<AuditListTable items={websiteListResponse.items} />),
wrapInThemedTestApp(auditList(websiteListResponse)),
);
const website = websiteListResponse.items.find(
w => w.url === 'https://anchor.fm',
@@ -60,7 +82,7 @@ describe('AuditListTable', () => {
it('renders the status for a given row', async () => {
const rendered = render(
wrapInThemedTestApp(<AuditListTable items={websiteListResponse.items} />),
wrapInThemedTestApp(auditList(websiteListResponse)),
);
const completed = await rendered.findAllByText('completed');
@@ -85,9 +107,7 @@ describe('AuditListTable', () => {
describe('sparklines', () => {
it('correctly maps the data from the website payload', () => {
const rendered = render(
wrapInThemedTestApp(
<AuditListTable items={websiteListResponse.items} />,
),
wrapInThemedTestApp(auditList(websiteListResponse)),
);
const backstageSEO = rendered.getByTitle(
'trendline for SEO category of https://backstage.io',
@@ -97,9 +117,7 @@ describe('AuditListTable', () => {
it('does not break when no data is available', () => {
const rendered = render(
wrapInThemedTestApp(
<AuditListTable items={websiteListResponse.items} />,
),
wrapInThemedTestApp(auditList(websiteListResponse)),
);
const anchorSEO = rendered.queryByTitle(
'trendline for SEO category of https://anchor.fm',
@@ -15,7 +15,6 @@
*/
import React, { FC, useMemo } from 'react';
import {
Link,
Table,
TableBody,
TableCell,
@@ -24,31 +23,11 @@ import {
TableRow,
} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import { TrendLine } from '@backstage/core';
import {
Audit,
AuditCompleted,
LighthouseCategoryId,
Website,
} from '../../api';
import { formatTime } from '../../utils';
import AuditStatusIcon from '../AuditStatusIcon';
export const CATEGORIES: LighthouseCategoryId[] = [
'accessibility',
'performance',
'seo',
'best-practices',
];
export const CATEGORY_LABELS: Record<LighthouseCategoryId, string> = {
accessibility: 'Accessibility',
performance: 'Performance',
seo: 'SEO',
'best-practices': 'Best Practices',
pwa: 'Progressive Web App',
};
import { CATEGORIES, CATEGORY_LABELS, SparklinesDataByCategory, buildSparklinesDataForItem } from '../../utils';
import Audit from '../Audit'
const useStyles = makeStyles(theme => ({
table: {
@@ -66,34 +45,13 @@ const useStyles = makeStyles(theme => ({
sparklinesCell: { minWidth: 120 },
}));
type SparklinesDataByCategory = Record<LighthouseCategoryId, number[]>;
function buildSparklinesDataForItem(item: Website): SparklinesDataByCategory {
return item.audits
.filter(
(audit: Audit): audit is AuditCompleted => audit.status === 'COMPLETED',
)
.reduce((scores, audit) => {
Object.values(audit.categories).forEach(category => {
scores[category.id] = scores[category.id] || [];
scores[category.id].unshift(category.score);
});
// edge case: if only one audit exists, force a "flat" sparkline
Object.values(scores).forEach(arr => {
if (arr.length === 1) arr.push(arr[0]);
});
return scores;
}, {} as SparklinesDataByCategory);
}
export const AuditListTable: FC<{ items: Website[] }> = ({ items }) => {
const classes = useStyles();
const categorySparklines: Record<string, SparklinesDataByCategory> = useMemo(
() =>
items.reduce(
(res, item) => ({
...res,
...res,
[item.url]: buildSparklinesDataForItem(item),
}),
{},
@@ -118,34 +76,11 @@ export const AuditListTable: FC<{ items: Website[] }> = ({ items }) => {
</TableHead>
<TableBody>
{items.map(website => (
<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={categorySparklines[website.url][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>
<Audit
key={website.url}
website={website}
categorySparkline={categorySparklines[website.url]}
/>
))}
</TableBody>
</Table>
@@ -63,7 +63,7 @@ const AuditList: FC<{}> = () => {
if (value?.total && value?.limit)
return Math.ceil(value?.total / value?.limit);
return 0;
}, [value?.total, value?.limit]);
}, [value]);
const history = useHistory();
@@ -84,7 +84,7 @@ const CreateAudit: FC<{}> = () => {
} finally {
setSubmitting(false);
}
}, [url, emulatedFormFactor, lighthouseApi, setSubmitting]);
}, [url, emulatedFormFactor, lighthouseApi, setSubmitting, errorApi, history]);
return (
<Page theme={pageTheme.tool}>
+37 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { useLocation } from 'react-router-dom';
import {Website, Audit, LighthouseCategoryId, AuditCompleted} from './api'
export function useQuery(): URLSearchParams {
return new URLSearchParams(useLocation().search);
}
@@ -28,3 +28,39 @@ export function formatTime(timestamp: string | Date) {
}
return date.toUTCString();
}
export const CATEGORIES: LighthouseCategoryId[] = [
'accessibility',
'performance',
'seo',
'best-practices',
];
export const CATEGORY_LABELS: Record<LighthouseCategoryId, string> = {
accessibility: 'Accessibility',
performance: 'Performance',
seo: 'SEO',
'best-practices': 'Best Practices',
pwa: 'Progressive Web App',
};
export type SparklinesDataByCategory = Record<LighthouseCategoryId, number[]>;
export function buildSparklinesDataForItem(item: Website): SparklinesDataByCategory {
return item.audits
.filter(
(audit: Audit): audit is AuditCompleted => audit.status === 'COMPLETED',
)
.reduce((scores, audit) => {
Object.values(audit.categories).forEach(category => {
scores[category.id] = scores[category.id] || [];
scores[category.id].unshift(category.score);
});
// edge case: if only one audit exists, force a "flat" sparkline
Object.values(scores).forEach(arr => {
if (arr.length === 1) arr.push(arr[0]);
});
return scores;
}, {} as SparklinesDataByCategory);
}