Merge branch 'mob/scaffolder-cra-fixes' of github.com:spotify/backstage into shmidt-i/universal-file-github-actions
This commit is contained in:
@@ -32,6 +32,7 @@ Codehilite
|
||||
codeowners
|
||||
config
|
||||
Config
|
||||
configmaps
|
||||
configs
|
||||
const
|
||||
cookiecutter
|
||||
@@ -67,6 +68,7 @@ graphviz
|
||||
Hackathons
|
||||
haproxy
|
||||
heroku
|
||||
horizontalpodautoscalers
|
||||
Hostname
|
||||
http
|
||||
https
|
||||
@@ -140,6 +142,7 @@ rankdir
|
||||
readme
|
||||
Readme
|
||||
Redash
|
||||
replicasets
|
||||
repo
|
||||
Repo
|
||||
repos
|
||||
|
||||
@@ -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 }) => (
|
||||
<AboutCard entity={entity} />
|
||||
</Grid>
|
||||
<RecentCICDRunsSwitcher entity={entity} />
|
||||
{isLighthouseAvailable(entity) && (
|
||||
<Grid item sm={4}>
|
||||
<LastLighthouseAuditCard />
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
|
||||
@@ -165,6 +175,11 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
title="CI/CD"
|
||||
element={<CICDSwitcher entity={entity} />}
|
||||
/>
|
||||
<EntityPageLayout.Content
|
||||
path="/lighthouse/*"
|
||||
title="Lighthouse"
|
||||
element={<LighthouseRouter entity={entity} />}
|
||||
/>
|
||||
<EntityPageLayout.Content
|
||||
path="/sentry"
|
||||
title="Sentry"
|
||||
|
||||
@@ -20,6 +20,6 @@ export * from './api/types';
|
||||
export * from './routes';
|
||||
export { useEntityCompoundName } from './components/useEntityCompoundName';
|
||||
export { Router } from './components/Router';
|
||||
export { useEntity } from './hooks/useEntity';
|
||||
export { useEntity, EntityContext } from './hooks/useEntity';
|
||||
export { AboutCard } from './components/AboutCard';
|
||||
export { EntityPageLayout } from './components/EntityPageLayout';
|
||||
|
||||
@@ -6,6 +6,66 @@ This is the backend part of the Kubernetes plugin.
|
||||
|
||||
It responds to Kubernetes requests from the frontend.
|
||||
|
||||
## Links
|
||||
## Configuration
|
||||
|
||||
- [The Backstage homepage](https://backstage.io)
|
||||
### clusterLocatorMethod
|
||||
|
||||
This configures how to determine which clusters a component is running in.
|
||||
|
||||
Currently, the only valid locator method is:
|
||||
|
||||
#### configMultiTenant
|
||||
|
||||
This configuration assumes that all components run on all the provided clusters.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
kubernetes:
|
||||
clusterLocatorMethod: 'configMultiTenant'
|
||||
clusters:
|
||||
- url: http://127.0.0.1:9999
|
||||
name: minikube
|
||||
serviceAccountToken: <TOKEN FROM STEP 4>
|
||||
authProvider: 'serviceAccount'
|
||||
- url: http://127.0.0.2:9999
|
||||
name: gke-cluster-1
|
||||
authProvider: 'google'
|
||||
```
|
||||
|
||||
##### clusters
|
||||
|
||||
Used by the `configMultiTenant` `clusterLocatorMethod` to construct Kubernetes clients.
|
||||
|
||||
###### url
|
||||
|
||||
The base url to the Kubernetes control plane. Can be found by using the `Kubernetes master` result from running the `kubectl cluster-info` command.
|
||||
|
||||
###### name
|
||||
|
||||
A name to represent this cluster, this must be unique within the `clusters` array. Users will see this value in the Service Catalog Kubernetes plugin.
|
||||
|
||||
###### authProvider
|
||||
|
||||
This determines how the Kubernetes client authenticate with the Kubernetes cluster. Valid values are:
|
||||
|
||||
| Value | Description |
|
||||
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `serviceAccount` | This will use a Kubernetes [service account](https://kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/) to access the Kubernetes API. When this is used the `serviceAccountToken` field should also be set. |
|
||||
| `google` | This will use a user's google auth token from the [google auth plugin](https://backstage.io/docs/auth/) to access the Kubernetes API. |
|
||||
|
||||
###### serviceAccount (optional)
|
||||
|
||||
The service account token to be used when using the `authProvider`, `serviceAccount`.
|
||||
|
||||
## RBAC
|
||||
|
||||
The current RBAC permissions required are read-only cluster wide, for the following objects:
|
||||
|
||||
- pods
|
||||
- services
|
||||
- configmaps
|
||||
- deployments
|
||||
- replicasets
|
||||
- horizontalpodautoscalers
|
||||
- ingresses
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Dice roller
|
||||
|
||||
An app to roll dice (it doesn't actually do that).
|
||||
This can be used to run the kubernetes plugin locally against a mock service.
|
||||
|
||||
# Viewing in local Minikube running Backstage locally
|
||||
|
||||
@@ -23,22 +23,24 @@ An app to roll dice (it doesn't actually do that).
|
||||
6. Register existing component in Backstage
|
||||
- https://github.com/mclarke47/dice-roller/blob/master/catalog-info.yaml
|
||||
|
||||
Update `app-config.yaml` as follows.
|
||||
Update `app-config.development.yaml` as follows.
|
||||
|
||||
```yaml
|
||||
---
|
||||
kubernetes:
|
||||
clusterLocatorMethod: 'configMultiTenant'
|
||||
clusters:
|
||||
- url: <KUBERNETES MASTER BASE URL FROM STEP 2>
|
||||
name: minikube
|
||||
serviceAccountToken: <TOKEN FROM STEP 4>
|
||||
authProvider: 'serviceAccount'
|
||||
```
|
||||
|
||||
### Getting the service account token
|
||||
|
||||
Mac copy to clipboard:
|
||||
|
||||
```
|
||||
kubectl get secret DICE_ROLLER_TOKEN_NAME -o=json | jq -r '.data["token"]' | base64 --decode | pbcopy
|
||||
kubectl get secret $(kubectl get sa dice-roller -o=json | jq -r .secrets[0].name) -o=json | jq -r '.data["token"]' | base64 --decode | pbcopy
|
||||
```
|
||||
|
||||
Paste into `app-config.yaml` `kubernetes.clusters[].serviceAccountToken`
|
||||
Paste into `app-config.development.yaml` `kubernetes.clusters[0].serviceAccountToken`
|
||||
|
||||
@@ -11,3 +11,24 @@ Your plugin has been added to the example app in this repository, meaning you'll
|
||||
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
|
||||
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
|
||||
It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
|
||||
|
||||
## Surfacing your Kubernetes components as part of an entity
|
||||
|
||||
### Adding the entity annotation
|
||||
|
||||
In order for Backstage to detect that an entity has Kubernetes components,
|
||||
the following annotation should be added to the entity.
|
||||
|
||||
```yaml
|
||||
annotations:
|
||||
'backstage.io/kubernetes-id': dice-roller
|
||||
```
|
||||
|
||||
### Labeling Kubernetes components
|
||||
|
||||
In order for Kubernetes components to show up in the service catalog
|
||||
as a part of an entity, Kubernetes components must be labeled with the following label:
|
||||
|
||||
```yaml
|
||||
'backstage.io/kubernetes-id': <ENTITY_NAME>
|
||||
```
|
||||
|
||||
@@ -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 }) => (
|
||||
<EntityPageLayout>
|
||||
// ...
|
||||
<EntityPageLayout.Content
|
||||
path="/lighthouse"
|
||||
title="Lighthouse"
|
||||
element={<LighthouseRouter entity={entity} />}
|
||||
/>
|
||||
</EntityPageLayout>
|
||||
);
|
||||
```
|
||||
|
||||
> 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 }) => (
|
||||
<Grid container spacing={3}>
|
||||
// ...
|
||||
{isLighthouseAvailable(entity) && (
|
||||
<Grid item sm={4}>
|
||||
<LastLighthouseAuditCard />
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
```
|
||||
|
||||
@@ -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 const LIGHTHOUSE_WEBSITE_URL_ANNOTATION = 'lighthouse.com/website-url';
|
||||
@@ -21,17 +21,22 @@
|
||||
"start": "backstage-cli plugin:serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.24",
|
||||
"@backstage/config": "^0.1.1-alpha.24",
|
||||
"@backstage/core": "^0.1.1-alpha.24",
|
||||
"@backstage/core-api": "^0.1.1-alpha.24",
|
||||
"@backstage/plugin-catalog": "^0.1.1-alpha.24",
|
||||
"@backstage/theme": "^0.1.1-alpha.24",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.45",
|
||||
"@testing-library/react-hooks": "^3.4.2",
|
||||
"react": "^16.13.1",
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.24",
|
||||
|
||||
@@ -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';
|
||||
import { EmptyState } from '@backstage/core';
|
||||
|
||||
export const isPluginApplicableToEntity = (entity: Entity) =>
|
||||
Boolean(entity.metadata.annotations?.[LIGHTHOUSE_WEBSITE_URL_ANNOTATION]);
|
||||
|
||||
export const Router = () => (
|
||||
<Routes>
|
||||
@@ -28,3 +35,24 @@ export const Router = () => (
|
||||
<Route path={`/${createAuditRouteRef.path}`} element={<CreateAudit />} />
|
||||
</Routes>
|
||||
);
|
||||
|
||||
export const EmbeddedRouter = ({ entity }: { entity: Entity }) =>
|
||||
!isPluginApplicableToEntity(entity) ? (
|
||||
<EmptyState
|
||||
missing="field"
|
||||
title="Your plugin is missing an annotation"
|
||||
description={`Please add the ${LIGHTHOUSE_WEBSITE_URL_ANNOTATION} annotation`}
|
||||
/>
|
||||
) : (
|
||||
<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,109 @@
|
||||
/*
|
||||
* 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; dense?: boolean }> = ({
|
||||
audit,
|
||||
dense = false,
|
||||
}) => {
|
||||
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} dense={dense} />;
|
||||
};
|
||||
|
||||
export const LastLighthouseAuditCard: FC<{ dense?: boolean }> = ({
|
||||
dense = false,
|
||||
}) => {
|
||||
const { value: website, loading, error } = useWebsiteForEntity();
|
||||
|
||||
let content;
|
||||
if (loading) {
|
||||
content = <Progress />;
|
||||
}
|
||||
if (error) {
|
||||
content = null;
|
||||
}
|
||||
if (website) {
|
||||
content = (
|
||||
<LighthouseAuditSummary audit={website.lastAudit} dense={dense} />
|
||||
);
|
||||
}
|
||||
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';
|
||||
|
||||
@@ -49,12 +49,11 @@ export const JobStatusModal = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (jobStatus === 'COMPLETED') {
|
||||
onComplete(job!);
|
||||
setDialogTitle('Successfully created component');
|
||||
} else if (jobStatus === 'FAILED') {
|
||||
onComplete(job!);
|
||||
} else if (jobStatus === 'FAILED')
|
||||
setDialogTitle('Failed to create component');
|
||||
}
|
||||
}, [jobStatus, onComplete]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [jobStatus, onComplete, setDialogTitle]);
|
||||
|
||||
return (
|
||||
<Dialog open onClose={onClose} fullWidth>
|
||||
|
||||
@@ -4566,6 +4566,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"
|
||||
@@ -5678,6 +5686,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"
|
||||
|
||||
Reference in New Issue
Block a user