feat: implement Lighthouse plugin using lighthouse-audit-service
This commit is contained in:
@@ -5,9 +5,10 @@
|
||||
"dependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.3",
|
||||
"@backstage/core": "^0.1.1-alpha.3",
|
||||
"@backstage/plugin-home-page": "^0.1.1-alpha.3",
|
||||
"@backstage/plugin-welcome": "^0.1.1-alpha.3",
|
||||
"@backstage/theme": "^0.1.1-alpha.3",
|
||||
"@backstage/plugin-home-page": "^0.1.1-alpha.3",
|
||||
"@backstage/plugin-lighthouse": "^0.1.1-alpha.3",
|
||||
"@backstage/plugin-welcome": "^0.1.1-alpha.3",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
|
||||
@@ -21,13 +21,21 @@ import {
|
||||
featureFlagsApiRef,
|
||||
FeatureFlags,
|
||||
} from '@backstage/core';
|
||||
import {
|
||||
lighthouseApiRef,
|
||||
LighthouseRestApi,
|
||||
} from '@backstage/plugin-lighthouse';
|
||||
|
||||
import { ErrorDisplayForwarder } from './components/ErrorDisplay/ErrorDisplay';
|
||||
|
||||
const builder = ApiRegistry.builder();
|
||||
|
||||
export const errorDialogForwarder = new ErrorDisplayForwarder();
|
||||
|
||||
builder.add(errorApiRef, errorDialogForwarder);
|
||||
|
||||
builder.add(featureFlagsApiRef, new FeatureFlags());
|
||||
|
||||
builder.add(lighthouseApiRef, new LighthouseRestApi('http://localhost:3003'));
|
||||
|
||||
export default builder.build() as ApiHolder;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { default as HomePagePlugin } from '@backstage/plugin-home-page';
|
||||
import { default as WelcomePlugin } from '@backstage/plugin-welcome';
|
||||
export { HomePagePlugin, WelcomePlugin };
|
||||
import { default as LighthousePlugin } from '@backstage/plugin-lighthouse';
|
||||
export { HomePagePlugin, WelcomePlugin, LighthousePlugin };
|
||||
|
||||
@@ -39,7 +39,7 @@ export default {
|
||||
}),
|
||||
commonjs({
|
||||
include: ['node_modules/**', '../../node_modules/**'],
|
||||
exclude: ['**/*.stories.js'],
|
||||
exclude: ['**/*.stories.*', '**/*.test.*'],
|
||||
}),
|
||||
postcss(),
|
||||
imageFiles(),
|
||||
|
||||
@@ -50,6 +50,6 @@
|
||||
"react-router": "^5.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@backstage/theme": "^0.1.1-alpha.2"
|
||||
"@backstage/theme": "^0.1.1-alpha.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,5 +17,6 @@
|
||||
export { default as ApiProvider, useApi } from './ApiProvider';
|
||||
export { default as ApiRegistry } from './ApiRegistry';
|
||||
export { default as ApiTestRegistry } from './ApiTestRegistry';
|
||||
export { default as ApiRef } from './ApiRef';
|
||||
export * from './types';
|
||||
export * from './definitions';
|
||||
|
||||
@@ -32,3 +32,4 @@ export { AlphaLabel, BetaLabel } from './components/Lifecycle';
|
||||
export { default as SupportButton } from './components/SupportButton';
|
||||
export { default as SortableTable } from './components/SortableTable';
|
||||
export { FeatureCalloutCircular } from './components/FeatureDiscovery/FeatureCalloutCircular';
|
||||
export * from './components/Status';
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Theme, makeStyles } from '@material-ui/core';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
@@ -25,12 +26,27 @@ const useStyles = makeStyles((theme: Theme) => ({
|
||||
paddingBottom: theme.spacing(3),
|
||||
...theme.mixins.gutters({}),
|
||||
},
|
||||
stretch: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
const Content: FC<{}> = ({ children, ...props }) => {
|
||||
const Content: FC<{ stretch?: boolean; className?: string }> = ({
|
||||
children,
|
||||
className,
|
||||
stretch,
|
||||
...props
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<article {...props} className={classes.root}>
|
||||
<article
|
||||
{...props}
|
||||
className={classNames(classes.root, className, {
|
||||
[classes.stretch]: stretch,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</article>
|
||||
);
|
||||
|
||||
@@ -27,7 +27,7 @@ const useStyles = makeStyles(() => ({
|
||||
"'pageHeader pageHeader pageHeader' 'pageSubheader pageSubheader pageSubheader' 'pageNav pageContent pageSidebar'",
|
||||
gridTemplateRows: 'auto auto 1fr',
|
||||
gridTemplateColumns: 'auto 1fr auto',
|
||||
minHeight: '100%',
|
||||
minHeight: '100vh',
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"react-router-dom": "^5.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@backstage/theme": "^0.1.1-alpha.2",
|
||||
"@backstage/theme": "^0.1.1-alpha.3",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
"react-dom": "^16.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.2",
|
||||
"@backstage/theme": "^0.1.1-alpha.2",
|
||||
"@backstage/core": "^0.1.1-alpha.3",
|
||||
"@backstage/theme": "^0.1.1-alpha.3",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# @backstage/plugin-lighthouse
|
||||
|
||||
A frontend for [lighthouse-audit-service](https://github.com/spotify/lighthouse-audit-service), this plugin allows you to trigger Lighthouse audits on websites and track them over time.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Use cases
|
||||
|
||||
Google's [Lighthouse](https://developers.google.com/web/tools/lighthouse) auditing tool for websites
|
||||
is a great open-source resource forbenchmarking and improving the accessibility, performance, SEO, and best practices of your site.
|
||||
At Spotify, we keep track of Lighthouse audit scores over time to look at trends and overall areas for investment.
|
||||
|
||||
This plugin allows you to generate on-demand Lighthouse audits for websites, and to track the trends for the
|
||||
top-level categories of Lighthouse at a glance.
|
||||
|
||||
In the future, we hope to add support for scheduling audits (which we do internally), as well as allowing
|
||||
custom runs of Lighthouse to be ingested (for auditing sites that require authentication or some session state).
|
||||
|
||||
### Installation
|
||||
|
||||
To get started, you will need a running instance of [lighthouse-audit-service](https://github.com/spotify/lighthouse-audit-service).
|
||||
_It's likely you will need to enable CORS when running lighthouse-audit-service. Initialize the app
|
||||
with the environment variable `LAS_CORS` set to `true`._
|
||||
|
||||
When you have an instance running that Backstage can hook into, make sure to export the plugin in
|
||||
your app's [`plugins.ts`](https://github.com/spotify/backstage/blob/master/packages/app/src/plugins.ts)
|
||||
to enable the plugin:
|
||||
|
||||
```js
|
||||
export { default as LighthousePlugin } from '@backstage/plugin-lighthouse';
|
||||
```
|
||||
|
||||
Then, you need to use the `lighthouseApiRef` exported from the plugin to initialize the Rest API in
|
||||
your [`apis.ts`](https://github.com/spotify/backstage/blob/master/packages/app/src/apis.ts).
|
||||
|
||||
```js
|
||||
import { ApiHolder, ApiRegistry } from '@backstage/core';
|
||||
import {
|
||||
lighthouseApiRef,
|
||||
LighthouseRestApi,
|
||||
} from '@backstage/lighthouse-audits';
|
||||
|
||||
const builder = ApiRegistry.builder();
|
||||
|
||||
export const lighthouseApi =
|
||||
new LighthouseRestApi(/* your service url here! */);
|
||||
builder.add(lighthouseApiRef, lighthouseApi);
|
||||
|
||||
export default builder.build() as ApiHolder;
|
||||
```
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "@backstage/plugin-lighthouse",
|
||||
"version": "0.1.1-alpha.3",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:watch": "backstage-cli plugin:build --watch",
|
||||
"build": "backstage-cli build-cache -- backstage-cli plugin:build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-markdown": "^4.3.1",
|
||||
"react-sparklines": "^1.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.3",
|
||||
"@backstage/core": "^0.1.1-alpha.3",
|
||||
"@backstage/test-utils": "^0.1.1-alpha.3",
|
||||
"@backstage/theme": "^0.1.1-alpha.3",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react-sparklines": "^1.7.0",
|
||||
"@types/testing-library__jest-dom": "5.0.2",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-use": "^13.24.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.3",
|
||||
"@backstage/theme": "^0.1.1-alpha.3",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-use": "^13.24.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"id": "80d92fff-52ec-4ef5-848f-9cec8b383e41",
|
||||
"url": "https://anchor.fm",
|
||||
"timeCreated": "2020-03-31T15:34:02.199-04:00",
|
||||
"status": "RUNNING"
|
||||
}
|
||||
@@ -0,0 +1,579 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"url": "https://anchor.fm",
|
||||
"audits": [
|
||||
{
|
||||
"id": "80d92fff-52ec-4ef5-848f-9cec8b383e41",
|
||||
"url": "https://anchor.fm",
|
||||
"timeCreated": "2020-03-31T15:34:02.199-04:00",
|
||||
"status": "RUNNING"
|
||||
}
|
||||
],
|
||||
"lastAudit": {
|
||||
"id": "80d92fff-52ec-4ef5-848f-9cec8b383e41",
|
||||
"url": "https://anchor.fm",
|
||||
"timeCreated": "2020-03-31T15:34:02.199-04:00",
|
||||
"status": "RUNNING"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "https://artists.spotify.com/",
|
||||
"audits": [
|
||||
{
|
||||
"id": "d6389155-6a3c-43d4-8032-f9ae98f663a4",
|
||||
"url": "https://artists.spotify.com/",
|
||||
"timeCreated": "2020-03-31T15:31:10.769-04:00",
|
||||
"timeCompleted": "2020-03-31T15:31:17.311-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": {
|
||||
"id": "pwa",
|
||||
"title": "Progressive Web App",
|
||||
"score": 0.56
|
||||
},
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 0.91 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.62
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.83
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 0.93
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"lastAudit": {
|
||||
"id": "d6389155-6a3c-43d4-8032-f9ae98f663a4",
|
||||
"url": "https://artists.spotify.com/",
|
||||
"timeCreated": "2020-03-31T15:31:10.769-04:00",
|
||||
"timeCompleted": "2020-03-31T15:31:17.311-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": { "id": "pwa", "title": "Progressive Web App", "score": 0.56 },
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 0.91 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.62
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.83
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 0.93
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "https://spotify.com",
|
||||
"audits": [
|
||||
{
|
||||
"id": "f471916c-80bc-4a22-93de-f3347835e785",
|
||||
"url": "https://spotify.com",
|
||||
"timeCreated": "2020-03-31T15:29:25.68-04:00",
|
||||
"timeCompleted": "2020-03-31T15:29:33.085-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": {
|
||||
"id": "pwa",
|
||||
"title": "Progressive Web App",
|
||||
"score": 0.33
|
||||
},
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 0.9 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.43
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.82
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 0.86
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "047f506b-15f8-4eb1-b177-d8e3d0377b85",
|
||||
"url": "https://spotify.com",
|
||||
"timeCreated": "2020-03-27T12:58:34.605-04:00",
|
||||
"timeCompleted": "2020-03-27T12:58:43.842-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": {
|
||||
"id": "pwa",
|
||||
"title": "Progressive Web App",
|
||||
"score": 0.33
|
||||
},
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 0.92 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.44
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.82
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 0.86
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2e7810fd-fe42-4620-8787-b047b7f6c57a",
|
||||
"url": "https://spotify.com",
|
||||
"timeCreated": "2020-03-26T14:09:59.146-04:00",
|
||||
"timeCompleted": "2020-03-26T14:10:05.356-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": {
|
||||
"id": "pwa",
|
||||
"title": "Progressive Web App",
|
||||
"score": 0.33
|
||||
},
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 0.92 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.47
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.82
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 0.86
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "0f094aa2-12c6-479e-bb52-fad2b089d5c1",
|
||||
"url": "https://spotify.com",
|
||||
"timeCreated": "2020-03-26T14:02:03.023-04:00",
|
||||
"timeCompleted": "2020-03-26T14:02:33.336-04:00",
|
||||
"status": "FAILED"
|
||||
},
|
||||
{
|
||||
"id": "628d7eb6-e26a-4d1a-9690-adc3c6af1baa",
|
||||
"url": "https://spotify.com",
|
||||
"timeCreated": "2020-03-26T14:00:43.878-04:00",
|
||||
"timeCompleted": "2020-03-26T14:01:14.203-04:00",
|
||||
"status": "FAILED"
|
||||
},
|
||||
{
|
||||
"id": "71b8e012-88fc-49d8-9d88-3b03ef9ec34e",
|
||||
"url": "https://spotify.com",
|
||||
"timeCreated": "2020-03-26T13:59:51.243-04:00",
|
||||
"timeCompleted": "2020-03-26T13:59:51.595-04:00",
|
||||
"status": "FAILED"
|
||||
}
|
||||
],
|
||||
"lastAudit": {
|
||||
"id": "f471916c-80bc-4a22-93de-f3347835e785",
|
||||
"url": "https://spotify.com",
|
||||
"timeCreated": "2020-03-31T15:29:25.68-04:00",
|
||||
"timeCompleted": "2020-03-31T15:29:33.085-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": { "id": "pwa", "title": "Progressive Web App", "score": 0.33 },
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 0.9 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.43
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.82
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 0.86
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "https://www.soundtrap.com/",
|
||||
"audits": [
|
||||
{
|
||||
"id": "3fba085f-651e-4e09-a368-70998d6594d4",
|
||||
"url": "https://www.soundtrap.com/",
|
||||
"timeCreated": "2020-03-31T15:19:48.711-04:00",
|
||||
"timeCompleted": "2020-03-31T15:19:59.422-04:00",
|
||||
"status": "FAILED"
|
||||
}
|
||||
],
|
||||
"lastAudit": {
|
||||
"id": "3fba085f-651e-4e09-a368-70998d6594d4",
|
||||
"url": "https://www.soundtrap.com/",
|
||||
"timeCreated": "2020-03-31T15:19:48.711-04:00",
|
||||
"timeCompleted": "2020-03-31T15:19:59.422-04:00",
|
||||
"status": "FAILED"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "https://backstage.io",
|
||||
"audits": [
|
||||
{
|
||||
"id": "51211f04-848a-4c54-a891-ce747502958c",
|
||||
"url": "https://backstage.io",
|
||||
"timeCreated": "2020-03-30T18:23:24.734-04:00",
|
||||
"timeCompleted": "2020-03-30T18:23:30.533-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": {
|
||||
"id": "pwa",
|
||||
"title": "Progressive Web App",
|
||||
"score": 0.56
|
||||
},
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 1 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.98
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.81
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8a6b32f8-930e-4750-a699-7046a51e7b8e",
|
||||
"url": "https://backstage.io",
|
||||
"timeCreated": "2020-03-30T09:34:28.25-04:00",
|
||||
"timeCompleted": "2020-03-30T09:34:33.275-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": {
|
||||
"id": "pwa",
|
||||
"title": "Progressive Web App",
|
||||
"score": 0.54
|
||||
},
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 1 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.99
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.81
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "259bf4c9-57ad-479d-8072-06da8956fdf4",
|
||||
"url": "https://backstage.io",
|
||||
"timeCreated": "2020-03-27T16:10:13.512-04:00",
|
||||
"timeCompleted": "2020-03-27T16:10:17.998-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": {
|
||||
"id": "pwa",
|
||||
"title": "Progressive Web App",
|
||||
"score": 0.54
|
||||
},
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 1 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.99
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.81
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "0c2ce744-019a-42f2-8c61-9b49478a2ecd",
|
||||
"url": "https://backstage.io",
|
||||
"timeCreated": "2020-03-27T13:01:21.399-04:00",
|
||||
"timeCompleted": "2020-03-27T13:01:25.761-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": {
|
||||
"id": "pwa",
|
||||
"title": "Progressive Web App",
|
||||
"score": 0.56
|
||||
},
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 1 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.99
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.81
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"lastAudit": {
|
||||
"id": "51211f04-848a-4c54-a891-ce747502958c",
|
||||
"url": "https://backstage.io",
|
||||
"timeCreated": "2020-03-30T18:23:24.734-04:00",
|
||||
"timeCompleted": "2020-03-30T18:23:30.533-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": { "id": "pwa", "title": "Progressive Web App", "score": 0.56 },
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 1 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.98
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.81
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "https://adstudio.spotify.com",
|
||||
"audits": [
|
||||
{
|
||||
"id": "5c89fad2-96ed-4728-856c-f9fbcfb8244e",
|
||||
"url": "https://adstudio.spotify.com",
|
||||
"timeCreated": "2020-03-27T15:29:00.859-04:00",
|
||||
"timeCompleted": "2020-03-27T15:29:11.014-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": {
|
||||
"id": "pwa",
|
||||
"title": "Progressive Web App",
|
||||
"score": 0.73
|
||||
},
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 1 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.56
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.84
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "7b68a014-5e23-462c-952c-dae575d9e10c",
|
||||
"url": "https://adstudio.spotify.com",
|
||||
"timeCreated": "2020-03-27T15:28:37.319-04:00",
|
||||
"timeCompleted": "2020-03-27T15:28:47.727-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": {
|
||||
"id": "pwa",
|
||||
"title": "Progressive Web App",
|
||||
"score": 0.73
|
||||
},
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 1 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.55
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.84
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "275ae2d4-4409-45ae-b1dd-91121478cc3c",
|
||||
"url": "https://adstudio.spotify.com",
|
||||
"timeCreated": "2020-03-27T12:24:19.422-04:00",
|
||||
"timeCompleted": "2020-03-27T12:24:30.299-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": {
|
||||
"id": "pwa",
|
||||
"title": "Progressive Web App",
|
||||
"score": 0.74
|
||||
},
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 0.98 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.51
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.84
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"lastAudit": {
|
||||
"id": "5c89fad2-96ed-4728-856c-f9fbcfb8244e",
|
||||
"url": "https://adstudio.spotify.com",
|
||||
"timeCreated": "2020-03-27T15:29:00.859-04:00",
|
||||
"timeCompleted": "2020-03-27T15:29:11.014-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": { "id": "pwa", "title": "Progressive Web App", "score": 0.73 },
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 1 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.56
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.84
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "https://spotify.com/us",
|
||||
"audits": [
|
||||
{
|
||||
"id": "cac7f052-361f-49ff-9112-c780dd720416",
|
||||
"url": "https://spotify.com/us",
|
||||
"timeCreated": "2020-03-27T12:17:41.711-04:00",
|
||||
"timeCompleted": "2020-03-27T12:17:48.85-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": {
|
||||
"id": "pwa",
|
||||
"title": "Progressive Web App",
|
||||
"score": 0.33
|
||||
},
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 0.92 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.46
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.82
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 0.86
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"lastAudit": {
|
||||
"id": "cac7f052-361f-49ff-9112-c780dd720416",
|
||||
"url": "https://spotify.com/us",
|
||||
"timeCreated": "2020-03-27T12:17:41.711-04:00",
|
||||
"timeCompleted": "2020-03-27T12:17:48.85-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": { "id": "pwa", "title": "Progressive Web App", "score": 0.33 },
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 0.92 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.46
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.82
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 0.86
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"total": 7,
|
||||
"limit": 10,
|
||||
"offset": 0
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"url": "https://spotify.com",
|
||||
"audits": [
|
||||
{
|
||||
"id": "f471916c-80bc-4a22-93de-f3347835e785",
|
||||
"url": "https://spotify.com",
|
||||
"timeCreated": "2020-03-31T15:29:25.68-04:00",
|
||||
"status": "RUNNING"
|
||||
},
|
||||
{
|
||||
"id": "047f506b-15f8-4eb1-b177-d8e3d0377b85",
|
||||
"url": "https://spotify.com",
|
||||
"timeCreated": "2020-03-27T12:58:34.605-04:00",
|
||||
"timeCompleted": "2020-03-27T12:58:43.842-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": {
|
||||
"id": "pwa",
|
||||
"title": "Progressive Web App",
|
||||
"score": 0.33
|
||||
},
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 0.92 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.44
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.82
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 0.86
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2e7810fd-fe42-4620-8787-b047b7f6c57a",
|
||||
"url": "https://spotify.com",
|
||||
"timeCreated": "2020-03-26T14:09:59.146-04:00",
|
||||
"timeCompleted": "2020-03-26T14:10:05.356-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": {
|
||||
"id": "pwa",
|
||||
"title": "Progressive Web App",
|
||||
"score": 0.33
|
||||
},
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 0.92 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.47
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.82
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 0.86
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "0f094aa2-12c6-479e-bb52-fad2b089d5c1",
|
||||
"url": "https://spotify.com",
|
||||
"timeCreated": "2020-03-26T14:02:03.023-04:00",
|
||||
"timeCompleted": "2020-03-26T14:02:33.336-04:00",
|
||||
"status": "FAILED"
|
||||
},
|
||||
{
|
||||
"id": "628d7eb6-e26a-4d1a-9690-adc3c6af1baa",
|
||||
"url": "https://spotify.com",
|
||||
"timeCreated": "2020-03-26T14:00:43.878-04:00",
|
||||
"timeCompleted": "2020-03-26T14:01:14.203-04:00",
|
||||
"status": "FAILED"
|
||||
},
|
||||
{
|
||||
"id": "71b8e012-88fc-49d8-9d88-3b03ef9ec34e",
|
||||
"url": "https://spotify.com",
|
||||
"timeCreated": "2020-03-26T13:59:51.243-04:00",
|
||||
"timeCompleted": "2020-03-26T13:59:51.595-04:00",
|
||||
"status": "FAILED"
|
||||
}
|
||||
],
|
||||
"lastAudit": {
|
||||
"id": "f471916c-80bc-4a22-93de-f3347835e785",
|
||||
"url": "https://spotify.com",
|
||||
"timeCreated": "2020-03-31T15:29:25.68-04:00",
|
||||
"timeCompleted": "2020-03-31T15:29:33.085-04:00",
|
||||
"status": "COMPLETED",
|
||||
"categories": {
|
||||
"pwa": { "id": "pwa", "title": "Progressive Web App", "score": 0.33 },
|
||||
"seo": { "id": "seo", "title": "SEO", "score": 0.9 },
|
||||
"performance": {
|
||||
"id": "performance",
|
||||
"title": "Performance",
|
||||
"score": 0.43
|
||||
},
|
||||
"accessibility": {
|
||||
"id": "accessibility",
|
||||
"title": "Accessibility",
|
||||
"score": 0.82
|
||||
},
|
||||
"best-practices": {
|
||||
"id": "best-practices",
|
||||
"title": "Best Practices",
|
||||
"score": 0.86
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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 { ApiRef } from '@backstage/core';
|
||||
|
||||
export type LighthouseCategoryId =
|
||||
| 'pwa'
|
||||
| 'seo'
|
||||
| 'performance'
|
||||
| 'accessibility'
|
||||
| 'best-practices';
|
||||
|
||||
export interface LighthouseCategoryAbbr {
|
||||
id: LighthouseCategoryId;
|
||||
score: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface LASListRequest {
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface LASListResponse<Item> {
|
||||
items: Item[];
|
||||
total: number;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
interface AuditBase {
|
||||
id: string;
|
||||
url: string;
|
||||
timeCreated: string;
|
||||
}
|
||||
|
||||
export interface AuditRunning extends AuditBase {
|
||||
status: 'RUNNING';
|
||||
}
|
||||
|
||||
export interface AuditFailed extends AuditBase {
|
||||
status: 'FAILED';
|
||||
timeCompleted: string;
|
||||
}
|
||||
|
||||
export interface AuditCompleted extends AuditBase {
|
||||
status: 'COMPLETED';
|
||||
timeCompleted: string;
|
||||
report: Object;
|
||||
categories: Record<LighthouseCategoryId, LighthouseCategoryAbbr>;
|
||||
}
|
||||
|
||||
export type Audit = AuditRunning | AuditFailed | AuditCompleted;
|
||||
|
||||
export interface Website {
|
||||
url: string;
|
||||
audits: Audit[];
|
||||
lastAudit: Audit;
|
||||
}
|
||||
|
||||
export type WebsiteListResponse = LASListResponse<Website>;
|
||||
|
||||
export interface TriggerAuditPayload {
|
||||
url: string;
|
||||
options: {
|
||||
lighthouseConfig: {
|
||||
settings: {
|
||||
emulatedFormFactor: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export class FetchError extends Error {
|
||||
get name(): string {
|
||||
return this.constructor.name;
|
||||
}
|
||||
|
||||
static async forResponse(resp: Response): Promise<FetchError> {
|
||||
return new FetchError(
|
||||
`Request failed with status code ${
|
||||
resp.status
|
||||
}.\nReason: ${await resp.text()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type LighthouseApi = {
|
||||
url: string;
|
||||
getWebsiteList: (listOptions: LASListRequest) => Promise<WebsiteListResponse>;
|
||||
getWebsiteForAuditId: (auditId: string) => Promise<Website>;
|
||||
triggerAudit: (payload: TriggerAuditPayload) => Promise<Audit>;
|
||||
};
|
||||
|
||||
export const lighthouseApiRef = new ApiRef<LighthouseApi>({
|
||||
id: 'plugin.lighthouse.service',
|
||||
description: 'Used by the Lighthouse plugin to make requests',
|
||||
});
|
||||
|
||||
export class LighthouseRestApi implements LighthouseApi {
|
||||
constructor(public url: string) {}
|
||||
|
||||
private async fetch<T = any>(input: string, init?: RequestInit): Promise<T> {
|
||||
const resp = await fetch(`${this.url}${input}`, init);
|
||||
if (!resp.ok) throw await FetchError.forResponse(resp);
|
||||
return await resp.json();
|
||||
}
|
||||
|
||||
async getWebsiteList({ limit, offset }: LASListRequest = {}): Promise<
|
||||
WebsiteListResponse
|
||||
> {
|
||||
return await this.fetch<WebsiteListResponse>(
|
||||
`/v1/websites?limit=${limit}&offset=${offset}`,
|
||||
);
|
||||
}
|
||||
|
||||
async getWebsiteForAuditId(auditId: string): Promise<Website> {
|
||||
return await this.fetch<Website>(`/v1/audits/${auditId}/website`);
|
||||
}
|
||||
|
||||
async triggerAudit(payload: TriggerAuditPayload): Promise<Audit> {
|
||||
return await this.fetch<Audit>('/v1/audits', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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 fs from 'fs';
|
||||
import path from 'path';
|
||||
import { render } from '@testing-library/react';
|
||||
import { wrapInThemedTestApp } from '@backstage/test-utils';
|
||||
|
||||
import AuditListTable from './AuditListTable';
|
||||
import { WebsiteListResponse } from '../../api';
|
||||
import { formatTime } from '../../utils';
|
||||
|
||||
const websiteListResponseJson = fs
|
||||
.readFileSync(
|
||||
path.join(__dirname, '../../__fixtures__/website-list-response.json'),
|
||||
)
|
||||
.toString();
|
||||
|
||||
const websiteListResponse: WebsiteListResponse = JSON.parse(
|
||||
websiteListResponseJson,
|
||||
);
|
||||
|
||||
describe('AuditListTable', () => {
|
||||
it('renders the link to each website', () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(<AuditListTable items={websiteListResponse.items} />),
|
||||
);
|
||||
const link = rendered.queryByText('https://anchor.fm');
|
||||
const website = websiteListResponse.items.find(
|
||||
w => w.url === 'https://anchor.fm',
|
||||
);
|
||||
if (!website)
|
||||
throw new Error('https://anchor.fm must be present in fixture');
|
||||
expect(link).toBeInTheDocument();
|
||||
expect(link).toHaveAttribute(
|
||||
'href',
|
||||
`/lighthouse/audit/${website.lastAudit.id}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the dates that are available for a given row', () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(<AuditListTable items={websiteListResponse.items} />),
|
||||
);
|
||||
const website = websiteListResponse.items.find(
|
||||
w => w.url === 'https://anchor.fm',
|
||||
);
|
||||
if (!website)
|
||||
throw new Error('https://anchor.fm must be present in fixture');
|
||||
expect(
|
||||
rendered.queryByText(formatTime(website.lastAudit.timeCreated)),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the status for a given row', async () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(<AuditListTable items={websiteListResponse.items} />),
|
||||
);
|
||||
|
||||
const completed = await rendered.findAllByText('completed');
|
||||
expect(completed).toHaveLength(
|
||||
websiteListResponse.items.filter(w => w.lastAudit.status === 'COMPLETED')
|
||||
.length,
|
||||
);
|
||||
|
||||
const failed = await rendered.findAllByText('failed');
|
||||
expect(failed).toHaveLength(
|
||||
websiteListResponse.items.filter(w => w.lastAudit.status === 'FAILED')
|
||||
.length,
|
||||
);
|
||||
|
||||
const running = await rendered.findAllByText('failed');
|
||||
expect(running).toHaveLength(
|
||||
websiteListResponse.items.filter(w => w.lastAudit.status === 'RUNNING')
|
||||
.length,
|
||||
);
|
||||
});
|
||||
|
||||
describe('sparklines', () => {
|
||||
it('correctly maps the data from the website payload', () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<AuditListTable items={websiteListResponse.items} />,
|
||||
),
|
||||
);
|
||||
const backstageSEO = rendered.getByTitle(
|
||||
'trendline for SEO category of https://backstage.io',
|
||||
);
|
||||
expect(backstageSEO).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not break when no data is available', () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<AuditListTable items={websiteListResponse.items} />,
|
||||
),
|
||||
);
|
||||
const anchorSEO = rendered.queryByTitle(
|
||||
'trendline for SEO category of https://anchor.fm',
|
||||
);
|
||||
expect(anchorSEO).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
/*
|
||||
* 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, useMemo } from 'react';
|
||||
import {
|
||||
Link,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from '@material-ui/core';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
import {
|
||||
Audit,
|
||||
AuditCompleted,
|
||||
LighthouseCategoryId,
|
||||
Website,
|
||||
} from '../../api';
|
||||
import { formatTime } from '../../utils';
|
||||
import CategoryTrendline from '../CategoryTrendline';
|
||||
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',
|
||||
};
|
||||
|
||||
const useStyles = makeStyles({
|
||||
table: {
|
||||
minWidth: 650,
|
||||
},
|
||||
status: {
|
||||
textTransform: 'capitalize',
|
||||
},
|
||||
link: { padding: '0.75rem 0', display: 'inline-block' },
|
||||
statusCell: { whiteSpace: 'nowrap' },
|
||||
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,
|
||||
[item.url]: buildSparklinesDataForItem(item),
|
||||
}),
|
||||
{},
|
||||
),
|
||||
[items],
|
||||
);
|
||||
|
||||
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 => (
|
||||
<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}
|
||||
>
|
||||
<CategoryTrendline
|
||||
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>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuditListTable;
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const mocks = {
|
||||
replace: jest.fn(),
|
||||
push: jest.fn(),
|
||||
};
|
||||
return {
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useHistory: jest.fn(() => mocks),
|
||||
};
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import mockFetch from 'jest-fetch-mock';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { ApiRegistry, ApiProvider } from '@backstage/core';
|
||||
import { wrapInThemedTestApp, wrapInTheme } from '@backstage/test-utils';
|
||||
|
||||
import { lighthouseApiRef, LighthouseRestApi } from '../../api';
|
||||
import AuditList from '.';
|
||||
|
||||
const { useHistory } = require.requireMock('react-router-dom');
|
||||
|
||||
const websiteListJson = fs
|
||||
.readFileSync(
|
||||
path.join(__dirname, '../../__fixtures__/website-list-response.json'),
|
||||
)
|
||||
.toString();
|
||||
|
||||
describe('AuditList', () => {
|
||||
let apis: ApiRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
apis = ApiRegistry.from([
|
||||
[lighthouseApiRef, new LighthouseRestApi('http://lighthouse')],
|
||||
]);
|
||||
mockFetch.mockResponse(websiteListJson);
|
||||
});
|
||||
|
||||
it('should render the table', async () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<AuditList />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
const element = await rendered.findByText('https://anchor.fm');
|
||||
expect(element).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a link to create a new audit', async () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<AuditList />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
const element = await rendered.findByText('Create Audit');
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element.parentElement).toHaveAttribute(
|
||||
'href',
|
||||
'/lighthouse/create-audit',
|
||||
);
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
it('requests the correct limit and offset from the api based on the query', () => {
|
||||
mockFetch.mockClear();
|
||||
render(
|
||||
wrapInTheme(
|
||||
<MemoryRouter initialEntries={['/lighthouse?page=2']}>
|
||||
<ApiProvider apis={apis}>
|
||||
<AuditList />
|
||||
</ApiProvider>
|
||||
</MemoryRouter>,
|
||||
),
|
||||
);
|
||||
expect(mockFetch).toHaveBeenLastCalledWith(
|
||||
'http://lighthouse/v1/websites?limit=10&offset=10',
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
describe('when only one page is needed', () => {
|
||||
it('hides pagination elements', () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<AuditList />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
expect(rendered.queryByLabelText(/Go to page/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when multiple pages are needed', () => {
|
||||
beforeEach(() => {
|
||||
const response = JSON.parse(websiteListJson);
|
||||
response.limit = 5;
|
||||
response.offset = 5;
|
||||
response.total = 7;
|
||||
mockFetch.mockResponseOnce(JSON.stringify(response));
|
||||
});
|
||||
|
||||
it('shows pagination elements', async () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<AuditList />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
expect(
|
||||
await rendered.findByLabelText(/Go to page/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('changes the page on click', async () => {
|
||||
const rendered = render(
|
||||
wrapInTheme(
|
||||
<MemoryRouter initialEntries={['/lighthouse?page=2']}>
|
||||
<ApiProvider apis={apis}>
|
||||
<AuditList />
|
||||
</ApiProvider>
|
||||
</MemoryRouter>,
|
||||
),
|
||||
);
|
||||
const element = await rendered.findByLabelText(/Go to page 1/);
|
||||
fireEvent.click(element);
|
||||
expect(useHistory().replace).toHaveBeenCalledWith(`/lighthouse?page=1`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when waiting on the request', () => {
|
||||
it('should render the loader', async () => {
|
||||
mockFetch.mockResponseOnce(() => new Promise(() => {}));
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<AuditList />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
const element = await rendered.findByTestId('progress');
|
||||
expect(element).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the audits fail', () => {
|
||||
it('should render an error', async () => {
|
||||
mockFetch.mockRejectOnce(new Error('failed to fetch'));
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<AuditList />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
const element = await rendered.findByTestId('error-message');
|
||||
expect(element).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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, { useState, useMemo, FC, ReactNode } from 'react';
|
||||
import { useLocalStorage, useAsync } from 'react-use';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Grid, Button } from '@material-ui/core';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import Pagination from '@material-ui/lab/Pagination';
|
||||
import {
|
||||
InfoCard,
|
||||
Header,
|
||||
Page,
|
||||
Content,
|
||||
ContentHeader,
|
||||
HeaderLabel,
|
||||
Progress,
|
||||
pageTheme,
|
||||
useApi,
|
||||
} from '@backstage/core';
|
||||
|
||||
import { lighthouseApiRef, WebsiteListResponse } from '../../api';
|
||||
import { useQuery } from '../../utils';
|
||||
import LighthouseSupportButton from '../SupportButton';
|
||||
import LighthouseIntro, { LIGHTHOUSE_INTRO_LOCAL_STORAGE } from '../Intro';
|
||||
import AuditListTable from './AuditListTable';
|
||||
|
||||
export const LIMIT = 10;
|
||||
|
||||
const AuditList: FC<{}> = () => {
|
||||
const [dismissedStored] = useLocalStorage(LIGHTHOUSE_INTRO_LOCAL_STORAGE);
|
||||
const [dismissed, setDismissed] = useState(dismissedStored);
|
||||
|
||||
const query = useQuery();
|
||||
const page = query.get('page')
|
||||
? parseInt(query.get('page') as string, 10) || 1
|
||||
: 1;
|
||||
|
||||
const lighthouseApi = useApi(lighthouseApiRef);
|
||||
const { value, loading, error } = useAsync(
|
||||
async (): Promise<WebsiteListResponse> =>
|
||||
lighthouseApi.getWebsiteList({
|
||||
limit: LIMIT,
|
||||
offset: (page - 1) * LIMIT,
|
||||
}),
|
||||
[page],
|
||||
);
|
||||
|
||||
const pageCount: number = useMemo(() => {
|
||||
if (value?.total && value?.limit)
|
||||
return Math.ceil(value?.total / value?.limit);
|
||||
return 0;
|
||||
}, [value?.total, value?.limit]);
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
let content: ReactNode = null;
|
||||
if (value) {
|
||||
content = (
|
||||
<>
|
||||
<AuditListTable items={value?.items || []} />
|
||||
{pageCount > 1 && (
|
||||
<Pagination
|
||||
page={page}
|
||||
count={pageCount}
|
||||
onChange={(_event: Event, newPage: number) => {
|
||||
history.replace(`/lighthouse?page=${newPage}`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} else if (loading) {
|
||||
content = <Progress />;
|
||||
} else if (error) {
|
||||
content = (
|
||||
<Alert severity="error" data-testid="error-message">
|
||||
{error.message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
title="Lighthouse"
|
||||
subtitle="Website audits powered by Lighthouse"
|
||||
>
|
||||
<HeaderLabel label="Owner" value="Spotify" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Content>
|
||||
<LighthouseIntro onDismiss={() => setDismissed(true)} />
|
||||
<ContentHeader
|
||||
title="Audits"
|
||||
description="View all audits run for your website through Backstage here. Track the trend of your most recent audits."
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
href="/lighthouse/create-audit"
|
||||
>
|
||||
Create Audit
|
||||
</Button>
|
||||
{dismissed && <LighthouseSupportButton />}
|
||||
</ContentHeader>
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<InfoCard>{content}</InfoCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuditList;
|
||||
@@ -0,0 +1,28 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
/*
|
||||
* 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 { StatusPending, StatusError, StatusOK } from '@backstage/core';
|
||||
|
||||
import { Audit } from '../../api';
|
||||
|
||||
const AuditStatusIcon: FC<{ audit: Audit }> = ({ audit }) => {
|
||||
if (audit.status === 'FAILED') return <StatusError />;
|
||||
if (audit.status === 'COMPLETED') return <StatusOK />;
|
||||
return <StatusPending />;
|
||||
};
|
||||
|
||||
export default AuditStatusIcon;
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable jest/no-disabled-tests */
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: jest.fn(() => ({})),
|
||||
}));
|
||||
|
||||
import React from 'react';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import mockFetch from 'jest-fetch-mock';
|
||||
import { render } from '@testing-library/react';
|
||||
import { wrapInThemedTestApp } from '@backstage/test-utils';
|
||||
import { ApiRegistry, ApiProvider } from '@backstage/core';
|
||||
|
||||
import AuditView from '.';
|
||||
import { lighthouseApiRef, LighthouseRestApi, Audit, Website } from '../../api';
|
||||
import { formatTime } from '../../utils';
|
||||
|
||||
const { useParams }: { useParams: jest.Mock } = require.requireMock(
|
||||
'react-router-dom',
|
||||
);
|
||||
|
||||
const websiteResponseJson = fs
|
||||
.readFileSync(
|
||||
path.join(__dirname, '../../__fixtures__/website-response.json'),
|
||||
)
|
||||
.toString();
|
||||
|
||||
describe('AuditView', () => {
|
||||
let apis: ApiRegistry;
|
||||
let website: Website;
|
||||
let id: string;
|
||||
|
||||
beforeEach(() => {
|
||||
mockFetch.mockResponse(websiteResponseJson);
|
||||
apis = ApiRegistry.from([
|
||||
[lighthouseApiRef, new LighthouseRestApi('https://lighthouse')],
|
||||
]);
|
||||
website = JSON.parse(websiteResponseJson) as Website;
|
||||
id = website.audits.find(a => a.status === 'COMPLETED')?.id as string;
|
||||
useParams.mockReturnValue({ id });
|
||||
});
|
||||
|
||||
it('renders the iframe for the selected audit', async () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<AuditView />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
const iframe = await rendered.findByTitle(
|
||||
'Lighthouse audit for https://spotify.com',
|
||||
);
|
||||
expect(iframe).toBeInTheDocument();
|
||||
expect(iframe).toHaveAttribute('src', `https://lighthouse/v1/audits/${id}`);
|
||||
});
|
||||
|
||||
it('renders a link to create a new audit for this website', async () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<AuditView />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
const button = await rendered.findByText('Create Audit');
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button.parentElement).toHaveAttribute(
|
||||
'href',
|
||||
`/lighthouse/create-audit?url=${encodeURIComponent(
|
||||
'https://spotify.com',
|
||||
)}`,
|
||||
);
|
||||
});
|
||||
|
||||
describe('sidebar', () => {
|
||||
it('renders a list of all audits for the website', async () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<AuditView />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
await rendered.findByTestId('audit-sidebar');
|
||||
|
||||
website.audits.forEach(a => {
|
||||
expect(
|
||||
rendered.queryByText(formatTime(a.timeCreated)),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the current audit as active', async () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<AuditView />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
await rendered.findByTestId('audit-sidebar');
|
||||
|
||||
const audit = website.audits.find(a => a.id === id) as Audit;
|
||||
const auditElement = rendered.getByText(formatTime(audit.timeCreated));
|
||||
expect(auditElement.parentElement?.parentElement?.className).toContain(
|
||||
'selected',
|
||||
);
|
||||
|
||||
const notSelectedAudit = website.audits.find(a => a.id !== id) as Audit;
|
||||
const notSelectedAuditElement = rendered.getByText(
|
||||
formatTime(notSelectedAudit.timeCreated),
|
||||
);
|
||||
expect(
|
||||
notSelectedAuditElement.parentElement?.parentElement?.className,
|
||||
).not.toContain('selected');
|
||||
});
|
||||
|
||||
it('navigates to the next report when an audit is clicked', async () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<AuditView />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
await rendered.findByTestId('audit-sidebar');
|
||||
|
||||
website.audits.forEach(a => {
|
||||
expect(
|
||||
rendered.getByText(formatTime(a.timeCreated)).parentElement
|
||||
?.parentElement,
|
||||
).toHaveAttribute('href', `/lighthouse/audit/${a.id}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the request for the website by id is pending', () => {
|
||||
it('it shows the loading', async () => {
|
||||
mockFetch.mockImplementationOnce(() => new Promise(() => {}));
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<AuditView />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
expect(await rendered.findByTestId('progress')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the request for the website by id fails', () => {
|
||||
it('it shows an error', async () => {
|
||||
mockFetch.mockRejectOnce(new Error('failed to fetch'));
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<AuditView />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
expect(await rendered.findByText('failed to fetch')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('when a loading audit is accessed', () => {
|
||||
it('shows a loading view', async () => {
|
||||
id = website.audits.find(a => a.status === 'RUNNING')?.id as string;
|
||||
useParams.mockReturnValueOnce({ id });
|
||||
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<AuditView />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
await rendered.findByTestId('audit-sidebar');
|
||||
|
||||
expect(rendered.queryByTestId('progress')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('when a failed audit is accessed', () => {
|
||||
it('shows an error message', async () => {
|
||||
id = website.audits.find(a => a.status === 'FAILED')?.id as string;
|
||||
useParams.mockReturnValueOnce({ id });
|
||||
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<AuditView />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
await rendered.findByTestId('audit-sidebar');
|
||||
|
||||
expect(rendered.queryByText(/This audit failed/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* 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, { useState, useEffect, ReactNode, FC } from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import {
|
||||
makeStyles,
|
||||
Button,
|
||||
Grid,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
} from '@material-ui/core';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import {
|
||||
useApi,
|
||||
pageTheme,
|
||||
InfoCard,
|
||||
Header,
|
||||
Page,
|
||||
Content,
|
||||
ContentHeader,
|
||||
HeaderLabel,
|
||||
Progress,
|
||||
} from '@backstage/core';
|
||||
|
||||
import { lighthouseApiRef, Website, Audit } from '../../api';
|
||||
import AuditStatusIcon from '../AuditStatusIcon';
|
||||
import LighthouseSupportButton from '../SupportButton';
|
||||
import { formatTime } from '../../utils';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
contentGrid: {
|
||||
height: '100%',
|
||||
},
|
||||
iframe: {
|
||||
border: '0',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
errorOutput: { whiteSpace: 'pre' },
|
||||
});
|
||||
|
||||
interface AuditLinkListProps {
|
||||
audits?: Audit[];
|
||||
selectedId: string;
|
||||
}
|
||||
const AuditLinkList: FC<AuditLinkListProps> = ({
|
||||
audits = [],
|
||||
selectedId,
|
||||
}: AuditLinkListProps) => (
|
||||
<List
|
||||
data-testid="audit-sidebar"
|
||||
component="nav"
|
||||
aria-label="lighthouse audit history"
|
||||
>
|
||||
{audits.map(audit => (
|
||||
<ListItem
|
||||
key={audit.id}
|
||||
selected={audit.id === selectedId}
|
||||
button
|
||||
component={Link}
|
||||
replace
|
||||
to={`/lighthouse/audit/${audit.id}`}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<AuditStatusIcon audit={audit} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={formatTime(audit.timeCreated)} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
|
||||
const AuditView: FC<{ audit?: Audit }> = ({ audit }: { audit?: Audit }) => {
|
||||
const classes = useStyles();
|
||||
const params = useParams<{ id: string }>();
|
||||
const { url: lighthouseUrl } = useApi(lighthouseApiRef);
|
||||
|
||||
if (audit?.status === 'RUNNING') return <Progress />;
|
||||
if (audit?.status === 'FAILED')
|
||||
return (
|
||||
<Alert severity="error">
|
||||
This audit failed when attempting to run after several retries. Check
|
||||
that your instance of lighthouse-audit-service can successfully connect
|
||||
to your website and try again.
|
||||
</Alert>
|
||||
);
|
||||
|
||||
return (
|
||||
<InfoCard variant="fullHeight">
|
||||
<iframe
|
||||
className={classes.iframe}
|
||||
title={`Lighthouse audit${audit?.url ? ` for ${audit.url}` : ''}`}
|
||||
src={`${lighthouseUrl}/v1/audits/${params.id}`}
|
||||
/>
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
|
||||
const ConnectedAuditView: FC<{}> = () => {
|
||||
const lighthouseApi = useApi(lighthouseApiRef);
|
||||
const params = useParams<{ id: string }>();
|
||||
const classes = useStyles();
|
||||
|
||||
const { loading, error, value: nextValue } = useAsync<Website>(
|
||||
async () => lighthouseApi.getWebsiteForAuditId(params.id),
|
||||
[params.id],
|
||||
);
|
||||
const [value, setValue] = useState<Website>();
|
||||
useEffect(() => {
|
||||
if (!!nextValue && nextValue.url !== value?.url) {
|
||||
setValue(nextValue);
|
||||
}
|
||||
}, [value, nextValue, setValue]);
|
||||
|
||||
let content: ReactNode = null;
|
||||
if (value) {
|
||||
content = (
|
||||
<Grid container alignItems="stretch" className={classes.contentGrid}>
|
||||
<Grid item xs={3}>
|
||||
<AuditLinkList audits={value?.audits} selectedId={params.id} />
|
||||
</Grid>
|
||||
<Grid item xs={9}>
|
||||
<AuditView audit={value?.audits.find(a => a.id === params.id)} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
} else if (loading) {
|
||||
content = <Progress />;
|
||||
} else if (error) {
|
||||
content = (
|
||||
<Alert
|
||||
data-testid="error-message"
|
||||
severity="error"
|
||||
className={classes.errorOutput}
|
||||
>
|
||||
{error.message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
let createAuditButtonUrl = '/lighthouse/create-audit';
|
||||
if (value?.url) {
|
||||
createAuditButtonUrl += `?url=${encodeURIComponent(value.url)}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
title="Lighthouse"
|
||||
subtitle="Website audits powered by Lighthouse"
|
||||
>
|
||||
<HeaderLabel label="Owner" value="Spotify" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Content stretch>
|
||||
<ContentHeader
|
||||
title={value?.url || 'Audit'}
|
||||
description="See a history of all Lighthouse audits for your website run through Backstage."
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
href={createAuditButtonUrl}
|
||||
>
|
||||
Create Audit
|
||||
</Button>
|
||||
<LighthouseSupportButton />
|
||||
</ContentHeader>
|
||||
{content}
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectedAuditView;
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable jest/no-disabled-tests */
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { wrapInThemedTestApp } from '@backstage/test-utils';
|
||||
|
||||
import CategoryTrendline from '.';
|
||||
|
||||
describe('CategoryTrendline', () => {
|
||||
describe('when no data is present', () => {
|
||||
it('renders null without throwing', () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(<CategoryTrendline data={[]} title="sparkline" />),
|
||||
);
|
||||
expect(rendered.queryByTitle('sparkline')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when one datapoint is present', () => {
|
||||
it('renders as a straight line', () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<CategoryTrendline data={[0.5]} title="sparkline" />,
|
||||
),
|
||||
);
|
||||
expect(rendered.getByTitle('sparkline')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('when the data finishes above the success threshold', () => {
|
||||
it('renders with the correct color', () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<CategoryTrendline data={[0.5, 0.95]} title="sparkline" />,
|
||||
),
|
||||
);
|
||||
expect(rendered.getByTitle('sparkline')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('when the data finishes within the the warning threshold', () => {
|
||||
it('renders with the correct color', () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<CategoryTrendline data={[0.5, 0.65]} title="sparkline" />,
|
||||
),
|
||||
);
|
||||
expect(rendered.getByTitle('sparkline')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('when the data finishes within the the error threshold', () => {
|
||||
it('renders with the correct color', () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<CategoryTrendline data={[0.5, 0.4]} title="sparkline" />,
|
||||
),
|
||||
);
|
||||
expect(rendered.getByTitle('sparkline')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
/*
|
||||
* 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 { Sparklines, SparklinesLine, SparklinesProps } from 'react-sparklines';
|
||||
import { COLORS } from '@backstage/theme';
|
||||
|
||||
function color(data: number[]): string | undefined {
|
||||
const lastNum = data[data.length - 1];
|
||||
if (!lastNum) return undefined;
|
||||
if (lastNum >= 0.9) return COLORS.STATUS.OK;
|
||||
if (lastNum >= 0.5) return COLORS.STATUS.WARNING;
|
||||
return COLORS.STATUS.ERROR;
|
||||
}
|
||||
|
||||
const CategoryTrendline: FC<SparklinesProps & { title?: string }> = props => {
|
||||
if (!props.data) return null;
|
||||
return (
|
||||
<Sparklines width={120} height={30} min={0} max={1} {...props}>
|
||||
{props.title && <title>{props.title}</title>}
|
||||
<SparklinesLine color={color(props.data)} />
|
||||
</Sparklines>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryTrendline;
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const mocks = {
|
||||
replace: jest.fn(),
|
||||
push: jest.fn(),
|
||||
};
|
||||
return {
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useHistory: jest.fn(() => mocks),
|
||||
};
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import mockFetch from 'jest-fetch-mock';
|
||||
import { wait, render, fireEvent } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import {
|
||||
ApiRegistry,
|
||||
ApiProvider,
|
||||
ErrorApi,
|
||||
errorApiRef,
|
||||
} from '@backstage/core';
|
||||
import { wrapInThemedTestApp, wrapInTheme } from '@backstage/test-utils';
|
||||
|
||||
import { lighthouseApiRef, LighthouseRestApi } from '../../api';
|
||||
import CreateAudit from '.';
|
||||
|
||||
const { useHistory }: { useHistory: jest.Mock } = require.requireMock(
|
||||
'react-router-dom',
|
||||
);
|
||||
|
||||
const createAuditResponseJson = fs
|
||||
.readFileSync(
|
||||
path.join(__dirname, '../../__fixtures__/create-audit-response.json'),
|
||||
)
|
||||
.toString();
|
||||
|
||||
// TODO add act() to these tests without breaking them!
|
||||
describe('CreateAudit', () => {
|
||||
let apis: ApiRegistry;
|
||||
let errorApi: ErrorApi;
|
||||
|
||||
beforeEach(() => {
|
||||
errorApi = { post: jest.fn() };
|
||||
apis = ApiRegistry.from([
|
||||
[lighthouseApiRef, new LighthouseRestApi('http://lighthouse')],
|
||||
[errorApiRef, errorApi],
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders the form', () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<CreateAudit />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
expect(rendered.getByLabelText(/URL/)).toBeEnabled();
|
||||
expect(rendered.getByLabelText(/URL/)).toHaveAttribute('value', '');
|
||||
expect(rendered.getByText(/Create Audit/)).toBeEnabled();
|
||||
});
|
||||
|
||||
describe('when the location contains a url', () => {
|
||||
it('prefills the url into the form', () => {
|
||||
const url = 'https://spotify.com';
|
||||
const rendered = render(
|
||||
wrapInTheme(
|
||||
<MemoryRouter
|
||||
initialEntries={[
|
||||
`/lighthouse/create-audit?url=${encodeURIComponent(url)}`,
|
||||
]}
|
||||
>
|
||||
<ApiProvider apis={apis}>
|
||||
<CreateAudit />
|
||||
</ApiProvider>
|
||||
</MemoryRouter>,
|
||||
),
|
||||
);
|
||||
expect(rendered.getByLabelText(/URL/)).toHaveAttribute('value', url);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when waiting on the request', () => {
|
||||
it('disables the form fields', () => {
|
||||
mockFetch.mockResponseOnce(() => new Promise(() => {}));
|
||||
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<CreateAudit />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(rendered.getByLabelText(/URL/), {
|
||||
target: { value: 'https://spotify.com' },
|
||||
});
|
||||
fireEvent.click(rendered.getByText(/Create Audit/));
|
||||
|
||||
expect(rendered.getByLabelText(/URL/)).toBeDisabled();
|
||||
expect(rendered.getByText(/Create Audit/)).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the audit is successfully created', () => {
|
||||
it('triggers a location change to the table', async () => {
|
||||
useHistory().push.mockClear();
|
||||
mockFetch.mockResponseOnce(createAuditResponseJson);
|
||||
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<CreateAudit />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(rendered.getByLabelText(/URL/), {
|
||||
target: { value: 'https://spotify.com' },
|
||||
});
|
||||
fireEvent.click(rendered.getByText(/Create Audit/));
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'http://lighthouse/v1/audits',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
}),
|
||||
);
|
||||
|
||||
await wait(() => expect(rendered.getByLabelText(/URL/)).toBeEnabled());
|
||||
|
||||
expect(useHistory().push).toHaveBeenCalledWith('/lighthouse');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the audits fail', () => {
|
||||
it('should render an error', async () => {
|
||||
(errorApi.post as jest.Mock).mockClear();
|
||||
mockFetch.mockRejectOnce(new Error('failed to post'));
|
||||
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<CreateAudit />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
fireEvent.change(rendered.getByLabelText(/URL/), {
|
||||
target: { value: 'https://spotify.com' },
|
||||
});
|
||||
fireEvent.click(rendered.getByText(/Create Audit/));
|
||||
|
||||
await wait(() => expect(rendered.getByLabelText(/URL/)).toBeEnabled());
|
||||
await new Promise(r => setTimeout(r, 0));
|
||||
|
||||
expect(errorApi.post).toHaveBeenCalledWith(expect.any(Error));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* 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, { useState, useCallback, FC } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import {
|
||||
makeStyles,
|
||||
Grid,
|
||||
Button,
|
||||
List,
|
||||
ListItem,
|
||||
MenuItem,
|
||||
TextField,
|
||||
} from '@material-ui/core';
|
||||
import {
|
||||
errorApiRef,
|
||||
useApi,
|
||||
InfoCard,
|
||||
Header,
|
||||
Page,
|
||||
pageTheme,
|
||||
Content,
|
||||
ContentHeader,
|
||||
HeaderLabel,
|
||||
} from '@backstage/core';
|
||||
|
||||
import { lighthouseApiRef } from '../../api';
|
||||
import { useQuery } from '../../utils';
|
||||
import LighthouseSupportButton from '../SupportButton';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
input: {
|
||||
minWidth: 300,
|
||||
},
|
||||
buttonList: {
|
||||
marginLeft: theme.spacing(-1),
|
||||
marginRight: theme.spacing(-1),
|
||||
'& > *': {
|
||||
margin: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const CreateAudit: FC<{}> = () => {
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const lighthouseApi = useApi(lighthouseApiRef);
|
||||
const classes = useStyles();
|
||||
const query = useQuery();
|
||||
const history = useHistory();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [url, setUrl] = useState<string>(query.get('url') || '');
|
||||
const [emulatedFormFactor, setEmulatedFormFactor] = useState('mobile');
|
||||
|
||||
const triggerAudit = useCallback(async (): Promise<void> => {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
// TODO use the id from the response to redirect to the audit page for that id when
|
||||
// FAILED and RUNNING audits are supported
|
||||
await lighthouseApi.triggerAudit({
|
||||
url,
|
||||
options: {
|
||||
lighthouseConfig: {
|
||||
settings: {
|
||||
emulatedFormFactor,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
history.push('/lighthouse');
|
||||
} catch (err) {
|
||||
errorApi.post(err);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}, [url, emulatedFormFactor, lighthouseApi, setSubmitting]);
|
||||
|
||||
return (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
title="Lighthouse"
|
||||
subtitle="Website audits powered by Lighthouse"
|
||||
>
|
||||
<HeaderLabel label="Owner" value="Spotify" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentHeader
|
||||
title="Trigger a new audit"
|
||||
description="Submitting this form will immediately trigger and store a new Lighthouse audit. Trigger audits to track your website's accessibility, performance, SEO, and best practices over time."
|
||||
>
|
||||
<LighthouseSupportButton />
|
||||
</ContentHeader>
|
||||
<Grid container direction="column">
|
||||
<Grid item xs={12} sm={6}>
|
||||
<InfoCard>
|
||||
<form
|
||||
onSubmit={ev => {
|
||||
ev.preventDefault();
|
||||
triggerAudit();
|
||||
}}
|
||||
>
|
||||
<List>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="lighthouse-create-audit-url-tf"
|
||||
className={classes.input}
|
||||
label="URL"
|
||||
placeholder="https://spotify.com"
|
||||
helperText="The target URL for Lighthouse to use."
|
||||
required
|
||||
disabled={submitting}
|
||||
onChange={ev => setUrl(ev.target.value)}
|
||||
value={url}
|
||||
inputProps={{ 'aria-label': 'URL' }}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="lighthouse-create-audit-emulated-form-factor-tf"
|
||||
className={classes.input}
|
||||
label="Emulated Form Factor"
|
||||
helperText="Device to simulate when auditing"
|
||||
select
|
||||
required
|
||||
disabled={submitting}
|
||||
onChange={ev => setEmulatedFormFactor(ev.target.value)}
|
||||
value={emulatedFormFactor}
|
||||
inputProps={{ 'aria-label': 'Emulated form factor' }}
|
||||
>
|
||||
<MenuItem value="mobile">Mobile</MenuItem>
|
||||
<MenuItem value="desktop">Desktop</MenuItem>
|
||||
</TextField>
|
||||
</ListItem>
|
||||
<ListItem className={classes.buttonList}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
href="/lighthouse"
|
||||
disabled={submitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
>
|
||||
Create Audit
|
||||
</Button>
|
||||
</ListItem>
|
||||
</List>
|
||||
</form>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateAudit;
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable jest/no-disabled-tests */
|
||||
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { wrapInThemedTestApp } from '@backstage/test-utils';
|
||||
|
||||
import LighthouseIntro from '.';
|
||||
|
||||
describe('LighthouseIntro', () => {
|
||||
it('renders successfully', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<LighthouseIntro />));
|
||||
expect(
|
||||
rendered.queryByText('Welcome to Lighthouse in Backstage!'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('tabs', () => {
|
||||
const firstTabRe = /This plugin allows you to/;
|
||||
const secondTabRe = /you will need a running instance of/;
|
||||
|
||||
it('selects the first text element', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<LighthouseIntro />));
|
||||
expect(rendered.queryByText(firstTabRe)).toBeInTheDocument();
|
||||
expect(rendered.queryByText(secondTabRe)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the other text when the tab is clicked', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<LighthouseIntro />));
|
||||
fireEvent.click(rendered.getByText('Setup'));
|
||||
expect(rendered.queryByText(firstTabRe)).not.toBeInTheDocument();
|
||||
expect(rendered.queryByText(secondTabRe)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('closing', () => {
|
||||
it('hides the content on click', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<LighthouseIntro />));
|
||||
const welcomeMessage = rendered.queryByText(
|
||||
'Welcome to Lighthouse in Backstage!',
|
||||
);
|
||||
expect(welcomeMessage).toBeInTheDocument();
|
||||
fireEvent.click(rendered.getByText('Hide intro'));
|
||||
|
||||
expect(welcomeMessage).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import Markdown from 'react-markdown';
|
||||
import { ContentHeader, InfoCard } from '@backstage/core';
|
||||
import { makeStyles, Button, Grid, Tabs, Tab } from '@material-ui/core';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
|
||||
import LighthouseSupportButton from '../SupportButton';
|
||||
|
||||
export const LIGHTHOUSE_INTRO_LOCAL_STORAGE =
|
||||
'@backstage/lighthouse-plugin/intro-dismissed';
|
||||
|
||||
const USE_CASES = `
|
||||
Google's [Lighthouse](https://developers.google.com/web/tools/lighthouse) auditing tool for websites
|
||||
is a great open-source resource forbenchmarking and improving the accessibility, performance, SEO, and best practices of your site.
|
||||
At Spotify, we keep track of Lighthouse audit scores over time to look at trends and overall areas for investment.
|
||||
|
||||
This plugin allows you to generate on-demand Lighthouse audits for websites, and to track the trends for the
|
||||
top-level categories of Lighthouse at a glance.
|
||||
|
||||
In the future, we hope to add support for scheduling audits (which we do internally), as well as allowing
|
||||
custom runs of Lighthouse to be ingested (for auditing sites that require authentication or some session state).
|
||||
`;
|
||||
|
||||
const SETUP = `
|
||||
To get started, you will need a running instance of [lighthouse-audit-service](https://github.com/spotify/lighthouse-audit-service).
|
||||
_It's likely you will need to enable CORS when running lighthouse-audit-service. Initialize the app
|
||||
with the environment variable \`LAS_CORS\` set to \`true\`._
|
||||
|
||||
When you have an instance running that Backstage can hook into, make sure to export the plugin in
|
||||
your app's [\`plugins.ts\`](https://github.com/spotify/backstage/blob/master/packages/app/src/plugins.ts)
|
||||
to enable the plugin:
|
||||
|
||||
\`\`\`js
|
||||
export { default as LighthousePlugin } from '@backstage/plugin-lighthouse';
|
||||
\`\`\`
|
||||
|
||||
Then, you need to use the \`lighthouseApiRef\` exported from the plugin to initialize the Rest API in
|
||||
your [\`apis.ts\`](https://github.com/spotify/backstage/blob/master/packages/app/src/apis.ts).
|
||||
|
||||
\`\`\`js
|
||||
import { ApiHolder, ApiRegistry } from '@backstage/core';
|
||||
import {
|
||||
lighthouseApiRef,
|
||||
LighthouseRestApi,
|
||||
} from '@backstage/lighthouse-audits';
|
||||
|
||||
const builder = ApiRegistry.builder();
|
||||
|
||||
export const lighthouseApi =
|
||||
new LighthouseRestApi(/* your service url here! */);
|
||||
builder.add(lighthouseApiRef, lighthouseApi);
|
||||
|
||||
export default builder.build() as ApiHolder;
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
tabs: { marginBottom: -18 },
|
||||
tab: { minWidth: 72, paddingLeft: 1, paddingRight: 1 },
|
||||
content: { marginBottom: theme.spacing(2) },
|
||||
closeButtonContainer: { height: '100%' },
|
||||
closeButtonItem: { paddingBottom: 0 },
|
||||
}));
|
||||
|
||||
function GettingStartedCard() {
|
||||
const classes = useStyles();
|
||||
const [value, setValue] = useState(0);
|
||||
return (
|
||||
<InfoCard
|
||||
title="Get started"
|
||||
subheader={
|
||||
<Tabs
|
||||
value={value}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
onChange={(_ev, newValue: number) => setValue(newValue)}
|
||||
aria-label="get started tabs"
|
||||
className={classes.tabs}
|
||||
>
|
||||
<Tab className={classes.tab} label="Use cases" />
|
||||
<Tab className={classes.tab} label="Setup" />
|
||||
</Tabs>
|
||||
}
|
||||
divider
|
||||
actions={
|
||||
<>
|
||||
<Grid container direction="row" justify="flex-end">
|
||||
<Grid item>
|
||||
<Button
|
||||
component="a"
|
||||
href="https://github.com/spotify/lighthouse-audit-service"
|
||||
size="small"
|
||||
target="_blank"
|
||||
>
|
||||
Check out the README
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{value === 0 && <Markdown source={USE_CASES} />}
|
||||
{value === 1 && <Markdown source={SETUP} />}
|
||||
</InfoCard>
|
||||
);
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
export default function LighthouseIntro({ onDismiss = () => {} }: Props) {
|
||||
const classes = useStyles();
|
||||
const [dismissed, setDismissed] = useLocalStorage(
|
||||
LIGHTHOUSE_INTRO_LOCAL_STORAGE,
|
||||
false,
|
||||
);
|
||||
|
||||
if (dismissed) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContentHeader title="Welcome to Lighthouse in Backstage!">
|
||||
<LighthouseSupportButton />
|
||||
</ContentHeader>
|
||||
<Grid className={classes.content} container spacing={3} direction="row">
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<GettingStartedCard />
|
||||
</Grid>
|
||||
{/* TODO add link and image for blog post here */}
|
||||
{/* <Grid item xs={12} sm={6} md={4}>
|
||||
<InfoCard>Blog</InfoCard>
|
||||
</Grid> */}
|
||||
<Grid item xs={12} sm={6} md={8}>
|
||||
<Grid
|
||||
container
|
||||
justify="flex-end"
|
||||
alignItems="flex-end"
|
||||
className={classes.closeButtonContainer}
|
||||
>
|
||||
<Grid item className={classes.closeButtonItem}>
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
onDismiss();
|
||||
setDismissed(true);
|
||||
}}
|
||||
>
|
||||
<CloseIcon /> Hide intro
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 { SupportButton } from '@backstage/core';
|
||||
|
||||
export default function LighthouseSupportButton() {
|
||||
return (
|
||||
<SupportButton>
|
||||
Lighthouse audits run for any web domain, stored over time.
|
||||
</SupportButton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 './plugin';
|
||||
export * from './api';
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 plugin from './plugin';
|
||||
|
||||
describe('lighthouse', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(plugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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 { createPlugin } from '@backstage/core';
|
||||
import AuditList from './components/AuditList';
|
||||
import AuditView from './components/AuditView';
|
||||
import CreateAudit from './components/CreateAudit';
|
||||
|
||||
export default createPlugin({
|
||||
id: 'lighthouse',
|
||||
register({ router }) {
|
||||
router.registerRoute('/lighthouse', AuditList);
|
||||
router.registerRoute('/lighthouse/audit/:id', AuditView);
|
||||
router.registerRoute('/lighthouse/create-audit', CreateAudit);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 '@testing-library/jest-dom/extend-expect';
|
||||
require('jest-fetch-mock').enableMocks();
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 { useLocation } from 'react-router-dom';
|
||||
|
||||
export function useQuery(): URLSearchParams {
|
||||
return new URLSearchParams(useLocation().search);
|
||||
}
|
||||
|
||||
export function formatTime(timestamp: string | Date) {
|
||||
let date: Date;
|
||||
if (timestamp instanceof Date) {
|
||||
date = timestamp;
|
||||
} else {
|
||||
date = new Date(timestamp);
|
||||
}
|
||||
return date.toUTCString();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@
|
||||
"@types/testing-library__jest-dom": "5.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@backstage/core": "^0.1.1-alpha.2",
|
||||
"@backstage/theme": "^0.1.1-alpha.2",
|
||||
"@backstage/core": "^0.1.1-alpha.3",
|
||||
"@backstage/theme": "^0.1.1-alpha.3",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
|
||||
@@ -3769,6 +3769,13 @@
|
||||
"@types/history" "*"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-sparklines@^1.7.0":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.npmjs.org/@types/react-sparklines/-/react-sparklines-1.7.0.tgz#f956d0f7b0e746ad445ce1cd250fe81f8a384684"
|
||||
integrity sha512-Vd+cME7+Yy3kFNhnid9EBIKiyCQ/at8nqDczIs0UYfIB8AtaRJPqekigv02biOsIbQCvxyvIAIjiTKOC+hHNbA==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-syntax-highlighter@11.0.4":
|
||||
version "11.0.4"
|
||||
resolved "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.4.tgz#d86d17697db62f98046874f62fdb3e53a0bbc4cd"
|
||||
@@ -5227,6 +5234,11 @@ babylon@^6.18.0:
|
||||
resolved "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
|
||||
integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==
|
||||
|
||||
bail@^1.0.0:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776"
|
||||
integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
@@ -6147,6 +6159,11 @@ code-point-at@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
|
||||
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
|
||||
|
||||
collapse-white-space@^1.0.2:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287"
|
||||
integrity sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==
|
||||
|
||||
collect-v8-coverage@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.0.tgz#150ee634ac3650b71d9c985eb7f608942334feb1"
|
||||
@@ -6696,6 +6713,14 @@ cross-env@^7.0.0:
|
||||
dependencies:
|
||||
cross-spawn "^7.0.1"
|
||||
|
||||
cross-fetch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.4.tgz#7bef7020207e684a7638ef5f2f698e24d9eb283c"
|
||||
integrity sha512-MSHgpjQqgbT/94D4CyADeNoYh52zMkCX4pcJvPP5WqPsLFMKjr2TCMg381ox5qI0ii2dPwaLx/00477knXqXVw==
|
||||
dependencies:
|
||||
node-fetch "2.6.0"
|
||||
whatwg-fetch "3.0.0"
|
||||
|
||||
cross-spawn-promise@^0.10.1:
|
||||
version "0.10.2"
|
||||
resolved "https://registry.npmjs.org/cross-spawn-promise/-/cross-spawn-promise-0.10.2.tgz#0e6338149caf53a6d557ac5c65efb3086d8704ac"
|
||||
@@ -7527,7 +7552,7 @@ dom-helpers@^5.0.1:
|
||||
"@babel/runtime" "^7.6.3"
|
||||
csstype "^2.6.7"
|
||||
|
||||
dom-serializer@0:
|
||||
dom-serializer@0, dom-serializer@^0.2.1:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
|
||||
integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==
|
||||
@@ -7569,6 +7594,13 @@ domhandler@^2.3.0:
|
||||
dependencies:
|
||||
domelementtype "1"
|
||||
|
||||
domhandler@^3.0, domhandler@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/domhandler/-/domhandler-3.0.0.tgz#51cd13efca31da95bbb0c5bee3a48300e333b3e9"
|
||||
integrity sha512-eKLdI5v9m67kbXQbJSNn1zjh0SDzvzWVWtX+qEI3eMjZw8daH9k8rlj1FZY9memPwjiskQFbe7vHVVJIAqoEhw==
|
||||
dependencies:
|
||||
domelementtype "^2.0.1"
|
||||
|
||||
domutils@1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
|
||||
@@ -7585,6 +7617,15 @@ domutils@^1.5.1, domutils@^1.7.0:
|
||||
dom-serializer "0"
|
||||
domelementtype "1"
|
||||
|
||||
domutils@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/domutils/-/domutils-2.0.0.tgz#15b8278e37bfa8468d157478c58c367718133c08"
|
||||
integrity sha512-n5SelJ1axbO636c2yUtOGia/IcJtVtlhQbFiVDBZHKV5ReJO1ViX7sFEemtuyoAnBxk5meNSYgA8V4s0271efg==
|
||||
dependencies:
|
||||
dom-serializer "^0.2.1"
|
||||
domelementtype "^2.0.1"
|
||||
domhandler "^3.0.0"
|
||||
|
||||
dot-case@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.3.tgz#21d3b52efaaba2ea5fda875bb1aa8124521cf4aa"
|
||||
@@ -8477,7 +8518,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
|
||||
assign-symbols "^1.0.0"
|
||||
is-extendable "^1.0.1"
|
||||
|
||||
extend@~3.0.2:
|
||||
extend@^3.0.0, extend@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
||||
@@ -9855,6 +9896,16 @@ html-minifier@^3.2.3:
|
||||
relateurl "0.2.x"
|
||||
uglify-js "3.4.x"
|
||||
|
||||
html-to-react@^1.3.4:
|
||||
version "1.4.2"
|
||||
resolved "https://registry.npmjs.org/html-to-react/-/html-to-react-1.4.2.tgz#7b628ab56cd63a52f2d0b79d0fa838a51f088a57"
|
||||
integrity sha512-TdTfxd95sRCo6QL8admCkE7mvNNrXtGoVr1dyS+7uvc8XCqAymnf/6ckclvnVbQNUo2Nh21VPwtfEHd0khiV7g==
|
||||
dependencies:
|
||||
domhandler "^3.0"
|
||||
htmlparser2 "^4.0"
|
||||
lodash.camelcase "^4.3.0"
|
||||
ramda "^0.26"
|
||||
|
||||
html-webpack-plugin@4.0.0-beta.11:
|
||||
version "4.0.0-beta.11"
|
||||
resolved "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.0.0-beta.11.tgz#3059a69144b5aecef97708196ca32f9e68677715"
|
||||
@@ -9904,6 +9955,16 @@ htmlparser2@^3.3.0:
|
||||
inherits "^2.0.1"
|
||||
readable-stream "^3.1.1"
|
||||
|
||||
htmlparser2@^4.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78"
|
||||
integrity sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==
|
||||
dependencies:
|
||||
domelementtype "^2.0.1"
|
||||
domhandler "^3.0.0"
|
||||
domutils "^2.0.0"
|
||||
entities "^2.0.0"
|
||||
|
||||
http-cache-semantics@^3.8.1:
|
||||
version "3.8.1"
|
||||
resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2"
|
||||
@@ -10221,7 +10282,7 @@ inflight@^1.0.4, inflight@~1.0.6:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
|
||||
inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
@@ -10472,7 +10533,7 @@ is-binary-path@~2.1.0:
|
||||
dependencies:
|
||||
binary-extensions "^2.0.0"
|
||||
|
||||
is-buffer@^1.0.2, is-buffer@^1.1.5:
|
||||
is-buffer@^1.0.2, is-buffer@^1.1.4, is-buffer@^1.1.5:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
||||
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
|
||||
@@ -10874,6 +10935,11 @@ is-utf8@^0.2.0, is-utf8@^0.2.1:
|
||||
resolved "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
|
||||
integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
|
||||
|
||||
is-whitespace-character@^1.0.0:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7"
|
||||
integrity sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==
|
||||
|
||||
is-window@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d"
|
||||
@@ -10884,6 +10950,11 @@ is-windows@^1.0.0, is-windows@^1.0.1, is-windows@^1.0.2:
|
||||
resolved "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
||||
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
|
||||
|
||||
is-word-character@^1.0.0:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz#ce0e73216f98599060592f62ff31354ddbeb0230"
|
||||
integrity sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==
|
||||
|
||||
is-wsl@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
|
||||
@@ -11278,6 +11349,14 @@ jest-environment-node@^25.1.0:
|
||||
jest-mock "^25.1.0"
|
||||
jest-util "^25.1.0"
|
||||
|
||||
jest-fetch-mock@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz#31749c456ae27b8919d69824f1c2bd85fe0a1f3b"
|
||||
integrity sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==
|
||||
dependencies:
|
||||
cross-fetch "^3.0.4"
|
||||
promise-polyfill "^8.1.3"
|
||||
|
||||
jest-get-type@^24.9.0:
|
||||
version "24.9.0"
|
||||
resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e"
|
||||
@@ -12950,6 +13029,11 @@ map-visit@^1.0.0:
|
||||
dependencies:
|
||||
object-visit "^1.0.0"
|
||||
|
||||
markdown-escapes@^1.0.0:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535"
|
||||
integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==
|
||||
|
||||
markdown-to-jsx@^6.9.1, markdown-to-jsx@^6.9.3:
|
||||
version "6.11.0"
|
||||
resolved "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-6.11.0.tgz#a2e3f2bc781c3402d8bb0f8e0a12a186474623b0"
|
||||
@@ -12984,6 +13068,13 @@ md5.js@^1.3.4:
|
||||
inherits "^2.0.1"
|
||||
safe-buffer "^5.1.2"
|
||||
|
||||
mdast-add-list-metadata@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/mdast-add-list-metadata/-/mdast-add-list-metadata-1.0.1.tgz#95e73640ce2fc1fa2dcb7ec443d09e2bfe7db4cf"
|
||||
integrity sha512-fB/VP4MJ0LaRsog7hGPxgOrSL3gE/2uEdZyDuSEnKCv/8IkYHiDkIQSbChiJoHyxZZXZ9bzckyRk+vNxFzh8rA==
|
||||
dependencies:
|
||||
unist-util-visit-parents "1.1.2"
|
||||
|
||||
mdn-data@2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
|
||||
@@ -13583,6 +13674,11 @@ node-fetch-npm@^2.0.2:
|
||||
json-parse-better-errors "^1.0.0"
|
||||
safe-buffer "^5.1.1"
|
||||
|
||||
node-fetch@2.6.0, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
|
||||
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
|
||||
|
||||
node-fetch@^1.0.1:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
|
||||
@@ -13591,11 +13687,6 @@ node-fetch@^1.0.1:
|
||||
encoding "^0.1.11"
|
||||
is-stream "^1.0.1"
|
||||
|
||||
node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
|
||||
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
|
||||
|
||||
node-forge@0.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
|
||||
@@ -14577,7 +14668,7 @@ parse-asn1@^5.0.0:
|
||||
pbkdf2 "^3.0.3"
|
||||
safe-buffer "^5.1.1"
|
||||
|
||||
parse-entities@^1.1.2:
|
||||
parse-entities@^1.1.0, parse-entities@^1.1.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz#c31bf0f653b6661354f8973559cb86dd1d5edf50"
|
||||
integrity sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==
|
||||
@@ -15756,6 +15847,11 @@ promise-inflight@^1.0.1, promise-inflight@~1.0.1:
|
||||
resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
|
||||
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
|
||||
|
||||
promise-polyfill@^8.1.3:
|
||||
version "8.1.3"
|
||||
resolved "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.1.3.tgz#8c99b3cf53f3a91c68226ffde7bde81d7f904116"
|
||||
integrity sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g==
|
||||
|
||||
promise-retry@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz#6739e968e3051da20ce6497fb2b50f6911df3d6d"
|
||||
@@ -15818,7 +15914,7 @@ promzard@^0.3.0:
|
||||
dependencies:
|
||||
read "1"
|
||||
|
||||
prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||
prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||
version "15.7.2"
|
||||
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
||||
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
||||
@@ -16012,7 +16108,7 @@ raf@^3.4.1:
|
||||
dependencies:
|
||||
performance-now "^2.1.0"
|
||||
|
||||
ramda@0.26.1:
|
||||
ramda@0.26.1, ramda@^0.26:
|
||||
version "0.26.1"
|
||||
resolved "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06"
|
||||
integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==
|
||||
@@ -16177,7 +16273,7 @@ react-docgen@^5.0.0:
|
||||
node-dir "^0.1.10"
|
||||
strip-indent "^3.0.0"
|
||||
|
||||
react-dom@^16.12.0, react-dom@^16.8.3:
|
||||
react-dom@^16.12.0, react-dom@^16.13.1, react-dom@^16.8.3:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
|
||||
integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==
|
||||
@@ -16254,7 +16350,7 @@ react-inspector@^4.0.0:
|
||||
is-dom "^1.0.9"
|
||||
prop-types "^15.6.1"
|
||||
|
||||
react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4:
|
||||
react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
@@ -16264,6 +16360,20 @@ react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4:
|
||||
resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
||||
|
||||
react-markdown@^4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.npmjs.org/react-markdown/-/react-markdown-4.3.1.tgz#39f0633b94a027445b86c9811142d05381300f2f"
|
||||
integrity sha512-HQlWFTbDxTtNY6bjgp3C3uv1h2xcjCSi1zAEzfBW9OwJJvENSYiLXWNXN5hHLsoqai7RnZiiHzcnWdXk2Splzw==
|
||||
dependencies:
|
||||
html-to-react "^1.3.4"
|
||||
mdast-add-list-metadata "1.0.1"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^16.8.6"
|
||||
remark-parse "^5.0.0"
|
||||
unified "^6.1.5"
|
||||
unist-util-visit "^1.3.0"
|
||||
xtend "^4.0.1"
|
||||
|
||||
react-popper-tooltip@^2.8.3:
|
||||
version "2.10.1"
|
||||
resolved "https://registry.npmjs.org/react-popper-tooltip/-/react-popper-tooltip-2.10.1.tgz#e10875f31916297c694d64a677d6f8fa0a48b4d1"
|
||||
@@ -16391,6 +16501,13 @@ react-sizeme@^2.6.7:
|
||||
shallowequal "^1.1.0"
|
||||
throttle-debounce "^2.1.0"
|
||||
|
||||
react-sparklines@^1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.npmjs.org/react-sparklines/-/react-sparklines-1.7.0.tgz#9b1d97e8c8610095eeb2ad658d2e1fcf91f91a60"
|
||||
integrity sha512-bJFt9K4c5Z0k44G8KtxIhbG+iyxrKjBZhdW6afP+R7EnIq+iKjbWbEFISrf3WKNFsda+C46XAfnX0StS5fbDcg==
|
||||
dependencies:
|
||||
prop-types "^15.5.10"
|
||||
|
||||
react-syntax-highlighter@^11.0.2:
|
||||
version "11.0.2"
|
||||
resolved "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-11.0.2.tgz#4e3f376e752b20d2f54e4c55652fd663149e4029"
|
||||
@@ -16439,7 +16556,7 @@ react-use@^13.24.0:
|
||||
ts-easing "^0.2.0"
|
||||
tslib "^1.10.0"
|
||||
|
||||
react@^16.12.0, react@^16.8.3:
|
||||
react@^16.12.0, react@^16.13.1, react@^16.8.3:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.npmjs.org/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
|
||||
integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==
|
||||
@@ -16833,6 +16950,27 @@ relateurl@0.2.x, relateurl@^0.2.7:
|
||||
resolved "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
|
||||
integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
|
||||
|
||||
remark-parse@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmjs.org/remark-parse/-/remark-parse-5.0.0.tgz#4c077f9e499044d1d5c13f80d7a98cf7b9285d95"
|
||||
integrity sha512-b3iXszZLH1TLoyUzrATcTQUZrwNl1rE70rVdSruJFlDaJ9z5aMkhrG43Pp68OgfHndL/ADz6V69Zow8cTQu+JA==
|
||||
dependencies:
|
||||
collapse-white-space "^1.0.2"
|
||||
is-alphabetical "^1.0.0"
|
||||
is-decimal "^1.0.0"
|
||||
is-whitespace-character "^1.0.0"
|
||||
is-word-character "^1.0.0"
|
||||
markdown-escapes "^1.0.0"
|
||||
parse-entities "^1.1.0"
|
||||
repeat-string "^1.5.4"
|
||||
state-toggle "^1.0.0"
|
||||
trim "0.0.1"
|
||||
trim-trailing-lines "^1.0.0"
|
||||
unherit "^1.0.4"
|
||||
unist-util-remove-position "^1.0.0"
|
||||
vfile-location "^2.0.0"
|
||||
xtend "^4.0.1"
|
||||
|
||||
remove-trailing-separator@^1.0.1:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
|
||||
@@ -16854,7 +16992,7 @@ repeat-element@^1.1.2:
|
||||
resolved "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
|
||||
integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==
|
||||
|
||||
repeat-string@^1.6.1:
|
||||
repeat-string@^1.5.4, repeat-string@^1.6.1:
|
||||
version "1.6.1"
|
||||
resolved "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
|
||||
integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
|
||||
@@ -16866,6 +17004,11 @@ repeating@^2.0.0:
|
||||
dependencies:
|
||||
is-finite "^1.0.0"
|
||||
|
||||
replace-ext@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
|
||||
integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=
|
||||
|
||||
replace-in-file@^5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.npmjs.org/replace-in-file/-/replace-in-file-5.0.2.tgz#bd26203b66dfb5b8112ae36a2d2cf928ea4cfe12"
|
||||
@@ -16924,7 +17067,7 @@ request@^2.85.0, request@^2.87.0, request@^2.88.0:
|
||||
tunnel-agent "^0.6.0"
|
||||
uuid "^3.3.2"
|
||||
|
||||
request@cypress-io/request#b5af0d1fa47eec97ba980cde90a13e69a2afcd16:
|
||||
"request@github:cypress-io/request#b5af0d1fa47eec97ba980cde90a13e69a2afcd16":
|
||||
version "2.88.1"
|
||||
resolved "https://codeload.github.com/cypress-io/request/tar.gz/b5af0d1fa47eec97ba980cde90a13e69a2afcd16"
|
||||
dependencies:
|
||||
@@ -18097,6 +18240,11 @@ start-server-and-test@^1.10.11:
|
||||
ps-tree "1.2.0"
|
||||
wait-on "4.0.0"
|
||||
|
||||
state-toggle@^1.0.0:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe"
|
||||
integrity sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==
|
||||
|
||||
static-extend@^0.1.1:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
|
||||
@@ -18939,6 +19087,21 @@ trim-off-newlines@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3"
|
||||
integrity sha1-n5up2e+odkw4dpi8v+sshI8RrbM=
|
||||
|
||||
trim-trailing-lines@^1.0.0:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz#7f0739881ff76657b7776e10874128004b625a94"
|
||||
integrity sha512-4ku0mmjXifQcTVfYDfR5lpgV7zVqPg6zV9rdZmwOPqq0+Zq19xDqEgagqVbc4pOOShbncuAOIs59R3+3gcF3ZA==
|
||||
|
||||
trim@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd"
|
||||
integrity sha1-WFhUf2spB1fulczMZm+1AITEYN0=
|
||||
|
||||
trough@^1.0.0:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
|
||||
integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==
|
||||
|
||||
ts-dedent@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/ts-dedent/-/ts-dedent-1.1.1.tgz#68fad040d7dbd53a90f545b450702340e17d18f3"
|
||||
@@ -19156,6 +19319,14 @@ unfetch@^4.1.0:
|
||||
resolved "https://registry.npmjs.org/unfetch/-/unfetch-4.1.0.tgz#6ec2dd0de887e58a4dee83a050ded80ffc4137db"
|
||||
integrity sha512-crP/n3eAPUJxZXM9T80/yv0YhkTEx2K1D3h7D1AJM6fzsWZrxdyRuLN0JH/dkZh1LNH8LxCnBzoPFCPbb2iGpg==
|
||||
|
||||
unherit@^1.0.4:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22"
|
||||
integrity sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==
|
||||
dependencies:
|
||||
inherits "^2.0.0"
|
||||
xtend "^4.0.0"
|
||||
|
||||
unicode-canonical-property-names-ecmascript@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
|
||||
@@ -19179,6 +19350,18 @@ unicode-property-aliases-ecmascript@^1.0.4:
|
||||
resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4"
|
||||
integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==
|
||||
|
||||
unified@^6.1.5:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.npmjs.org/unified/-/unified-6.2.0.tgz#7fbd630f719126d67d40c644b7e3f617035f6dba"
|
||||
integrity sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA==
|
||||
dependencies:
|
||||
bail "^1.0.0"
|
||||
extend "^3.0.0"
|
||||
is-plain-obj "^1.1.0"
|
||||
trough "^1.0.0"
|
||||
vfile "^2.0.0"
|
||||
x-is-string "^0.1.0"
|
||||
|
||||
union-value@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
|
||||
@@ -19227,6 +19410,42 @@ unique-string@^2.0.0:
|
||||
dependencies:
|
||||
crypto-random-string "^2.0.0"
|
||||
|
||||
unist-util-is@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz#d9e84381c2468e82629e4a5be9d7d05a2dd324cd"
|
||||
integrity sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==
|
||||
|
||||
unist-util-remove-position@^1.0.0:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.4.tgz#ec037348b6102c897703eee6d0294ca4755a2020"
|
||||
integrity sha512-tLqd653ArxJIPnKII6LMZwH+mb5q+n/GtXQZo6S6csPRs5zB0u79Yw8ouR3wTw8wxvdJFhpP6Y7jorWdCgLO0A==
|
||||
dependencies:
|
||||
unist-util-visit "^1.1.0"
|
||||
|
||||
unist-util-stringify-position@^1.0.0, unist-util-stringify-position@^1.1.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz#3f37fcf351279dcbca7480ab5889bb8a832ee1c6"
|
||||
integrity sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==
|
||||
|
||||
unist-util-visit-parents@1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-1.1.2.tgz#f6e3afee8bdbf961c0e6f028ea3c0480028c3d06"
|
||||
integrity sha512-yvo+MMLjEwdc3RhhPYSximset7rwjMrdt9E41Smmvg25UQIenzrN83cRnF1JMzoMi9zZOQeYXHSDf7p+IQkW3Q==
|
||||
|
||||
unist-util-visit-parents@^2.0.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz#25e43e55312166f3348cae6743588781d112c1e9"
|
||||
integrity sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==
|
||||
dependencies:
|
||||
unist-util-is "^3.0.0"
|
||||
|
||||
unist-util-visit@^1.1.0, unist-util-visit@^1.3.0:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz#4724aaa8486e6ee6e26d7ff3c8685960d560b1e3"
|
||||
integrity sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==
|
||||
dependencies:
|
||||
unist-util-visit-parents "^2.0.0"
|
||||
|
||||
universal-user-agent@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.1.tgz#fd8d6cb773a679a709e967ef8288a31fcc03e557"
|
||||
@@ -19489,6 +19708,28 @@ verror@1.10.0:
|
||||
core-util-is "1.0.2"
|
||||
extsprintf "^1.2.0"
|
||||
|
||||
vfile-location@^2.0.0:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.6.tgz#8a274f39411b8719ea5728802e10d9e0dff1519e"
|
||||
integrity sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==
|
||||
|
||||
vfile-message@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-1.1.1.tgz#5833ae078a1dfa2d96e9647886cd32993ab313e1"
|
||||
integrity sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==
|
||||
dependencies:
|
||||
unist-util-stringify-position "^1.1.1"
|
||||
|
||||
vfile@^2.0.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz#e62d8e72b20e83c324bc6c67278ee272488bf84a"
|
||||
integrity sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w==
|
||||
dependencies:
|
||||
is-buffer "^1.1.4"
|
||||
replace-ext "1.0.0"
|
||||
unist-util-stringify-position "^1.0.0"
|
||||
vfile-message "^1.0.0"
|
||||
|
||||
vm-browserify@^1.0.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
|
||||
@@ -19712,7 +19953,7 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5:
|
||||
dependencies:
|
||||
iconv-lite "0.4.24"
|
||||
|
||||
whatwg-fetch@>=0.10.0, whatwg-fetch@^3.0.0:
|
||||
whatwg-fetch@3.0.0, whatwg-fetch@>=0.10.0, whatwg-fetch@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
|
||||
integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==
|
||||
@@ -20076,6 +20317,11 @@ ws@^7.0.0:
|
||||
resolved "https://registry.npmjs.org/ws/-/ws-7.2.3.tgz#a5411e1fb04d5ed0efee76d26d5c46d830c39b46"
|
||||
integrity sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==
|
||||
|
||||
x-is-string@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz#474b50865af3a49a9c4657f05acd145458f77d82"
|
||||
integrity sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=
|
||||
|
||||
xdg-basedir@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
|
||||
@@ -20103,7 +20349,7 @@ xregexp@^4.3.0:
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.8.3"
|
||||
|
||||
xtend@^4.0.0, xtend@~4.0.1:
|
||||
xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
|
||||
Reference in New Issue
Block a user