Merge pull request #15846 from dpfaffenbauer/lighthouse-scheduler

Feature: Introduce Lighthouse Backend with Scheduling of Audits
This commit is contained in:
Ben Lambert
2023-02-01 11:03:38 +01:00
committed by GitHub
43 changed files with 978 additions and 346 deletions
+4
View File
@@ -105,3 +105,7 @@ const overviewContent = (
</EntitySwitch.Case>
</EntitySwitch>
```
## Schedule
If you want to run automated scheduled runs, you can install the [@backstage/lighthouse-backend](../lighthouse-backend/README.md) plugin
+1 -155
View File
@@ -7,50 +7,11 @@
import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { Config } from '@backstage/config';
import { Entity } from '@backstage/catalog-model';
import { InfoCardVariants } from '@backstage/core-components';
import { LighthouseApi } from '@backstage/plugin-lighthouse-common';
import { RouteRef } from '@backstage/core-plugin-api';
// @public (undocumented)
export type Audit = AuditRunning | AuditFailed | AuditCompleted;
// @public (undocumented)
export interface AuditBase {
// (undocumented)
id: string;
// (undocumented)
timeCreated: string;
// (undocumented)
url: string;
}
// @public (undocumented)
export interface AuditCompleted extends AuditBase {
// (undocumented)
categories: Record<LighthouseCategoryId, LighthouseCategoryAbbr>;
// (undocumented)
report: Object;
// (undocumented)
status: 'COMPLETED';
// (undocumented)
timeCompleted: string;
}
// @public (undocumented)
export interface AuditFailed extends AuditBase {
// (undocumented)
status: 'FAILED';
// (undocumented)
timeCompleted: string;
}
// @public (undocumented)
export interface AuditRunning extends AuditBase {
// (undocumented)
status: 'RUNNING';
}
// @public (undocumented)
export const EmbeddedRouter: () => JSX.Element;
@@ -63,93 +24,20 @@ export const EntityLastLighthouseAuditCard: (props: {
// @public (undocumented)
export const EntityLighthouseContent: () => JSX.Element;
// @public (undocumented)
export class FetchError extends Error {
// (undocumented)
static forResponse(resp: Response): Promise<FetchError>;
// (undocumented)
get name(): string;
}
// @public (undocumented)
export type FormFactor = 'mobile' | 'desktop';
// @public (undocumented)
const isLighthouseAvailable: (entity: Entity) => boolean;
export { isLighthouseAvailable };
export { isLighthouseAvailable as isPluginApplicableToEntity };
// @public (undocumented)
export interface LASListRequest {
// (undocumented)
limit?: number;
// (undocumented)
offset?: number;
}
// @public (undocumented)
export interface LASListResponse<Item> {
// (undocumented)
items: Item[];
// (undocumented)
limit: number;
// (undocumented)
offset: number;
// (undocumented)
total: number;
}
// @public (undocumented)
export const LastLighthouseAuditCard: (props: {
dense?: boolean;
variant?: InfoCardVariants;
}) => JSX.Element;
// @public (undocumented)
export type LighthouseApi = {
url: string;
getWebsiteList: (listOptions: LASListRequest) => Promise<WebsiteListResponse>;
getWebsiteForAuditId: (auditId: string) => Promise<Website>;
triggerAudit: (payload: TriggerAuditPayload) => Promise<Audit>;
getWebsiteByUrl: (websiteUrl: string) => Promise<Website>;
};
// @public (undocumented)
export const lighthouseApiRef: ApiRef<LighthouseApi>;
// @public (undocumented)
export interface LighthouseCategoryAbbr {
// (undocumented)
id: LighthouseCategoryId;
// (undocumented)
score: number;
// (undocumented)
title: string;
}
// @public (undocumented)
export type LighthouseCategoryId =
| 'pwa'
| 'seo'
| 'performance'
| 'accessibility'
| 'best-practices';
// @public (undocumented)
export type LighthouseConfigSettings = {
formFactor: FormFactor;
screenEmulation:
| {
mobile: boolean;
width: number;
height: number;
deviceScaleFactor: number;
disabled: boolean;
}
| undefined;
emulatedFormFactor: FormFactor;
};
// @public (undocumented)
export const LighthousePage: () => JSX.Element;
@@ -165,48 +53,6 @@ const lighthousePlugin: BackstagePlugin<
export { lighthousePlugin };
export { lighthousePlugin as plugin };
// @public (undocumented)
export class LighthouseRestApi implements LighthouseApi {
constructor(url: string);
// (undocumented)
static fromConfig(config: Config): LighthouseRestApi;
// (undocumented)
getWebsiteByUrl(websiteUrl: string): Promise<Website>;
// (undocumented)
getWebsiteForAuditId(auditId: string): Promise<Website>;
// (undocumented)
getWebsiteList(options?: LASListRequest): Promise<WebsiteListResponse>;
// (undocumented)
triggerAudit(payload: TriggerAuditPayload): Promise<Audit>;
// (undocumented)
url: string;
}
// @public (undocumented)
export const Router: () => JSX.Element;
// @public (undocumented)
export interface TriggerAuditPayload {
// (undocumented)
options: {
lighthouseConfig: {
settings: LighthouseConfigSettings;
};
};
// (undocumented)
url: string;
}
// @public (undocumented)
export interface Website {
// (undocumented)
audits: Audit[];
// (undocumented)
lastAudit: Audit;
// (undocumented)
url: string;
}
// @public (undocumented)
export type WebsiteListResponse = LASListResponse<Website>;
```
+2 -1
View File
@@ -16,7 +16,8 @@
import { createDevApp } from '@backstage/dev-utils';
import { lighthousePlugin } from '../src/plugin';
import { lighthouseApiRef, LighthouseRestApi } from '../src';
import { lighthouseApiRef } from '../src';
import { LighthouseRestApi } from '@backstage/plugin-lighthouse-common';
createDevApp()
.registerPlugin(lighthousePlugin)
+1
View File
@@ -38,6 +38,7 @@
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
"@backstage/plugin-lighthouse-common": "workspace:^",
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
+1 -172
View File
@@ -14,181 +14,10 @@
* limitations under the License.
*/
import { Config } from '@backstage/config';
import { createApiRef } from '@backstage/core-plugin-api';
/** @public */
export type LighthouseCategoryId =
| 'pwa'
| 'seo'
| 'performance'
| 'accessibility'
| 'best-practices';
/** @public */
export interface LighthouseCategoryAbbr {
id: LighthouseCategoryId;
score: number;
title: string;
}
/** @public */
export interface LASListRequest {
offset?: number;
limit?: number;
}
/** @public */
export interface LASListResponse<Item> {
items: Item[];
total: number;
offset: number;
limit: number;
}
/** @public */
export interface AuditBase {
id: string;
url: string;
timeCreated: string;
}
/** @public */
export interface AuditRunning extends AuditBase {
status: 'RUNNING';
}
/** @public */
export interface AuditFailed extends AuditBase {
status: 'FAILED';
timeCompleted: string;
}
/** @public */
export interface AuditCompleted extends AuditBase {
status: 'COMPLETED';
timeCompleted: string;
report: Object;
categories: Record<LighthouseCategoryId, LighthouseCategoryAbbr>;
}
/** @public */
export type Audit = AuditRunning | AuditFailed | AuditCompleted;
/** @public */
export interface Website {
url: string;
audits: Audit[];
lastAudit: Audit;
}
/** @public */
export type WebsiteListResponse = LASListResponse<Website>;
/** @public */
export type FormFactor = 'mobile' | 'desktop';
/** @public */
export type LighthouseConfigSettings = {
// For lighthouse 7+
formFactor: FormFactor;
screenEmulation:
| {
mobile: boolean;
width: number;
height: number;
deviceScaleFactor: number;
disabled: boolean;
}
| undefined;
// For lighthouse before 7
emulatedFormFactor: FormFactor;
};
/** @public */
export interface TriggerAuditPayload {
url: string;
options: {
lighthouseConfig: {
settings: LighthouseConfigSettings;
};
};
}
/** @public */
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()}`,
);
}
}
/** @public */
export type LighthouseApi = {
url: string;
getWebsiteList: (listOptions: LASListRequest) => Promise<WebsiteListResponse>;
getWebsiteForAuditId: (auditId: string) => Promise<Website>;
triggerAudit: (payload: TriggerAuditPayload) => Promise<Audit>;
getWebsiteByUrl: (websiteUrl: string) => Promise<Website>;
};
import { LighthouseApi } from '@backstage/plugin-lighthouse-common';
/** @public */
export const lighthouseApiRef = createApiRef<LighthouseApi>({
id: 'plugin.lighthouse.service',
});
/** @public */
export class LighthouseRestApi implements LighthouseApi {
static fromConfig(config: Config) {
return new LighthouseRestApi(config.getString('lighthouse.baseUrl'));
}
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(
options: LASListRequest = {},
): Promise<WebsiteListResponse> {
const { limit, offset } = options;
const params = new URLSearchParams();
if (typeof limit === 'number') params.append('limit', limit.toString());
if (typeof offset === 'number') params.append('offset', offset.toString());
return await this.fetch<WebsiteListResponse>(
`/v1/websites?${params.toString()}`,
);
}
async getWebsiteForAuditId(auditId: string): Promise<Website> {
return await this.fetch<Website>(
`/v1/audits/${encodeURIComponent(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',
},
});
}
async getWebsiteByUrl(websiteUrl: string): Promise<Website> {
return this.fetch<Website>(
`/v1/websites/${encodeURIComponent(websiteUrl)}`,
);
}
}
@@ -19,10 +19,10 @@ import { EntityProvider } from '@backstage/plugin-catalog-react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import React from 'react';
import {
lighthouseApiRef,
LighthouseRestApi,
WebsiteListResponse,
} from '../../api';
} from '@backstage/plugin-lighthouse-common';
import { lighthouseApiRef } from '../../api';
import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity';
import * as data from '../../__fixtures__/website-list-response.json';
import { AuditListForEntity } from './AuditListForEntity';
@@ -25,9 +25,9 @@ import AuditListTable from './AuditListTable';
import {
WebsiteListResponse,
lighthouseApiRef,
LighthouseRestApi,
} from '../../api';
} from '@backstage/plugin-lighthouse-common';
import { lighthouseApiRef } from '../../api';
import { formatTime } from '../../utils';
import { setupServer } from 'msw/node';
import * as data from '../../__fixtures__/website-list-response.json';
@@ -15,7 +15,8 @@
*/
import React, { useState, useEffect } from 'react';
import Typography from '@material-ui/core/Typography';
import { Website, lighthouseApiRef } from '../../api';
import { Website } from '@backstage/plugin-lighthouse-common';
import { lighthouseApiRef } from '../../api';
import useInterval from 'react-use/lib/useInterval';
import {
formatTime,
@@ -33,10 +33,10 @@ import { rest } from 'msw';
import { setupServer } from 'msw/node';
import React from 'react';
import {
lighthouseApiRef,
LighthouseRestApi,
WebsiteListResponse,
} from '../../api';
} from '@backstage/plugin-lighthouse-common';
import { lighthouseApiRef } from '../../api';
import * as data from '../../__fixtures__/website-list-response.json';
import AuditList from './index';
import { ApiProvider } from '@backstage/core-app-api';
@@ -15,7 +15,7 @@
*/
import React from 'react';
import { Audit } from '../../api';
import { Audit } from '@backstage/plugin-lighthouse-common';
import {
StatusError,
StatusOK,
@@ -38,7 +38,12 @@ import { render } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import React from 'react';
import { Audit, lighthouseApiRef, LighthouseRestApi, Website } from '../../api';
import {
Audit,
LighthouseRestApi,
Website,
} from '@backstage/plugin-lighthouse-common';
import { lighthouseApiRef } from '../../api';
import { formatTime } from '../../utils';
import * as data from '../../__fixtures__/website-response.json';
import AuditView from './index';
@@ -33,7 +33,8 @@ import {
useParams,
} from 'react-router-dom';
import useAsync from 'react-use/lib/useAsync';
import { Audit, lighthouseApiRef, Website } from '../../api';
import { Audit, Website } from '@backstage/plugin-lighthouse-common';
import { lighthouseApiRef } from '../../api';
import { formatTime } from '../../utils';
import AuditStatusIcon from '../AuditStatusIcon';
import LighthouseSupportButton from '../SupportButton';
@@ -22,7 +22,7 @@ import {
AuditCompleted,
LighthouseCategoryId,
WebsiteListResponse,
} from '../../api';
} from '@backstage/plugin-lighthouse-common';
import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity';
import * as data from '../../__fixtures__/website-list-response.json';
import { LastLighthouseAuditCard } from './LastLighthouseAuditCard';
@@ -15,7 +15,11 @@
*/
import React from 'react';
import Typography from '@material-ui/core/Typography';
import { Audit, AuditCompleted, LighthouseCategoryId } from '../../api';
import {
Audit,
AuditCompleted,
LighthouseCategoryId,
} from '@backstage/plugin-lighthouse-common';
import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity';
import AuditStatusIcon from '../AuditStatusIcon';
import {
@@ -32,7 +32,8 @@ import { fireEvent, render, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import React from 'react';
import { Audit, lighthouseApiRef, LighthouseRestApi } from '../../api';
import { Audit, LighthouseRestApi } from '@backstage/plugin-lighthouse-common';
import { lighthouseApiRef } from '../../api';
import * as data from '../../__fixtures__/create-audit-response.json';
import CreateAudit from './index';
@@ -27,9 +27,9 @@ import React, { useCallback, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
FormFactor,
lighthouseApiRef,
LighthouseConfigSettings,
} from '../../api';
} from '@backstage/plugin-lighthouse-common';
import { lighthouseApiRef } from '../../api';
import { useQuery } from '../../utils';
import LighthouseSupportButton from '../SupportButton';
@@ -17,7 +17,8 @@ import { Entity } from '@backstage/catalog-model';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { renderHook } from '@testing-library/react-hooks';
import React, { PropsWithChildren } from 'react';
import { lighthouseApiRef, WebsiteListResponse } from '../api';
import { WebsiteListResponse } from '@backstage/plugin-lighthouse-common';
import { lighthouseApiRef } from '../api';
import * as data from '../__fixtures__/website-list-response.json';
import { useWebsiteForEntity } from './useWebsiteForEntity';
+2 -1
View File
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { lighthouseApiRef, LighthouseRestApi } from './api';
import { LighthouseRestApi } from '@backstage/plugin-lighthouse-common';
import { lighthouseApiRef } from './api';
import {
createPlugin,
createRouteRef,
+6 -1
View File
@@ -14,7 +14,12 @@
* limitations under the License.
*/
import { useLocation } from 'react-router-dom';
import { Website, Audit, LighthouseCategoryId, AuditCompleted } from './api';
import {
Website,
Audit,
LighthouseCategoryId,
AuditCompleted,
} from '@backstage/plugin-lighthouse-common';
export function useQuery(): URLSearchParams {
return new URLSearchParams(useLocation().search);