@backstage-community/plugin-lighthouse 0.4.20 → 0.4.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/Router.esm.js +26 -0
  3. package/dist/Router.esm.js.map +1 -0
  4. package/dist/api.esm.js +8 -0
  5. package/dist/api.esm.js.map +1 -0
  6. package/dist/components/AuditList/AuditListForEntity.esm.js +43 -0
  7. package/dist/components/AuditList/AuditListForEntity.esm.js.map +1 -0
  8. package/dist/components/AuditList/AuditListTable.esm.js +92 -0
  9. package/dist/components/AuditList/AuditListTable.esm.js.map +1 -0
  10. package/dist/components/AuditList/index.esm.js +83 -0
  11. package/dist/components/AuditList/index.esm.js.map +1 -0
  12. package/dist/components/AuditStatusIcon/index.esm.js +11 -0
  13. package/dist/components/AuditStatusIcon/index.esm.js.map +1 -0
  14. package/dist/components/AuditView/index.esm.js +135 -0
  15. package/dist/components/AuditView/index.esm.js.map +1 -0
  16. package/dist/components/Cards/LastLighthouseAuditCard.esm.js +57 -0
  17. package/dist/components/Cards/LastLighthouseAuditCard.esm.js.map +1 -0
  18. package/dist/components/Cards/index.esm.js +2 -0
  19. package/dist/components/Cards/index.esm.js.map +1 -0
  20. package/dist/components/CreateAudit/index.esm.js +152 -0
  21. package/dist/components/CreateAudit/index.esm.js.map +1 -0
  22. package/dist/components/Intro/index.esm.js +130 -0
  23. package/dist/components/Intro/index.esm.js.map +1 -0
  24. package/dist/components/SupportButton/index.esm.js +9 -0
  25. package/dist/components/SupportButton/index.esm.js.map +1 -0
  26. package/dist/constants.esm.js +4 -0
  27. package/dist/constants.esm.js.map +1 -0
  28. package/dist/hooks/useWebsiteForEntity.esm.js +23 -0
  29. package/dist/hooks/useWebsiteForEntity.esm.js.map +1 -0
  30. package/dist/index.esm.js +4 -770
  31. package/dist/index.esm.js.map +1 -1
  32. package/dist/plugin.esm.js +55 -0
  33. package/dist/plugin.esm.js.map +1 -0
  34. package/dist/utils.esm.js +44 -0
  35. package/dist/utils.esm.js.map +1 -0
  36. package/package.json +17 -11
  37. package/dist/esm/index-DTZMI_cR.esm.js +0 -26
  38. package/dist/esm/index-DTZMI_cR.esm.js.map +0 -1
package/dist/index.esm.js CHANGED
@@ -1,771 +1,5 @@
1
- import { LighthouseRestApi } from '@backstage-community/plugin-lighthouse-common';
2
- import { createApiRef, createRouteRef, createPlugin, createApiFactory, configApiRef, createRoutableExtension, createComponentExtension, useApi, useRouteRef, errorApiRef } from '@backstage/core-plugin-api';
3
- import React, { useState, useEffect, useMemo, useCallback } from 'react';
4
- import { useLocation, generatePath, useNavigate, useParams, resolvePath, Link as Link$1, Routes, Route } from 'react-router-dom';
5
- import { useEntity, MissingAnnotationEmptyState } from '@backstage/plugin-catalog-react';
6
- import Button from '@material-ui/core/Button';
7
- import Grid from '@material-ui/core/Grid';
8
- import Pagination from '@material-ui/lab/Pagination';
9
- import useAsync from 'react-use/esm/useAsync';
10
- import useLocalStorage from 'react-use/esm/useLocalStorage';
11
- import Tab from '@material-ui/core/Tab';
12
- import Tabs from '@material-ui/core/Tabs';
13
- import { makeStyles } from '@material-ui/core/styles';
14
- import CloseIcon from '@material-ui/icons/Close';
15
- import { SupportButton, ContentHeader, InfoCard, Link, MarkdownContent, StatusError, StatusOK, StatusPending, TrendLine, Table, WarningPanel, Page, Header, HeaderLabel, Content, Progress, EmptyState, StructuredMetadataTable, StatusWarning } from '@backstage/core-components';
16
- import Typography from '@material-ui/core/Typography';
17
- import useInterval from 'react-use/esm/useInterval';
18
- import List from '@material-ui/core/List';
19
- import ListItem from '@material-ui/core/ListItem';
20
- import ListItemIcon from '@material-ui/core/ListItemIcon';
21
- import ListItemText from '@material-ui/core/ListItemText';
22
- import Alert from '@material-ui/lab/Alert';
23
- import MenuItem from '@material-ui/core/MenuItem';
24
- import TextField from '@material-ui/core/TextField';
25
-
26
- const lighthouseApiRef = createApiRef({
27
- id: "plugin.lighthouse.service"
28
- });
29
-
30
- const rootRouteRef = createRouteRef({
31
- id: "lighthouse"
32
- });
33
- createRouteRef({
34
- id: "lighthouse:audit"
35
- });
36
- const createAuditRouteRef = createRouteRef({
37
- id: "lighthouse:create-audit"
38
- });
39
- const entityContentRouteRef = createRouteRef({
40
- id: "lighthouse:entity-content"
41
- });
42
- const lighthousePlugin = createPlugin({
43
- id: "lighthouse",
44
- apis: [
45
- createApiFactory({
46
- api: lighthouseApiRef,
47
- deps: { configApi: configApiRef },
48
- factory: ({ configApi }) => LighthouseRestApi.fromConfig(configApi)
49
- })
50
- ],
51
- routes: {
52
- root: createAuditRouteRef,
53
- entityContent: entityContentRouteRef
54
- }
55
- });
56
- const LighthousePage = lighthousePlugin.provide(
57
- createRoutableExtension({
58
- name: "LighthousePage",
59
- component: () => Promise.resolve().then(function () { return Router$1; }).then((m) => m.Router),
60
- mountPoint: rootRouteRef
61
- })
62
- );
63
- const EntityLighthouseContent = lighthousePlugin.provide(
64
- createRoutableExtension({
65
- name: "EntityLighthouseContent",
66
- component: () => Promise.resolve().then(function () { return Router$1; }).then((m) => m.EmbeddedRouter),
67
- mountPoint: entityContentRouteRef
68
- })
69
- );
70
- const EntityLastLighthouseAuditCard = lighthousePlugin.provide(
71
- createComponentExtension({
72
- name: "EntityLastLighthouseAuditCard",
73
- component: {
74
- lazy: () => import('./esm/index-DTZMI_cR.esm.js').then((m) => m.LastLighthouseAuditCard)
75
- }
76
- })
77
- );
78
-
79
- function useQuery() {
80
- return new URLSearchParams(useLocation().search);
81
- }
82
- function formatTime(timestamp) {
83
- let date;
84
- if (timestamp instanceof Date) {
85
- date = timestamp;
86
- } else {
87
- date = new Date(timestamp);
88
- }
89
- return date.toUTCString();
90
- }
91
- const CATEGORIES = [
92
- "accessibility",
93
- "performance",
94
- "seo",
95
- "best-practices"
96
- ];
97
- const CATEGORY_LABELS = {
98
- accessibility: "Accessibility",
99
- performance: "Performance",
100
- seo: "SEO",
101
- "best-practices": "Best Practices",
102
- pwa: "Progressive Web App"
103
- };
104
- function buildSparklinesDataForItem(item) {
105
- return item.audits.filter(
106
- (audit) => audit.status === "COMPLETED"
107
- ).reduce((scores, audit) => {
108
- Object.values(audit.categories).forEach((category) => {
109
- scores[category.id] = scores[category.id] || [];
110
- scores[category.id].unshift(category.score);
111
- });
112
- Object.values(scores).forEach((arr) => {
113
- if (arr.length === 1)
114
- arr.push(arr[0]);
115
- });
116
- return scores;
117
- }, {});
118
- }
119
-
120
- function LighthouseSupportButton() {
121
- return /* @__PURE__ */ React.createElement(SupportButton, null, "Lighthouse audits run for any web domain, stored over time.");
122
- }
123
-
124
- const LIGHTHOUSE_INTRO_LOCAL_STORAGE = "@backstage/lighthouse-plugin/intro-dismissed";
125
- const USE_CASES = `
126
- Google's [Lighthouse](https://developers.google.com/web/tools/lighthouse) auditing tool for websites
127
- is a great open-source resource for benchmarking and improving the accessibility, performance, SEO, and best practices of your site.
128
- At Spotify, we keep track of Lighthouse audit scores over time to look at trends and overall areas for investment.
129
-
130
- This plugin allows you to generate on-demand Lighthouse audits for websites, and to track the trends for the
131
- top-level categories of Lighthouse at a glance.
132
-
133
- In the future, we hope to add support for scheduling audits (which we do internally), as well as allowing
134
- custom runs of Lighthouse to be ingested (for auditing sites that require authentication or some session state).
135
- `;
136
- const SETUP = `
137
- To get started, you will need a running instance of [lighthouse-audit-service](https://github.com/spotify/lighthouse-audit-service).
138
- _It's likely you will need to enable CORS when running lighthouse-audit-service. Initialize the app
139
- with the environment variable \`LAS_CORS\` set to \`true\`._
140
-
141
- When you have an instance running that Backstage can hook into, first install the plugin into your app:
142
-
143
- \`\`\`sh
144
- # From your Backstage root directory
145
- yarn --cwd packages/app add @backstage-community/plugin-lighthouse
146
- \`\`\`
147
-
148
- Modify your app routes in \`App.tsx\` to include the \`LighthousePage\` component exported from the plugin, for example:
149
-
150
- \`\`\`tsx
151
- // At the top imports
152
- import { LighthousePage } from '@backstage-community/plugin-lighthouse';
153
-
154
- <FlatRoutes>
155
- // ...
156
- <Route path="/lighthouse" element={<LighthousePage />} />
157
- // ...
158
- </FlatRoutes>;
159
- \`\`\`
160
-
161
- Then configure the \`lighthouse-audit-service\` URL in your [\`app-config.yaml\`](https://github.com/backstage/backstage/blob/master/app-config.yaml).
162
-
163
- \`\`\`yaml
164
- lighthouse:
165
- baseUrl: http://your-service-url
166
- \`\`\`
167
- `;
168
- const useStyles$2 = makeStyles((theme) => ({
169
- tabs: { marginBottom: -18 },
170
- tab: { minWidth: 72, paddingLeft: 1, paddingRight: 1 },
171
- content: { marginBottom: theme.spacing(2) },
172
- closeButtonContainer: { height: "100%" },
173
- closeButtonItem: { paddingBottom: 0 }
174
- }));
175
- function GettingStartedCard() {
176
- const classes = useStyles$2();
177
- const [value, setValue] = useState(0);
178
- return /* @__PURE__ */ React.createElement(
179
- InfoCard,
180
- {
181
- title: "Get started",
182
- subheader: /* @__PURE__ */ React.createElement(
183
- Tabs,
184
- {
185
- value,
186
- indicatorColor: "primary",
187
- textColor: "primary",
188
- onChange: (_ev, newValue) => setValue(newValue),
189
- "aria-label": "get started tabs",
190
- className: classes.tabs
191
- },
192
- /* @__PURE__ */ React.createElement(Tab, { className: classes.tab, label: "Use cases" }),
193
- /* @__PURE__ */ React.createElement(Tab, { className: classes.tab, label: "Setup" })
194
- ),
195
- divider: true,
196
- actions: /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Grid, { container: true, direction: "row", justifyContent: "flex-end" }, /* @__PURE__ */ React.createElement(Grid, { item: true }, /* @__PURE__ */ React.createElement(
197
- Button,
198
- {
199
- component: Link,
200
- to: "https://github.com/spotify/lighthouse-audit-service",
201
- size: "small"
202
- },
203
- "Check out the README"
204
- ))))
205
- },
206
- value === 0 && /* @__PURE__ */ React.createElement(MarkdownContent, { content: USE_CASES }),
207
- value === 1 && /* @__PURE__ */ React.createElement(MarkdownContent, { content: SETUP })
208
- );
209
- }
210
- function LighthouseIntro({ onDismiss = () => {
211
- } }) {
212
- const classes = useStyles$2();
213
- const [dismissed, setDismissed] = useLocalStorage(
214
- LIGHTHOUSE_INTRO_LOCAL_STORAGE,
215
- false
216
- );
217
- if (dismissed)
218
- return null;
219
- return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(ContentHeader, { title: "Welcome to Lighthouse in Backstage!" }, /* @__PURE__ */ React.createElement(LighthouseSupportButton, null)), /* @__PURE__ */ React.createElement(Grid, { className: classes.content, container: true, spacing: 3, direction: "row" }, /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12, sm: 6, md: 4 }, /* @__PURE__ */ React.createElement(GettingStartedCard, null)), /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12, sm: 6, md: 8 }, /* @__PURE__ */ React.createElement(
220
- Grid,
221
- {
222
- container: true,
223
- justifyContent: "flex-end",
224
- alignItems: "flex-end",
225
- className: classes.closeButtonContainer
226
- },
227
- /* @__PURE__ */ React.createElement(Grid, { item: true, className: classes.closeButtonItem }, /* @__PURE__ */ React.createElement(
228
- Button,
229
- {
230
- variant: "text",
231
- onClick: () => {
232
- onDismiss();
233
- setDismissed(true);
234
- }
235
- },
236
- /* @__PURE__ */ React.createElement(CloseIcon, null),
237
- " Hide intro"
238
- ))
239
- ))));
240
- }
241
-
242
- const AuditStatusIcon = ({ audit }) => {
243
- if (audit.status === "FAILED")
244
- return /* @__PURE__ */ React.createElement(StatusError, null);
245
- if (audit.status === "COMPLETED")
246
- return /* @__PURE__ */ React.createElement(StatusOK, null);
247
- return /* @__PURE__ */ React.createElement(StatusPending, null);
248
- };
249
-
250
- const columns = [
251
- {
252
- title: "Website URL",
253
- field: "websiteUrl"
254
- },
255
- ...CATEGORIES.map((category) => ({
256
- title: CATEGORY_LABELS[category],
257
- field: category
258
- })),
259
- {
260
- title: "Last Report",
261
- field: "lastReport",
262
- cellStyle: {
263
- whiteSpace: "nowrap"
264
- }
265
- },
266
- {
267
- title: "Last Audit Triggered",
268
- field: "lastAuditTriggered",
269
- cellStyle: {
270
- minWidth: 120
271
- }
272
- }
273
- ];
274
- const AuditListTable = ({ items }) => {
275
- const [websiteState, setWebsiteState] = useState(items);
276
- const lighthouseApi = useApi(lighthouseApiRef);
277
- useEffect(() => {
278
- setWebsiteState(items);
279
- }, [items]);
280
- const runRefresh = (websites) => {
281
- websites.forEach(async (website) => {
282
- const response = await lighthouseApi.getWebsiteForAuditId(
283
- website.lastAudit.id
284
- );
285
- const auditStatus = response.lastAudit.status;
286
- if (auditStatus === "COMPLETED" || auditStatus === "FAILED") {
287
- const newWebsiteData = websiteState.slice(0);
288
- newWebsiteData[newWebsiteData.findIndex((w) => w.url === response.url)] = response;
289
- setWebsiteState(newWebsiteData);
290
- }
291
- });
292
- };
293
- const runningWebsiteAudits = websiteState ? websiteState.filter((website) => website.lastAudit.status === "RUNNING") : [];
294
- useInterval(
295
- () => runRefresh(runningWebsiteAudits),
296
- runningWebsiteAudits.length > 0 ? 5e3 : null
297
- );
298
- const data = websiteState.map((website) => {
299
- const trendlineData = buildSparklinesDataForItem(website);
300
- const trendlines = {};
301
- CATEGORIES.forEach((category) => {
302
- trendlines[category] = /* @__PURE__ */ React.createElement(
303
- TrendLine,
304
- {
305
- title: `trendline for ${CATEGORY_LABELS[category]} category of ${website.url}`,
306
- data: trendlineData[category] || []
307
- }
308
- );
309
- });
310
- return {
311
- websiteUrl: /* @__PURE__ */ React.createElement(Link, { to: generatePath("audit/:id", { id: website.lastAudit.id }) }, website.url),
312
- ...trendlines,
313
- lastReport: /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(AuditStatusIcon, { audit: website.lastAudit }), " ", /* @__PURE__ */ React.createElement(Typography, { component: "span" }, website.lastAudit.status.toLocaleUpperCase("en-US"))),
314
- lastAuditTriggered: formatTime(website.lastAudit.timeCreated)
315
- };
316
- });
317
- return /* @__PURE__ */ React.createElement(
318
- Table,
319
- {
320
- options: {
321
- paging: false,
322
- toolbar: false
323
- },
324
- columns,
325
- data
326
- }
327
- );
328
- };
329
-
330
- const LIMIT = 10;
331
- const AuditList = () => {
332
- const [dismissedStored] = useLocalStorage(
333
- LIGHTHOUSE_INTRO_LOCAL_STORAGE
334
- );
335
- const [dismissed, setDismissed] = useState(dismissedStored);
336
- const query = useQuery();
337
- const page = query.get("page") ? parseInt(query.get("page"), 10) || 1 : 1;
338
- const lighthouseApi = useApi(lighthouseApiRef);
339
- const { value, loading, error } = useAsync(
340
- async () => await lighthouseApi.getWebsiteList({
341
- limit: LIMIT,
342
- offset: (page - 1) * LIMIT
343
- }),
344
- [page]
345
- );
346
- const pageCount = useMemo(() => {
347
- if ((value == null ? void 0 : value.total) && (value == null ? void 0 : value.limit))
348
- return Math.ceil((value == null ? void 0 : value.total) / (value == null ? void 0 : value.limit));
349
- return 0;
350
- }, [value == null ? void 0 : value.total, value == null ? void 0 : value.limit]);
351
- const navigate = useNavigate();
352
- let content = null;
353
- if (value) {
354
- content = /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(AuditListTable, { items: (value == null ? void 0 : value.items) || [] }), pageCount > 1 && /* @__PURE__ */ React.createElement(
355
- Pagination,
356
- {
357
- page,
358
- count: pageCount,
359
- onChange: (_event, newPage) => {
360
- navigate(`?page=${newPage}`);
361
- }
362
- }
363
- ));
364
- } else if (loading) {
365
- content = /* @__PURE__ */ React.createElement(Progress, null);
366
- } else if (error) {
367
- content = /* @__PURE__ */ React.createElement(WarningPanel, { severity: "error", title: "Could not load audit list." }, error.message);
368
- }
369
- return /* @__PURE__ */ React.createElement(Page, { themeId: "tool" }, /* @__PURE__ */ React.createElement(
370
- Header,
371
- {
372
- title: "Lighthouse",
373
- subtitle: "Website audits powered by Lighthouse"
374
- },
375
- /* @__PURE__ */ React.createElement(HeaderLabel, { label: "Owner", value: "Spotify" }),
376
- /* @__PURE__ */ React.createElement(HeaderLabel, { label: "Lifecycle", value: "Alpha" })
377
- ), /* @__PURE__ */ React.createElement(Content, null, /* @__PURE__ */ React.createElement(LighthouseIntro, { onDismiss: () => setDismissed(true) }), /* @__PURE__ */ React.createElement(
378
- ContentHeader,
379
- {
380
- title: "Audits",
381
- description: "View all audits run for your website through Backstage here. Track the trend of your most recent audits."
382
- },
383
- /* @__PURE__ */ React.createElement(
384
- Button,
385
- {
386
- variant: "contained",
387
- color: "primary",
388
- onClick: () => navigate("create-audit")
389
- },
390
- "Create Audit"
391
- ),
392
- dismissed && /* @__PURE__ */ React.createElement(LighthouseSupportButton, null)
393
- ), /* @__PURE__ */ React.createElement(Grid, { container: true, spacing: 3, direction: "column" }, /* @__PURE__ */ React.createElement(Grid, { item: true }, /* @__PURE__ */ React.createElement(InfoCard, { noPadding: true }, content)))));
394
- };
395
-
396
- const useStyles$1 = makeStyles({
397
- contentGrid: {
398
- height: "100%"
399
- },
400
- iframe: {
401
- border: "0",
402
- width: "100%",
403
- height: "100%"
404
- },
405
- errorOutput: { whiteSpace: "pre" }
406
- });
407
- const AuditLinkList = ({ audits = [], selectedId }) => {
408
- var _a, _b;
409
- const fromPath = (_b = (_a = useRouteRef(rootRouteRef)) == null ? void 0 : _a()) != null ? _b : "../";
410
- return /* @__PURE__ */ React.createElement(
411
- List,
412
- {
413
- "data-testid": "audit-sidebar",
414
- component: "nav",
415
- "aria-label": "lighthouse audit history"
416
- },
417
- audits.map((audit) => /* @__PURE__ */ React.createElement(
418
- ListItem,
419
- {
420
- key: audit.id,
421
- selected: audit.id === selectedId,
422
- button: true,
423
- component: Link$1,
424
- replace: true,
425
- to: resolvePath(
426
- generatePath("audit/:id", { id: audit.id }),
427
- fromPath
428
- )
429
- },
430
- /* @__PURE__ */ React.createElement(ListItemIcon, null, /* @__PURE__ */ React.createElement(AuditStatusIcon, { audit })),
431
- /* @__PURE__ */ React.createElement(ListItemText, { primary: formatTime(audit.timeCreated) })
432
- ))
433
- );
434
- };
435
- const AuditView = ({ audit }) => {
436
- const classes = useStyles$1();
437
- const params = useParams();
438
- const { url: lighthouseUrl } = useApi(lighthouseApiRef);
439
- if ((audit == null ? void 0 : audit.status) === "RUNNING")
440
- return /* @__PURE__ */ React.createElement(Progress, null);
441
- if ((audit == null ? void 0 : audit.status) === "FAILED")
442
- return /* @__PURE__ */ React.createElement(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.");
443
- return /* @__PURE__ */ React.createElement(InfoCard, { variant: "fullHeight" }, /* @__PURE__ */ React.createElement(
444
- "iframe",
445
- {
446
- className: classes.iframe,
447
- title: `Lighthouse audit${(audit == null ? void 0 : audit.url) ? ` for ${audit.url}` : ""}`,
448
- src: `${lighthouseUrl}/v1/audits/${encodeURIComponent(params.id)}`
449
- }
450
- ));
451
- };
452
- const AuditViewContent = () => {
453
- var _a, _b;
454
- const lighthouseApi = useApi(lighthouseApiRef);
455
- const fromPath = (_b = (_a = useRouteRef(rootRouteRef)) == null ? void 0 : _a()) != null ? _b : "../";
456
- const params = useParams();
457
- const classes = useStyles$1();
458
- const navigate = useNavigate();
459
- const {
460
- loading,
461
- error,
462
- value: nextValue
463
- } = useAsync(
464
- async () => await lighthouseApi.getWebsiteForAuditId(params.id),
465
- [params.id]
466
- );
467
- const [value, setValue] = useState();
468
- useEffect(() => {
469
- if (!!nextValue && nextValue.url !== (value == null ? void 0 : value.url)) {
470
- setValue(nextValue);
471
- }
472
- }, [value, nextValue, setValue]);
473
- let content = null;
474
- if (value) {
475
- content = /* @__PURE__ */ React.createElement(Grid, { container: true, alignItems: "stretch", className: classes.contentGrid }, /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 3 }, /* @__PURE__ */ React.createElement(AuditLinkList, { audits: value == null ? void 0 : value.audits, selectedId: params.id })), /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 9 }, /* @__PURE__ */ React.createElement(AuditView, { audit: value == null ? void 0 : value.audits.find((a) => a.id === params.id) })));
476
- } else if (loading) {
477
- content = /* @__PURE__ */ React.createElement(Progress, null);
478
- } else if (error) {
479
- content = /* @__PURE__ */ React.createElement(
480
- Alert,
481
- {
482
- "data-testid": "error-message",
483
- severity: "error",
484
- className: classes.errorOutput
485
- },
486
- error.message
487
- );
488
- }
489
- let createAuditButtonUrl = "create-audit";
490
- if (value == null ? void 0 : value.url) {
491
- createAuditButtonUrl += `?url=${encodeURIComponent(value.url)}`;
492
- }
493
- return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
494
- ContentHeader,
495
- {
496
- title: (value == null ? void 0 : value.url) || "Audit",
497
- description: "See a history of all Lighthouse audits for your website run through Backstage."
498
- },
499
- /* @__PURE__ */ React.createElement(
500
- Button,
501
- {
502
- variant: "contained",
503
- color: "primary",
504
- onClick: () => navigate(resolvePath(createAuditButtonUrl, fromPath))
505
- },
506
- "Create New Audit"
507
- ),
508
- /* @__PURE__ */ React.createElement(LighthouseSupportButton, null)
509
- ), content);
510
- };
511
- const ConnectedAuditView = () => /* @__PURE__ */ React.createElement(Page, { themeId: "tool" }, /* @__PURE__ */ React.createElement(Header, { title: "Lighthouse", subtitle: "Website audits powered by Lighthouse" }, /* @__PURE__ */ React.createElement(HeaderLabel, { label: "Owner", value: "Spotify" }), /* @__PURE__ */ React.createElement(HeaderLabel, { label: "Lifecycle", value: "Alpha" })), /* @__PURE__ */ React.createElement(Content, { stretch: true }, /* @__PURE__ */ React.createElement(AuditViewContent, null)));
512
-
513
- const useStyles = makeStyles((theme) => ({
514
- input: {
515
- minWidth: 300,
516
- [theme.breakpoints.down("xs")]: {
517
- minWidth: "100%"
518
- }
519
- },
520
- buttonList: {
521
- marginLeft: theme.spacing(-1),
522
- marginRight: theme.spacing(-1),
523
- "& > *": {
524
- margin: theme.spacing(1)
525
- },
526
- [theme.breakpoints.down("xs")]: {
527
- marginLeft: 0,
528
- marginRight: 0,
529
- flexDirection: "column",
530
- "& > *": {
531
- width: "100%"
532
- }
533
- }
534
- }
535
- }));
536
- const formFactorToScreenEmulationMap = {
537
- // the default is mobile, so no need to override
538
- mobile: void 0,
539
- // Values from lighthouse's cli "desktop" preset
540
- // https://github.com/GoogleChrome/lighthouse/blob/a6738e0033e7e5ca308b97c1c36f298b7d399402/lighthouse-core/config/constants.js#L71-L77
541
- desktop: {
542
- mobile: false,
543
- width: 1350,
544
- height: 940,
545
- deviceScaleFactor: 1,
546
- disabled: false
547
- }
548
- };
549
- const CreateAuditContent = () => {
550
- const errorApi = useApi(errorApiRef);
551
- const lighthouseApi = useApi(lighthouseApiRef);
552
- const classes = useStyles();
553
- const query = useQuery();
554
- const navigate = useNavigate();
555
- const [submitting, setSubmitting] = useState(false);
556
- const [url, setUrl] = useState(query.get("url") || "");
557
- const [formFactor, setFormFactor] = useState("mobile");
558
- const triggerAudit = useCallback(async () => {
559
- setSubmitting(true);
560
- try {
561
- await lighthouseApi.triggerAudit({
562
- url: url.replace(/\/$/, ""),
563
- options: {
564
- lighthouseConfig: {
565
- settings: {
566
- formFactor,
567
- emulatedFormFactor: formFactor,
568
- screenEmulation: formFactorToScreenEmulationMap[formFactor]
569
- }
570
- }
571
- }
572
- });
573
- navigate("..");
574
- } catch (err) {
575
- errorApi.post(err);
576
- } finally {
577
- setSubmitting(false);
578
- }
579
- }, [url, formFactor, lighthouseApi, setSubmitting, errorApi, navigate]);
580
- return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
581
- ContentHeader,
582
- {
583
- title: "Trigger a new audit",
584
- 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."
585
- },
586
- /* @__PURE__ */ React.createElement(LighthouseSupportButton, null)
587
- ), /* @__PURE__ */ React.createElement(Grid, { container: true, direction: "column" }, /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12, sm: 6 }, /* @__PURE__ */ React.createElement(InfoCard, null, /* @__PURE__ */ React.createElement(
588
- "form",
589
- {
590
- onSubmit: (ev) => {
591
- ev.preventDefault();
592
- triggerAudit();
593
- }
594
- },
595
- /* @__PURE__ */ React.createElement(List, null, /* @__PURE__ */ React.createElement(ListItem, null, /* @__PURE__ */ React.createElement(
596
- TextField,
597
- {
598
- name: "lighthouse-create-audit-url-tf",
599
- className: classes.input,
600
- label: "URL",
601
- placeholder: "https://spotify.com",
602
- helperText: "The target URL for Lighthouse to use.",
603
- required: true,
604
- disabled: submitting,
605
- onChange: (ev) => setUrl(ev.target.value),
606
- value: url,
607
- inputProps: { "aria-label": "URL" }
608
- }
609
- )), /* @__PURE__ */ React.createElement(ListItem, null, /* @__PURE__ */ React.createElement(
610
- TextField,
611
- {
612
- name: "lighthouse-create-audit-emulated-form-factor-tf",
613
- className: classes.input,
614
- label: "Emulated Form Factor",
615
- helperText: "Device to simulate when auditing",
616
- select: true,
617
- required: true,
618
- disabled: submitting,
619
- onChange: (ev) => setFormFactor(ev.target.value),
620
- value: formFactor,
621
- inputProps: { "aria-label": "Emulated form factor" }
622
- },
623
- /* @__PURE__ */ React.createElement(MenuItem, { value: "mobile" }, "Mobile"),
624
- /* @__PURE__ */ React.createElement(MenuItem, { value: "desktop" }, "Desktop")
625
- )), /* @__PURE__ */ React.createElement(ListItem, { className: classes.buttonList }, /* @__PURE__ */ React.createElement(
626
- Button,
627
- {
628
- variant: "outlined",
629
- color: "primary",
630
- onClick: () => navigate(".."),
631
- disabled: submitting
632
- },
633
- "Cancel"
634
- ), /* @__PURE__ */ React.createElement(
635
- Button,
636
- {
637
- variant: "contained",
638
- color: "primary",
639
- type: "submit",
640
- disabled: submitting
641
- },
642
- "Create Audit"
643
- )))
644
- )))));
645
- };
646
- const CreateAudit = () => /* @__PURE__ */ React.createElement(Page, { themeId: "tool" }, /* @__PURE__ */ React.createElement(Header, { title: "Lighthouse", subtitle: "Website audits powered by Lighthouse" }, /* @__PURE__ */ React.createElement(HeaderLabel, { label: "Owner", value: "Spotify" }), /* @__PURE__ */ React.createElement(HeaderLabel, { label: "Lifecycle", value: "Alpha" })), /* @__PURE__ */ React.createElement(Content, null, /* @__PURE__ */ React.createElement(CreateAuditContent, null)));
647
-
648
- const LIGHTHOUSE_WEBSITE_URL_ANNOTATION = "lighthouse.com/website-url";
649
-
650
- const useWebsiteForEntity = () => {
651
- var _a, _b;
652
- const { entity } = useEntity();
653
- const websiteUrl = (_b = (_a = entity.metadata.annotations) == null ? void 0 : _a[LIGHTHOUSE_WEBSITE_URL_ANNOTATION]) != null ? _b : "";
654
- const lighthouseApi = useApi(lighthouseApiRef);
655
- const errorApi = useApi(errorApiRef);
656
- const response = useAsync(
657
- () => lighthouseApi.getWebsiteByUrl(websiteUrl),
658
- [websiteUrl]
659
- );
660
- if (response.error && !response.error.message.includes("no audited website found for url")) {
661
- errorApi.post(response.error);
662
- }
663
- return response;
664
- };
665
-
666
- const AuditListForEntity = () => {
667
- var _a, _b, _c, _d;
668
- const { value, loading, error } = useWebsiteForEntity();
669
- const { entity } = useEntity();
670
- const navigate = useNavigate();
671
- const fromPath = (_b = (_a = useRouteRef(rootRouteRef)) == null ? void 0 : _a()) != null ? _b : "../";
672
- let createAuditButtonUrl = "create-audit";
673
- const websiteUrl = (_d = (_c = entity.metadata.annotations) == null ? void 0 : _c[LIGHTHOUSE_WEBSITE_URL_ANNOTATION]) != null ? _d : "";
674
- if (websiteUrl) {
675
- createAuditButtonUrl += `?url=${encodeURIComponent(websiteUrl)}`;
676
- }
677
- let content = null;
678
- content = /* @__PURE__ */ React.createElement(Content, null, /* @__PURE__ */ React.createElement(ContentHeader, { title: "Latest Audit" }, /* @__PURE__ */ React.createElement(
679
- Button,
680
- {
681
- variant: "contained",
682
- color: "primary",
683
- onClick: () => navigate(resolvePath(createAuditButtonUrl, fromPath))
684
- },
685
- "Create New Audit"
686
- ), /* @__PURE__ */ React.createElement(LighthouseSupportButton, null)), /* @__PURE__ */ React.createElement(AuditListTable, { items: value ? [value] : [] }));
687
- if (loading) {
688
- content = /* @__PURE__ */ React.createElement(Progress, null);
689
- }
690
- if (error && !error.message.includes("no audited website found for url")) {
691
- content = /* @__PURE__ */ React.createElement(WarningPanel, { severity: "error", title: "Could not load audit list." }, error.message);
692
- }
693
- return /* @__PURE__ */ React.createElement(InfoCard, { noPadding: true }, content);
694
- };
695
-
696
- const isLighthouseAvailable = (entity) => {
697
- var _a;
698
- return Boolean((_a = entity.metadata.annotations) == null ? void 0 : _a[LIGHTHOUSE_WEBSITE_URL_ANNOTATION]);
699
- };
700
- const Router = () => /* @__PURE__ */ React.createElement(Routes, null, /* @__PURE__ */ React.createElement(Route, { path: "/", element: /* @__PURE__ */ React.createElement(AuditList, null) }), /* @__PURE__ */ React.createElement(Route, { path: "/audit/:id", element: /* @__PURE__ */ React.createElement(ConnectedAuditView, null) }), /* @__PURE__ */ React.createElement(Route, { path: "/create-audit", element: /* @__PURE__ */ React.createElement(CreateAudit, null) }));
701
- const EmbeddedRouter = () => {
702
- const { entity } = useEntity();
703
- if (!isLighthouseAvailable(entity)) {
704
- return /* @__PURE__ */ React.createElement(
705
- MissingAnnotationEmptyState,
706
- {
707
- annotation: LIGHTHOUSE_WEBSITE_URL_ANNOTATION
708
- }
709
- );
710
- }
711
- return /* @__PURE__ */ React.createElement(Routes, null, /* @__PURE__ */ React.createElement(Route, { path: "/", element: /* @__PURE__ */ React.createElement(AuditListForEntity, null) }), /* @__PURE__ */ React.createElement(Route, { path: "/audit/:id", element: /* @__PURE__ */ React.createElement(AuditViewContent, null) }), /* @__PURE__ */ React.createElement(Route, { path: "/create-audit", element: /* @__PURE__ */ React.createElement(CreateAuditContent, null) }));
712
- };
713
-
714
- var Router$1 = /*#__PURE__*/Object.freeze({
715
- __proto__: null,
716
- EmbeddedRouter: EmbeddedRouter,
717
- Router: Router,
718
- isLighthouseAvailable: isLighthouseAvailable
719
- });
720
-
721
- const LighthouseCategoryScoreStatus = (props) => {
722
- const scoreAsPercentage = Math.round(props.score * 100);
723
- switch (true) {
724
- case scoreAsPercentage >= 90:
725
- return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(StatusOK, null), scoreAsPercentage, "%");
726
- case (scoreAsPercentage >= 50 && scoreAsPercentage < 90):
727
- return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(StatusWarning, null), scoreAsPercentage, "%");
728
- case scoreAsPercentage < 50:
729
- return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(StatusError, null), scoreAsPercentage, "%");
730
- default:
731
- return /* @__PURE__ */ React.createElement(Typography, { component: "span" }, "N/A");
732
- }
733
- };
734
- const LighthouseAuditStatus = (props) => /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(AuditStatusIcon, { audit: props.audit }), props.audit.status.toLocaleUpperCase("en-US"));
735
- const LighthouseAuditSummary = (props) => {
736
- const { audit, dense = false } = props;
737
- const { url } = audit;
738
- const flattenedCategoryData = {};
739
- if (audit.status === "COMPLETED") {
740
- const categories = audit.categories;
741
- const categoryIds = Object.keys(categories);
742
- categoryIds.forEach((id) => {
743
- const { title, score } = categories[id];
744
- flattenedCategoryData[title] = /* @__PURE__ */ React.createElement(LighthouseCategoryScoreStatus, { score });
745
- });
746
- }
747
- const tableData = {
748
- url,
749
- status: /* @__PURE__ */ React.createElement(LighthouseAuditStatus, { audit }),
750
- ...flattenedCategoryData
751
- };
752
- return /* @__PURE__ */ React.createElement(StructuredMetadataTable, { metadata: tableData, dense });
753
- };
754
- const LastLighthouseAuditCard = (props) => {
755
- const { dense = false, variant } = props;
756
- const { value: website, loading, error } = useWebsiteForEntity();
757
- let content;
758
- if (loading) {
759
- content = /* @__PURE__ */ React.createElement(Progress, null);
760
- }
761
- if (error) {
762
- content = error.message.includes("no audited website found for url") ? /* @__PURE__ */ React.createElement(EmptyState, { title: "No Audits Found", missing: "data" }) : /* @__PURE__ */ React.createElement(WarningPanel, { severity: "error", title: "Could not load audit list." }, error.message);
763
- }
764
- if (website) {
765
- content = /* @__PURE__ */ React.createElement(LighthouseAuditSummary, { audit: website.lastAudit, dense });
766
- }
767
- return /* @__PURE__ */ React.createElement(InfoCard, { title: "Lighthouse Audit", variant }, content);
768
- };
769
-
770
- export { EmbeddedRouter, EntityLastLighthouseAuditCard, EntityLighthouseContent, LastLighthouseAuditCard, LighthousePage, Router, isLighthouseAvailable, isLighthouseAvailable as isPluginApplicableToEntity, lighthouseApiRef, lighthousePlugin, lighthousePlugin as plugin };
1
+ export { EntityLastLighthouseAuditCard, EntityLighthouseContent, LighthousePage, lighthousePlugin, lighthousePlugin as plugin } from './plugin.esm.js';
2
+ export { EmbeddedRouter, Router, isLighthouseAvailable, isLighthouseAvailable as isPluginApplicableToEntity } from './Router.esm.js';
3
+ export { lighthouseApiRef } from './api.esm.js';
4
+ export { LastLighthouseAuditCard } from './components/Cards/LastLighthouseAuditCard.esm.js';
771
5
  //# sourceMappingURL=index.esm.js.map