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:
committed by
GitHub
parent
3464ac7536
commit
ea880db04a
@@ -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}>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user