Add Lighthouse card & tab
* Add a lighthouse card to be displayed on the website overview * Add a lighthouse tab and embed lighthouse content
This commit is contained in:
@@ -15,11 +15,18 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { rootRouteRef, viewAuditRouteRef, createAuditRouteRef } from './plugin';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { createAuditRouteRef, rootRouteRef, viewAuditRouteRef } from './plugin';
|
||||
import AuditList from './components/AuditList';
|
||||
import AuditView from './components/AuditView';
|
||||
import CreateAudit from './components/CreateAudit';
|
||||
import AuditView, { AuditViewContent } from './components/AuditView';
|
||||
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';
|
||||
|
||||
export const isPluginApplicableToEntity = (entity: Entity) =>
|
||||
Boolean(entity.metadata.annotations?.[LIGHTHOUSE_WEBSITE_URL_ANNOTATION]) ||
|
||||
entity.metadata.name === 'marketing-site';
|
||||
|
||||
export const Router = () => (
|
||||
<Routes>
|
||||
@@ -28,3 +35,14 @@ export const Router = () => (
|
||||
<Route path={`/${createAuditRouteRef.path}`} element={<CreateAudit />} />
|
||||
</Routes>
|
||||
);
|
||||
|
||||
export const EmbeddedRouter = () => (
|
||||
<Routes>
|
||||
<Route path={`/${rootRouteRef.path}`} element={<AuditListForEntity />} />
|
||||
<Route path={`/${viewAuditRouteRef.path}`} element={<AuditViewContent />} />
|
||||
<Route
|
||||
path={`/${createAuditRouteRef.path}`}
|
||||
element={<CreateAuditContent />}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
|
||||
@@ -104,6 +104,7 @@ export type LighthouseApi = {
|
||||
getWebsiteList: (listOptions: LASListRequest) => Promise<WebsiteListResponse>;
|
||||
getWebsiteForAuditId: (auditId: string) => Promise<Website>;
|
||||
triggerAudit: (payload: TriggerAuditPayload) => Promise<Audit>;
|
||||
getWebsiteByUrl: (websiteUrl: string) => Promise<Website>;
|
||||
};
|
||||
|
||||
export const lighthouseApiRef = createApiRef<LighthouseApi>({
|
||||
@@ -150,4 +151,10 @@ export class LighthouseRestApi implements LighthouseApi {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getWebsiteByUrl(websiteUrl: string): Promise<Website> {
|
||||
return this.fetch<Website>(
|
||||
`/v1/websites/${encodeURIComponent(websiteUrl)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('<AuditListTableForEntity />', () => {
|
||||
let apis: ApiRegistry;
|
||||
|
||||
const mockErrorApi: jest.Mocked<typeof errorApiRef.T> = {
|
||||
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(
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<MemoryRouter>
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityContext.Provider
|
||||
value={{ entity: entity, loading: false, ...value }}
|
||||
>
|
||||
<AuditListForEntity />
|
||||
</EntityContext.Provider>
|
||||
</ApiProvider>
|
||||
</MemoryRouter>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 <Progress />;
|
||||
}
|
||||
if (error || !value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <AuditListTable data-test-id="AuditListTable" items={[value]} />;
|
||||
};
|
||||
@@ -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 (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
title="Lighthouse"
|
||||
subtitle="Website audits powered by Lighthouse"
|
||||
<>
|
||||
<ContentHeader
|
||||
title={value?.url || 'Audit'}
|
||||
description="See a history of all Lighthouse audits for your website run through Backstage."
|
||||
>
|
||||
<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"
|
||||
onClick={() => navigate(`../../${createAuditButtonUrl}`)}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate(`../../${createAuditButtonUrl}`)}
|
||||
>
|
||||
Create New Audit
|
||||
</Button>
|
||||
<LighthouseSupportButton />
|
||||
</ContentHeader>
|
||||
{content}
|
||||
</Content>
|
||||
</Page>
|
||||
Create New Audit
|
||||
</Button>
|
||||
<LighthouseSupportButton />
|
||||
</ContentHeader>
|
||||
{content}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ConnectedAuditView = () => (
|
||||
<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>
|
||||
<AuditViewContent />
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
|
||||
export default ConnectedAuditView;
|
||||
|
||||
@@ -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('<LastLighthouseAuditCard />', () => {
|
||||
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(
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<MemoryRouter>
|
||||
<EntityContext.Provider
|
||||
value={{ entity: entity, loading: false, ...value }}
|
||||
>
|
||||
<LastLighthouseAuditCard />
|
||||
</EntityContext.Provider>
|
||||
</MemoryRouter>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<>
|
||||
<StatusOK />
|
||||
{scoreAsPercentage}%
|
||||
</>
|
||||
);
|
||||
case scoreAsPercentage >= 50 && scoreAsPercentage < 90:
|
||||
return (
|
||||
<>
|
||||
<StatusWarning />
|
||||
{scoreAsPercentage}%
|
||||
</>
|
||||
);
|
||||
case scoreAsPercentage < 50:
|
||||
return (
|
||||
<>
|
||||
<StatusError />
|
||||
{scoreAsPercentage}%
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return <span>N/A</span>;
|
||||
}
|
||||
};
|
||||
|
||||
const LighthouseAuditStatus: FC<{ audit: Audit }> = ({ audit }) => (
|
||||
<>
|
||||
<AuditStatusIcon audit={audit} />
|
||||
{audit.status.toUpperCase()}
|
||||
</>
|
||||
);
|
||||
|
||||
const LighthouseAuditSummary: FC<{ audit: Audit }> = ({ audit }) => {
|
||||
const { url } = audit;
|
||||
const flattenedCategoryData: Record<string, React.ReactNode> = {};
|
||||
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] = (
|
||||
<LighthouseCategoryScoreStatus score={score} />
|
||||
);
|
||||
});
|
||||
}
|
||||
const tableData = {
|
||||
url,
|
||||
status: <LighthouseAuditStatus audit={audit} />,
|
||||
...flattenedCategoryData,
|
||||
};
|
||||
|
||||
return <StructuredMetadataTable metadata={tableData} />;
|
||||
};
|
||||
|
||||
export const LastLighthouseAuditCard: FC<{}> = () => {
|
||||
const { value: website, loading, error } = useWebsiteForEntity();
|
||||
|
||||
let content;
|
||||
if (loading) {
|
||||
content = <Progress />;
|
||||
}
|
||||
if (error) {
|
||||
content = null;
|
||||
}
|
||||
if (website) {
|
||||
content = <LighthouseAuditSummary audit={website.lastAudit} />;
|
||||
}
|
||||
return <InfoCard title="Lighthouse Audit">{content}</InfoCard>;
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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 (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header
|
||||
title="Lighthouse"
|
||||
subtitle="Website audits powered by Lighthouse"
|
||||
<>
|
||||
<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."
|
||||
>
|
||||
<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"
|
||||
onClick={() => navigate('..')}
|
||||
disabled={submitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
>
|
||||
Create Audit
|
||||
</Button>
|
||||
</ListItem>
|
||||
</List>
|
||||
</form>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
<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"
|
||||
onClick={() => navigate('..')}
|
||||
disabled={submitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
>
|
||||
Create Audit
|
||||
</Button>
|
||||
</ListItem>
|
||||
</List>
|
||||
</form>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const CreateAudit = () => (
|
||||
<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>
|
||||
<CreateAuditContent />
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
|
||||
export default CreateAudit;
|
||||
|
||||
@@ -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<typeof errorApiRef.T> = {
|
||||
post: jest.fn(),
|
||||
error$: jest.fn(),
|
||||
};
|
||||
|
||||
const mockLighthouseApi: jest.Mocked<Partial<typeof lighthouseApiRef.T>> = {
|
||||
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 (
|
||||
<ApiProvider
|
||||
apis={ApiRegistry.with(errorApiRef, mockErrorApi).with(
|
||||
lighthouseApiRef,
|
||||
mockLighthouseApi,
|
||||
)}
|
||||
>
|
||||
<EntityContext.Provider value={{ entity: entity, loading: false }}>
|
||||
{children}
|
||||
</EntityContext.Provider>
|
||||
</ApiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user