From fba3d513f528720eb314f20ca764b9733a6ab1af Mon Sep 17 00:00:00 2001 From: Iain Billett Date: Fri, 25 Sep 2020 10:45:51 +0100 Subject: [PATCH 1/5] Add Lighthouse card & tab * Add a lighthouse card to be displayed on the website overview * Add a lighthouse tab and embed lighthouse content --- .../app/src/components/catalog/EntityPage.tsx | 15 ++ plugins/backstage-plugin-travis-ci | 1 + plugins/catalog/src/index.ts | 2 +- plugins/lighthouse/constants.ts | 16 ++ plugins/lighthouse/package.json | 4 + plugins/lighthouse/src/Router.tsx | 26 ++- plugins/lighthouse/src/api.ts | 7 + .../AuditList/AuditListForEntity.test.tsx | 144 +++++++++++++++ .../AuditList/AuditListForEntity.tsx | 31 ++++ .../src/components/AuditView/index.tsx | 51 +++--- .../Cards/LastLighthouseAuditCard.test.tsx | 172 ++++++++++++++++++ .../Cards/LastLighthouseAuditCard.tsx | 102 +++++++++++ .../lighthouse/src/components/Cards/index.ts | 16 ++ .../src/components/CreateAudit/index.tsx | 163 +++++++++-------- .../src/hooks/useWebsiteForEntity.test.tsx | 100 ++++++++++ .../src/hooks/useWebsiteForEntity.ts | 37 ++++ plugins/lighthouse/src/index.ts | 3 +- yarn.lock | 15 ++ 18 files changed, 795 insertions(+), 110 deletions(-) create mode 160000 plugins/backstage-plugin-travis-ci create mode 100644 plugins/lighthouse/constants.ts create mode 100644 plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx create mode 100644 plugins/lighthouse/src/components/AuditList/AuditListForEntity.tsx create mode 100644 plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx create mode 100644 plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx create mode 100644 plugins/lighthouse/src/components/Cards/index.ts create mode 100644 plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx create mode 100644 plugins/lighthouse/src/hooks/useWebsiteForEntity.ts diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index c2483b40ee..393ffe2631 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -49,6 +49,11 @@ import { import { Entity } from '@backstage/catalog-model'; import { Button, Grid } from '@material-ui/core'; import { EmptyState } from '@backstage/core'; +import { + EmbeddedRouter as LighthouseRouter, + LastLighthouseAuditCard, + isPluginApplicableToEntity as isLighthouseAvailable, +} from '@backstage/plugin-lighthouse/'; const CICDSwitcher = ({ entity }: { entity: Entity }) => { // This component is just an example of how you can implement your company's logic in entity page. @@ -115,6 +120,11 @@ const OverviewContent = ({ entity }: { entity: Entity }) => ( + {isLighthouseAvailable(entity) && ( + + + + )} ); @@ -165,6 +175,11 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => ( title="CI/CD" element={} /> + } + /> + Boolean(entity.metadata.annotations?.[LIGHTHOUSE_WEBSITE_URL_ANNOTATION]) || + entity.metadata.name === 'marketing-site'; export const Router = () => ( @@ -28,3 +35,14 @@ export const Router = () => ( } /> ); + +export const EmbeddedRouter = () => ( + + } /> + } /> + } + /> + +); diff --git a/plugins/lighthouse/src/api.ts b/plugins/lighthouse/src/api.ts index 5b6cd9aae7..03b42a38b9 100644 --- a/plugins/lighthouse/src/api.ts +++ b/plugins/lighthouse/src/api.ts @@ -104,6 +104,7 @@ export type LighthouseApi = { getWebsiteList: (listOptions: LASListRequest) => Promise; getWebsiteForAuditId: (auditId: string) => Promise; triggerAudit: (payload: TriggerAuditPayload) => Promise; + getWebsiteByUrl: (websiteUrl: string) => Promise; }; export const lighthouseApiRef = createApiRef({ @@ -150,4 +151,10 @@ export class LighthouseRestApi implements LighthouseApi { }, }); } + + async getWebsiteByUrl(websiteUrl: string): Promise { + return this.fetch( + `/v1/websites/${encodeURIComponent(websiteUrl)}`, + ); + } } diff --git a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx new file mode 100644 index 0000000000..7f10914d9c --- /dev/null +++ b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx @@ -0,0 +1,144 @@ +/* + * 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 { render } from '@testing-library/react'; +import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; +import { + lighthouseApiRef, + LighthouseRestApi, + WebsiteListResponse, +} from '../../api'; +import mockFetch from 'jest-fetch-mock'; + +import * as data from '../../__fixtures__/website-list-response.json'; +import { EntityContext } from '@backstage/plugin-catalog'; +import { Entity } from '@backstage/catalog-model'; +import { AuditListForEntity } from './AuditListForEntity'; +import { lightTheme } from '@backstage/theme'; +import { ThemeProvider } from '@material-ui/core'; +import { MemoryRouter } from 'react-router-dom'; +import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity'; + +jest.mock('../../hooks/useWebsiteForEntity', () => ({ + useWebsiteForEntity: jest.fn(), +})); + +const websiteListResponse = data as WebsiteListResponse; +const entityWebsite = websiteListResponse.items[0]; + +describe('', () => { + let apis: ApiRegistry; + + const mockErrorApi: jest.Mocked = { + post: jest.fn(), + error$: jest.fn(), + }; + + beforeEach(() => { + apis = ApiRegistry.from([ + [lighthouseApiRef, new LighthouseRestApi('http://lighthouse')], + [errorApiRef, mockErrorApi], + ]); + mockFetch.mockResponse(JSON.stringify(entityWebsite)); + (useWebsiteForEntity as jest.Mock).mockReturnValue({ + value: entityWebsite, + loading: false, + error: null, + }); + }); + + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + annotations: { + 'lighthouse.com/website-url': entityWebsite.url, + }, + }, + spec: { + owner: 'guest', + type: 'Website', + lifecycle: 'development', + }, + }; + + const subject = (value = {}) => + render( + + + + + + + + + , + ); + + it('renders the audit list for the entity', async () => { + const { findByText } = subject(); + expect(await findByText(entityWebsite.url)).toBeInTheDocument(); + }); + + describe('where the data is loading', () => { + beforeEach(() => { + (useWebsiteForEntity as jest.Mock).mockReturnValue({ + value: null, + loading: true, + error: null, + }); + }); + + it('renders a Progress element', async () => { + const { findByTestId } = subject(); + expect(await findByTestId('progress')).toBeInTheDocument(); + }); + }); + + describe('where there is an error loading data', () => { + beforeEach(() => { + (useWebsiteForEntity as jest.Mock).mockReturnValue({ + value: null, + loading: false, + error: 'error', + }); + }); + + it('renders nothing', async () => { + const { queryByTestId } = subject(); + expect(await queryByTestId('AuditListTable')).toBeNull(); + }); + }); + + describe('where there is not data', () => { + beforeEach(() => { + (useWebsiteForEntity as jest.Mock).mockReturnValue({ + value: null, + loading: false, + error: null, + }); + }); + + it('renders nothing', async () => { + const { queryByTestId } = subject(); + expect(await queryByTestId('AuditListTable')).toBeNull(); + }); + }); +}); diff --git a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.tsx b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.tsx new file mode 100644 index 0000000000..616fd0fa8a --- /dev/null +++ b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.tsx @@ -0,0 +1,31 @@ +/* + * 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 { AuditListTable } from './AuditListTable'; +import { Progress } from '@backstage/core'; +import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity'; + +export const AuditListForEntity = () => { + const { value, loading, error } = useWebsiteForEntity(); + if (loading) { + return ; + } + if (error || !value) { + return null; + } + + return ; +}; diff --git a/plugins/lighthouse/src/components/AuditView/index.tsx b/plugins/lighthouse/src/components/AuditView/index.tsx index d0e743a77f..54bd179418 100644 --- a/plugins/lighthouse/src/components/AuditView/index.tsx +++ b/plugins/lighthouse/src/components/AuditView/index.tsx @@ -124,7 +124,7 @@ const AuditView: FC<{ audit?: Audit }> = ({ audit }: { audit?: Audit }) => { ); }; -const ConnectedAuditView: FC<{}> = () => { +export const AuditViewContent: FC<{}> = () => { const lighthouseApi = useApi(lighthouseApiRef); const params = useParams() as { id: string }; const classes = useStyles(); @@ -173,32 +173,35 @@ const ConnectedAuditView: FC<{}> = () => { } return ( - -
+ - - -
- - navigate(`../../${createAuditButtonUrl}`)} > - - - - {content} - -
+ Create New Audit + + + + {content} + ); }; +const ConnectedAuditView = () => ( + +
+ + +
+ + + +
+); + export default ConnectedAuditView; diff --git a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx new file mode 100644 index 0000000000..20b3b9c072 --- /dev/null +++ b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx @@ -0,0 +1,172 @@ +/* + * 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 { render } from '@testing-library/react'; +import { + AuditCompleted, + LighthouseCategoryId, + WebsiteListResponse, +} from '../../api'; +import { EntityContext } from '@backstage/plugin-catalog'; +import { Entity } from '@backstage/catalog-model'; +import { LastLighthouseAuditCard } from './LastLighthouseAuditCard'; +import { lightTheme } from '@backstage/theme'; +import { ThemeProvider } from '@material-ui/core'; +import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity'; +import { MemoryRouter } from 'react-router-dom'; +import * as data from '../../__fixtures__/website-list-response.json'; + +jest.mock('../../hooks/useWebsiteForEntity', () => ({ + useWebsiteForEntity: jest.fn(), +})); + +const websiteListResponse = data as WebsiteListResponse; +let entityWebsite = websiteListResponse.items[2]; + +describe('', () => { + const asPercentage = (fraction: number) => `${fraction * 100}%`; + + beforeEach(() => { + (useWebsiteForEntity as jest.Mock).mockReturnValue({ + value: entityWebsite, + loading: false, + error: null, + }); + }); + + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + annotations: { + 'lighthouse.com/website-url': entityWebsite.url, + }, + }, + spec: { + owner: 'guest', + type: 'Website', + lifecycle: 'development', + }, + }; + + const subject = (value = {}) => + render( + + + + + + + , + ); + + describe('where the last audit completed successfully', () => { + const audit = entityWebsite.lastAudit as AuditCompleted; + + it('renders the performance data for the audit', async () => { + const { findByText } = subject(); + expect(await findByText(audit.url)).toBeInTheDocument(); + expect(await findByText(audit.status)).toBeInTheDocument(); + for (const category of Object.keys(audit.categories)) { + const { score } = audit.categories[category as LighthouseCategoryId]; + expect(await findByText(asPercentage(score))).toBeInTheDocument(); + } + }); + + describe('where a category score is not a number', () => { + beforeEach(() => { + entityWebsite = { ...entityWebsite }; + (entityWebsite.lastAudit as AuditCompleted).categories.accessibility.score = NaN; + }); + + afterEach(() => { + entityWebsite = websiteListResponse.items[2]; + }); + + it('renders the performance data for the audit', async () => { + const { findByText } = subject(); + expect(await findByText('N/A')).toBeInTheDocument(); + }); + }); + }); + + describe('where the last audit is in running', () => { + const audit = websiteListResponse.items[0].lastAudit as AuditCompleted; + + beforeEach(() => { + (useWebsiteForEntity as jest.Mock).mockReturnValue({ + value: websiteListResponse.items[0], + loading: false, + error: null, + }); + }); + + it('renders the url and status of the audit', async () => { + const { findByText } = subject(); + expect(await findByText(audit.url)).toBeInTheDocument(); + expect(await findByText(audit.status)).toBeInTheDocument(); + }); + }); + + describe('where the data is loading', () => { + beforeEach(() => { + (useWebsiteForEntity as jest.Mock).mockReturnValue({ + value: null, + loading: true, + error: null, + }); + }); + + it('renders a Progress element', async () => { + const { findByTestId } = subject(); + expect(await findByTestId('progress')).toBeInTheDocument(); + }); + }); + + describe('where there is an error loading data', () => { + beforeEach(() => { + (useWebsiteForEntity as jest.Mock).mockReturnValue({ + value: null, + loading: false, + error: 'error', + }); + }); + + it('renders nothing', async () => { + const { queryByTestId } = subject(); + expect(await queryByTestId('AuditListTable')).toBeNull(); + }); + }); + // + describe('where there is no data', () => { + beforeEach(() => { + (useWebsiteForEntity as jest.Mock).mockReturnValue({ + value: null, + loading: false, + error: null, + }); + }); + + it('renders nothing', async () => { + const { queryByTestId } = subject(); + expect(await queryByTestId('AuditListTable')).toBeNull(); + }); + }); +}); diff --git a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx new file mode 100644 index 0000000000..d82e6c5c6c --- /dev/null +++ b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx @@ -0,0 +1,102 @@ +/* + * 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 { Audit, AuditCompleted, LighthouseCategoryId } from '../../api'; +import { + InfoCard, + Progress, + StatusError, + StatusOK, + StatusWarning, + StructuredMetadataTable, +} from '@backstage/core'; +import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity'; +import AuditStatusIcon from '../AuditStatusIcon'; + +const LighthouseCategoryScoreStatus: FC<{ score: number }> = ({ score }) => { + const scoreAsPercentage = score * 100; + switch (true) { + case scoreAsPercentage >= 90: + return ( + <> + + {scoreAsPercentage}% + + ); + case scoreAsPercentage >= 50 && scoreAsPercentage < 90: + return ( + <> + + {scoreAsPercentage}% + + ); + case scoreAsPercentage < 50: + return ( + <> + + {scoreAsPercentage}% + + ); + default: + return N/A; + } +}; + +const LighthouseAuditStatus: FC<{ audit: Audit }> = ({ audit }) => ( + <> + + {audit.status.toUpperCase()} + +); + +const LighthouseAuditSummary: FC<{ audit: Audit }> = ({ audit }) => { + const { url } = audit; + const flattenedCategoryData: Record = {}; + if (audit.status === 'COMPLETED') { + const categories = (audit as AuditCompleted).categories; + const categoryIds = Object.keys(categories) as LighthouseCategoryId[]; + categoryIds.forEach((id: LighthouseCategoryId) => { + const { title, score } = categories[id]; + + flattenedCategoryData[title] = ( + + ); + }); + } + const tableData = { + url, + status: , + ...flattenedCategoryData, + }; + + return ; +}; + +export const LastLighthouseAuditCard: FC<{}> = () => { + const { value: website, loading, error } = useWebsiteForEntity(); + + let content; + if (loading) { + content = ; + } + if (error) { + content = null; + } + if (website) { + content = ; + } + return {content}; +}; diff --git a/plugins/lighthouse/src/components/Cards/index.ts b/plugins/lighthouse/src/components/Cards/index.ts new file mode 100644 index 0000000000..7595619808 --- /dev/null +++ b/plugins/lighthouse/src/components/Cards/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { LastLighthouseAuditCard } from './LastLighthouseAuditCard'; diff --git a/plugins/lighthouse/src/components/CreateAudit/index.tsx b/plugins/lighthouse/src/components/CreateAudit/index.tsx index 36bd97485a..ad46b06020 100644 --- a/plugins/lighthouse/src/components/CreateAudit/index.tsx +++ b/plugins/lighthouse/src/components/CreateAudit/index.tsx @@ -53,7 +53,7 @@ const useStyles = makeStyles(theme => ({ }, })); -const CreateAudit: FC<{}> = () => { +export const CreateAuditContent: FC<{}> = () => { const errorApi = useApi(errorApiRef); const lighthouseApi = useApi(lighthouseApiRef); const classes = useStyles(); @@ -94,88 +94,91 @@ const CreateAudit: FC<{}> = () => { ]); return ( - -
+ - - -
- - - - - - - -
{ - ev.preventDefault(); - triggerAudit(); - }} - > - - - setUrl(ev.target.value)} - value={url} - inputProps={{ 'aria-label': 'URL' }} - /> - - - setEmulatedFormFactor(ev.target.value)} - value={emulatedFormFactor} - inputProps={{ 'aria-label': 'Emulated form factor' }} - > - Mobile - Desktop - - - - - - - -
-
-
+ + + + + +
{ + ev.preventDefault(); + triggerAudit(); + }} + > + + + setUrl(ev.target.value)} + value={url} + inputProps={{ 'aria-label': 'URL' }} + /> + + + setEmulatedFormFactor(ev.target.value)} + value={emulatedFormFactor} + inputProps={{ 'aria-label': 'Emulated form factor' }} + > + Mobile + Desktop + + + + + + + +
+
-
-
+ + ); }; +const CreateAudit = () => ( + +
+ + +
+ + + +
+); + export default CreateAudit; diff --git a/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx new file mode 100644 index 0000000000..868e26f04c --- /dev/null +++ b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx @@ -0,0 +1,100 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core-api'; +import { lighthouseApiRef, WebsiteListResponse } from '../api'; +import { useWebsiteForEntity } from './useWebsiteForEntity'; +import { EntityContext } from '@backstage/plugin-catalog'; +import { Entity } from '@backstage/catalog-model'; +import * as data from '../__fixtures__/website-list-response.json'; + +const websiteListResponse = data as WebsiteListResponse; +const website = websiteListResponse.items[0]; + +const mockErrorApi: jest.Mocked = { + post: jest.fn(), + error$: jest.fn(), +}; + +const mockLighthouseApi: jest.Mocked> = { + getWebsiteByUrl: jest.fn(), +}; + +describe('useWebsiteForEntity', () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + annotations: { + 'lighthouse.com/website-url': website.url, + }, + }, + spec: { + owner: 'guest', + type: 'Website', + lifecycle: 'development', + }, + }; + + const wrapper: React.FC<{}> = ({ children }) => { + return ( + + + {children} + + + ); + }; + + const subject = () => + renderHook(useWebsiteForEntity, { + wrapper, + }); + + beforeEach(() => { + (mockLighthouseApi.getWebsiteByUrl as jest.Mock).mockResolvedValue(website); + }); + + it('returns the lighthouse information for the website url in annotations ', async () => { + const { result, waitForNextUpdate } = subject(); + await waitForNextUpdate(); + expect(result.current?.value).toBe(website); + }); + + describe('where there is an error', () => { + const error = new Error('useWebsiteForEntity unit test'); + + beforeEach(() => { + (mockLighthouseApi.getWebsiteByUrl as jest.Mock).mockRejectedValueOnce( + error, + ); + }); + + it('posts the error to the error api and returns the error to the caller', async () => { + const { result, waitForNextUpdate } = subject(); + await waitForNextUpdate(); + expect(result.current?.error).toBe(error); + expect(mockErrorApi.post).toHaveBeenCalledWith(error); + }); + }); +}); diff --git a/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts b/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts new file mode 100644 index 0000000000..c52e38f473 --- /dev/null +++ b/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts @@ -0,0 +1,37 @@ +/* + * 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 { useEntity } from '@backstage/plugin-catalog'; +import { LIGHTHOUSE_WEBSITE_URL_ANNOTATION } from '../../constants'; +import { errorApiRef, useApi } from '@backstage/core-api'; +import { lighthouseApiRef } from '../api'; +import { useAsync } from 'react-use'; + +// For the sake of simplicity we assume that an entity has only one website url. This is to avoid encoding a list +// type in an annotation which is a plain string. +export const useWebsiteForEntity = () => { + const { entity } = useEntity(); + const websiteUrl = + entity.metadata.annotations?.[LIGHTHOUSE_WEBSITE_URL_ANNOTATION] ?? ''; + const lighthouseApi = useApi(lighthouseApiRef); + const errorApi = useApi(errorApiRef); + const response = useAsync(() => lighthouseApi.getWebsiteByUrl(websiteUrl), [ + websiteUrl, + ]); + if (response.error) { + errorApi.post(response.error); + } + return response; +}; diff --git a/plugins/lighthouse/src/index.ts b/plugins/lighthouse/src/index.ts index bebdaaf713..64fe2f8cc0 100644 --- a/plugins/lighthouse/src/index.ts +++ b/plugins/lighthouse/src/index.ts @@ -15,5 +15,6 @@ */ export { plugin } from './plugin'; -export { Router } from './Router'; +export { Router, isPluginApplicableToEntity, EmbeddedRouter } from './Router'; export * from './api'; +export * from './components/Cards'; diff --git a/yarn.lock b/yarn.lock index a3b64f0650..832a5afc86 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4510,6 +4510,14 @@ "@babel/runtime" "^7.5.4" "@types/testing-library__react-hooks" "^3.3.0" +"@testing-library/react-hooks@^3.4.2": + version "3.4.2" + resolved "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-3.4.2.tgz#8deb94f7684e0d896edd84a4c90e5b79a0810bc2" + integrity sha512-RfPG0ckOzUIVeIqlOc1YztKgFW+ON8Y5xaSPbiBkfj9nMkkiLhLeBXT5icfPX65oJV/zCZu4z8EVnUc6GY9C5A== + dependencies: + "@babel/runtime" "^7.5.4" + "@types/testing-library__react-hooks" "^3.4.0" + "@testing-library/react@^10.4.1": version "10.4.3" resolved "https://registry.npmjs.org/@testing-library/react/-/react-10.4.3.tgz#c6f356688cffc51f6b35385583d664bb11a161f4" @@ -5622,6 +5630,13 @@ dependencies: "@types/react-test-renderer" "*" +"@types/testing-library__react-hooks@^3.4.0": + version "3.4.1" + resolved "https://registry.npmjs.org/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.4.1.tgz#b8d7311c6c1f7db3103e94095fe901f8fef6e433" + integrity sha512-G4JdzEcq61fUyV6wVW9ebHWEiLK2iQvaBuCHHn9eMSbZzVh4Z4wHnUGIvQOYCCYeu5DnUtFyNYuAAgbSaO/43Q== + dependencies: + "@types/react-test-renderer" "*" + "@types/through@*": version "0.0.30" resolved "https://registry.npmjs.org/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" From 4758e62c26f2882d34992b89a2cb48ab36ba40d8 Mon Sep 17 00:00:00 2001 From: Iain Billett Date: Sat, 3 Oct 2020 17:23:29 +0100 Subject: [PATCH 2/5] Add EmptyState to Lighthouse Router --- plugins/lighthouse/package.json | 3 ++- plugins/lighthouse/src/Router.tsx | 29 ++++++++++++++++++++--------- yarn.lock | 13 +++++++++++++ 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 32e11d71fd..fb3a6bcf30 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -35,7 +35,8 @@ "react-dom": "^16.13.1", "react-markdown": "^4.3.1", "react-router-dom": "6.0.0-beta.0", - "react-use": "^15.3.3" + "react-use": "^15.3.3", + "@types/react": "^16.9.50" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.24", diff --git a/plugins/lighthouse/src/Router.tsx b/plugins/lighthouse/src/Router.tsx index 576bbf82e1..6036d9954d 100644 --- a/plugins/lighthouse/src/Router.tsx +++ b/plugins/lighthouse/src/Router.tsx @@ -23,6 +23,7 @@ import CreateAudit, { CreateAuditContent } from './components/CreateAudit'; import { Entity } from '@backstage/catalog-model'; import { LIGHTHOUSE_WEBSITE_URL_ANNOTATION } from '../constants'; import { AuditListForEntity } from './components/AuditList/AuditListForEntity'; +import { EmptyState } from '@backstage/core'; export const isPluginApplicableToEntity = (entity: Entity) => Boolean(entity.metadata.annotations?.[LIGHTHOUSE_WEBSITE_URL_ANNOTATION]) || @@ -36,13 +37,23 @@ export const Router = () => ( ); -export const EmbeddedRouter = () => ( - - } /> - } /> - } +export const EmbeddedRouter = ({ entity }: { entity: Entity }) => + !isPluginApplicableToEntity(entity) ? ( + - -); + ) : ( + + } /> + } + /> + } + /> + + ); diff --git a/yarn.lock b/yarn.lock index 832a5afc86..d11ca34aec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5430,6 +5430,14 @@ dependencies: csstype "^2.2.0" +"@types/react@^16.9.50": + version "16.9.50" + resolved "https://registry.npmjs.org/@types/react/-/react-16.9.50.tgz#cb5f2c22d42de33ca1f5efc6a0959feb784a3a2d" + integrity sha512-kPx5YsNnKDJejTk1P+lqThwxN2PczrocwsvqXnjvVvKpFescoY62ZiM3TV7dH1T8lFhlHZF+PE5xUyimUwqEGA== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + "@types/reactcss@*": version "1.2.3" resolved "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.3.tgz#af28ae11bbb277978b99d04d1eedfd068ca71834" @@ -9316,6 +9324,11 @@ csstype@^2.2.0, csstype@^2.5.2, csstype@^2.5.5, csstype@^2.5.7, csstype@^2.6.5, resolved "https://registry.npmjs.org/csstype/-/csstype-2.6.9.tgz#05141d0cd557a56b8891394c1911c40c8a98d098" integrity sha512-xz39Sb4+OaTsULgUERcCk+TJj8ylkL4aSVDQiX/ksxbELSqwkgt4d4RD7fovIdgJGSuNYqwZEiVjYY5l0ask+Q== +csstype@^3.0.2: + version "3.0.3" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz#2b410bbeba38ba9633353aff34b05d9755d065f8" + integrity sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag== + csv-generate@^3.2.4: version "3.2.4" resolved "https://registry.npmjs.org/csv-generate/-/csv-generate-3.2.4.tgz#440dab9177339ee0676c9e5c16f50e2b3463c019" From 5b52d983e32c9124990ca0b5ce13164da1cac095 Mon Sep 17 00:00:00 2001 From: Iain Billett Date: Sat, 3 Oct 2020 19:30:12 +0100 Subject: [PATCH 3/5] Fix @types/react dependency version --- plugins/lighthouse/package.json | 2 +- yarn.lock | 13 ------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index fb3a6bcf30..7446f09ed4 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -36,7 +36,7 @@ "react-markdown": "^4.3.1", "react-router-dom": "6.0.0-beta.0", "react-use": "^15.3.3", - "@types/react": "^16.9.50" + "@types/react": "^16.9" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.24", diff --git a/yarn.lock b/yarn.lock index d11ca34aec..832a5afc86 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5430,14 +5430,6 @@ dependencies: csstype "^2.2.0" -"@types/react@^16.9.50": - version "16.9.50" - resolved "https://registry.npmjs.org/@types/react/-/react-16.9.50.tgz#cb5f2c22d42de33ca1f5efc6a0959feb784a3a2d" - integrity sha512-kPx5YsNnKDJejTk1P+lqThwxN2PczrocwsvqXnjvVvKpFescoY62ZiM3TV7dH1T8lFhlHZF+PE5xUyimUwqEGA== - dependencies: - "@types/prop-types" "*" - csstype "^3.0.2" - "@types/reactcss@*": version "1.2.3" resolved "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.3.tgz#af28ae11bbb277978b99d04d1eedfd068ca71834" @@ -9324,11 +9316,6 @@ csstype@^2.2.0, csstype@^2.5.2, csstype@^2.5.5, csstype@^2.5.7, csstype@^2.6.5, resolved "https://registry.npmjs.org/csstype/-/csstype-2.6.9.tgz#05141d0cd557a56b8891394c1911c40c8a98d098" integrity sha512-xz39Sb4+OaTsULgUERcCk+TJj8ylkL4aSVDQiX/ksxbELSqwkgt4d4RD7fovIdgJGSuNYqwZEiVjYY5l0ask+Q== -csstype@^3.0.2: - version "3.0.3" - resolved "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz#2b410bbeba38ba9633353aff34b05d9755d065f8" - integrity sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag== - csv-generate@^3.2.4: version "3.2.4" resolved "https://registry.npmjs.org/csv-generate/-/csv-generate-3.2.4.tgz#440dab9177339ee0676c9e5c16f50e2b3463c019" From 79379097f7ce3167d937809bf58058f71ccd0b7a Mon Sep 17 00:00:00 2001 From: Iain Billett Date: Sun, 4 Oct 2020 13:49:58 +0100 Subject: [PATCH 4/5] Update Lighthouse plugin README Add instructions for integration with the catalog. --- plugins/lighthouse/README.md | 63 +++++++++++++++++++ .../Cards/LastLighthouseAuditCard.tsx | 15 +++-- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/plugins/lighthouse/README.md b/plugins/lighthouse/README.md index b5fecb85f8..19244f3cf0 100644 --- a/plugins/lighthouse/README.md +++ b/plugins/lighthouse/README.md @@ -57,3 +57,66 @@ Then configure the lighthouse service url in your [`app-config.yaml`](https://gi lighthouse: baseUrl: http://your-service-url ``` + +### Integration with the Catalog + +The lighthouse plugin can be integrated into the catalog so that lighthouse audit information relating to a component +can be displayed within that component's entity page. In order to link an Entity to its lighthouse audits the entity +must be annotated as follows: + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + # ... + annotations: + # ... + lighthouse.com/website-url: # A single website url e.g. https://backstage.io/ +``` + +> NOTE: The lighthouse plugin only supports one website url per component at this time. + +Add a lighthouse tab to the EntityPage: + +```tsx +// packages/app/src/components/catalog/EntityPage.tsx +import { EmbeddedRouter as LighthouseRouter } from '@backstage/plugin-lighthouse'; + +// ... +const WebsiteEntityPage = ({ entity }: { entity: Entity }) => ( + + // ... + } + /> + +); +``` + +> NOTE: The embedded router renders page content without a header section allowing it to be rendered within a +> catalog plugin page. + +Add a Lighthouse card to the overview tab on the EntityPage: + +```tsx +// packages/app/src/components/catalog/EntityPage.tsx +import { + LastLighthouseAuditCard, + isPluginApplicableToEntity as isLighthouseAvailable, +} from '@backstage/plugin-lighthouse'; + +// ... + +const OverviewContent = ({ entity }: { entity: Entity }) => ( + + // ... + {isLighthouseAvailable(entity) && ( + + + + )} + +); +``` diff --git a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx index d82e6c5c6c..d3bc362877 100644 --- a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx +++ b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx @@ -62,7 +62,10 @@ const LighthouseAuditStatus: FC<{ audit: Audit }> = ({ audit }) => ( ); -const LighthouseAuditSummary: FC<{ audit: Audit }> = ({ audit }) => { +const LighthouseAuditSummary: FC<{ audit: Audit; dense?: boolean }> = ({ + audit, + dense = false, +}) => { const { url } = audit; const flattenedCategoryData: Record = {}; if (audit.status === 'COMPLETED') { @@ -82,10 +85,12 @@ const LighthouseAuditSummary: FC<{ audit: Audit }> = ({ audit }) => { ...flattenedCategoryData, }; - return ; + return ; }; -export const LastLighthouseAuditCard: FC<{}> = () => { +export const LastLighthouseAuditCard: FC<{ dense?: boolean }> = ({ + dense = false, +}) => { const { value: website, loading, error } = useWebsiteForEntity(); let content; @@ -96,7 +101,9 @@ export const LastLighthouseAuditCard: FC<{}> = () => { content = null; } if (website) { - content = ; + content = ( + + ); } return {content}; }; From 4e76fea2699f91ee47bbf5fdfbd99ee2df19fd15 Mon Sep 17 00:00:00 2001 From: Iain Billett Date: Tue, 6 Oct 2020 08:34:04 +0100 Subject: [PATCH 5/5] Fixes for Lighthouse widgets --- plugins/backstage-plugin-travis-ci | 1 - plugins/lighthouse/src/Router.tsx | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 160000 plugins/backstage-plugin-travis-ci diff --git a/plugins/backstage-plugin-travis-ci b/plugins/backstage-plugin-travis-ci deleted file mode 160000 index b70556a6e5..0000000000 --- a/plugins/backstage-plugin-travis-ci +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b70556a6e5476cb64ff5c23123052c79ee926269 diff --git a/plugins/lighthouse/src/Router.tsx b/plugins/lighthouse/src/Router.tsx index 6036d9954d..df481483c3 100644 --- a/plugins/lighthouse/src/Router.tsx +++ b/plugins/lighthouse/src/Router.tsx @@ -26,8 +26,7 @@ import { AuditListForEntity } from './components/AuditList/AuditListForEntity'; import { EmptyState } from '@backstage/core'; export const isPluginApplicableToEntity = (entity: Entity) => - Boolean(entity.metadata.annotations?.[LIGHTHOUSE_WEBSITE_URL_ANNOTATION]) || - entity.metadata.name === 'marketing-site'; + Boolean(entity.metadata.annotations?.[LIGHTHOUSE_WEBSITE_URL_ANNOTATION]); export const Router = () => (