Merge branch 'master' into alund/transparent
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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 249 KiB |
@@ -74,6 +74,7 @@ app.
|
||||
```
|
||||
app
|
||||
├── app-config.yaml
|
||||
├── catalog-info.yaml
|
||||
├── lerna.json
|
||||
├── package.json
|
||||
└── packages
|
||||
@@ -83,6 +84,9 @@ app
|
||||
|
||||
- **app-config.yaml**: Main configuration file for the app. See
|
||||
[Configuration](https://backstage.io/docs/conf/) for more information.
|
||||
- **catalog-info.yaml**: Catalog Entities descriptors. See
|
||||
[Descriptor Format of Catalog Entities](https://backstage.io/docs/features/software-catalog/descriptor-format)
|
||||
to get started.
|
||||
- **lerna.json**: Contains information about workspaces and other lerna
|
||||
configuration needed for the monorepo setup.
|
||||
- **package.json**: Root package.json for the project. _Note: Be sure that you
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"classnames": "^2.2.6",
|
||||
"clsx": "^1.1.0",
|
||||
"lodash": "^4.17.15",
|
||||
"material-table": "1.68.0",
|
||||
"material-table": "^1.69.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"rc-progress": "^3.0.0",
|
||||
"react": "^16.12.0",
|
||||
|
||||
+8
-8
@@ -15,41 +15,41 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { GaugeProgress } from './GaugeProgress';
|
||||
import { Gauge } from './Gauge';
|
||||
|
||||
const containerStyle = { width: 300 };
|
||||
|
||||
export default {
|
||||
title: 'GaugeProgress',
|
||||
component: GaugeProgress,
|
||||
title: 'Gauge',
|
||||
component: Gauge,
|
||||
};
|
||||
|
||||
export const Default = () => (
|
||||
<div style={containerStyle}>
|
||||
<GaugeProgress value={0.8} />
|
||||
<Gauge value={0.8} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const MediumProgress = () => (
|
||||
<div style={containerStyle}>
|
||||
<GaugeProgress value={0.5} />
|
||||
<Gauge value={0.5} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const LowProgress = () => (
|
||||
<div style={containerStyle}>
|
||||
<GaugeProgress value={0.2} />
|
||||
<Gauge value={0.2} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const InverseLowProgress = () => (
|
||||
<div style={containerStyle}>
|
||||
<GaugeProgress value={0.2} inverse />
|
||||
<Gauge value={0.2} inverse />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const AbsoluteProgress = () => (
|
||||
<div style={containerStyle}>
|
||||
<GaugeProgress value={89.2} fractional={false} unit="m/s" />
|
||||
<Gauge value={89.2} fractional={false} unit="m/s" />
|
||||
</div>
|
||||
);
|
||||
+6
-6
@@ -17,32 +17,32 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { GaugeProgress, getProgressColor } from './GaugeProgress';
|
||||
import { Gauge, getProgressColor } from './Gauge';
|
||||
|
||||
describe('<GaugeProgress />', () => {
|
||||
describe('<Gauge />', () => {
|
||||
it('renders without exploding', () => {
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(<GaugeProgress value={10} fractional={false} />),
|
||||
wrapInTestApp(<Gauge value={10} fractional={false} />),
|
||||
);
|
||||
getByText('10%');
|
||||
});
|
||||
it('handles fractional prop', () => {
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(<GaugeProgress value={0.1} fractional />),
|
||||
wrapInTestApp(<Gauge value={0.1} fractional />),
|
||||
);
|
||||
getByText('10%');
|
||||
});
|
||||
|
||||
it('handles max prop', () => {
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(<GaugeProgress value={1} max={10} fractional={false} />),
|
||||
wrapInTestApp(<Gauge value={1} max={10} fractional={false} />),
|
||||
);
|
||||
getByText('1%');
|
||||
});
|
||||
|
||||
it('handles unit prop', () => {
|
||||
const { getByText } = render(
|
||||
wrapInTestApp(<GaugeProgress value={10} fractional={false} unit="m" />),
|
||||
wrapInTestApp(<Gauge value={10} fractional={false} unit="m" />),
|
||||
);
|
||||
getByText('10m');
|
||||
});
|
||||
+1
-1
@@ -77,7 +77,7 @@ export function getProgressColor(
|
||||
return palette.status.ok;
|
||||
}
|
||||
|
||||
export const GaugeProgress: FC<Props> = props => {
|
||||
export const Gauge: FC<Props> = props => {
|
||||
const classes = useStyles(props);
|
||||
const theme = useTheme<BackstageTheme>();
|
||||
const { value, fractional, inverse, unit, max } = {
|
||||
@@ -18,7 +18,7 @@ import React, { FC } from 'react';
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
import { InfoCard } from '../../layout/InfoCard';
|
||||
import { BottomLinkProps } from '../../layout/BottomLink';
|
||||
import { GaugeProgress } from './GaugeProgress';
|
||||
import { Gauge } from './Gauge';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
@@ -48,7 +48,7 @@ export const GaugeCard: FC<Props> = props => {
|
||||
deepLink={deepLink}
|
||||
variant={variant}
|
||||
>
|
||||
<GaugeProgress value={progress} />
|
||||
<Gauge value={progress} />
|
||||
</InfoCard>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ import { Tooltip, useTheme } from '@material-ui/core';
|
||||
// @ts-ignore
|
||||
import { Line } from 'rc-progress';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import { getProgressColor } from './GaugeProgress';
|
||||
import { getProgressColor } from './Gauge';
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
*/
|
||||
|
||||
export { GaugeCard } from './GaugeCard';
|
||||
export { GaugeProgress } from './GaugeProgress';
|
||||
export { Gauge } from './Gauge';
|
||||
export { LinearGauge } from './LinearGauge';
|
||||
|
||||
@@ -65,7 +65,12 @@ export const ErrorPage = ({
|
||||
Go back
|
||||
</Link>
|
||||
... or if you think this is a bug, please file an{' '}
|
||||
<Link href="https://github.com/spotify/backstage/issues">issue.</Link>
|
||||
<Link
|
||||
href="https://github.com/spotify/backstage/issues"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
issue.
|
||||
</Link>
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: {{name}}
|
||||
description: An example of a Backstage application.
|
||||
# Example for optional annotations
|
||||
# annotations:
|
||||
# github.com/project-slug: spotify/backstage
|
||||
# backstage.io/techdocs-ref: github:https://github.com/spotify/backstage.git
|
||||
spec:
|
||||
type: website
|
||||
owner: john@example.com
|
||||
lifecycle: experimental
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @param {import('knex')} knex
|
||||
*/
|
||||
exports.up = async function up(knex) {
|
||||
await knex.schema.alterTable('entities', table => {
|
||||
table.text('full_name').nullable();
|
||||
});
|
||||
|
||||
await knex('entities').update({
|
||||
full_name: knex.raw(
|
||||
"LOWER(kind) || ':' || LOWER(COALESCE(namespace, 'default')) || '/' || LOWER(name)",
|
||||
),
|
||||
});
|
||||
|
||||
try {
|
||||
await knex.schema.alterTable('entities', table => {
|
||||
table.text('full_name').notNullable().alter();
|
||||
});
|
||||
} catch (e) {
|
||||
// SQLite does not support alter column, ignore
|
||||
}
|
||||
|
||||
await knex.schema.alterTable('entities', table => {
|
||||
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta
|
||||
table.unique(['full_name'], 'entities_unique_full_name');
|
||||
table.dropUnique([], 'entities_unique_name');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('knex')} knex
|
||||
*/
|
||||
exports.down = async function down(knex) {
|
||||
await knex.schema.alterTable('entities', table => {
|
||||
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta
|
||||
table.dropUnique([], 'entities_unique_full_name');
|
||||
table.unique(['kind', 'namespace', 'name'], 'entities_unique_name');
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('entities_search', table => {
|
||||
table.dropColumn('full_name');
|
||||
});
|
||||
};
|
||||
@@ -383,11 +383,18 @@ export class CommonDatabase implements Database {
|
||||
locationId: string | undefined,
|
||||
entity: Entity,
|
||||
): DbEntitiesRow {
|
||||
const lowerKind = entity.kind.toLowerCase();
|
||||
const lowerNamespace = (
|
||||
entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE
|
||||
).toLowerCase();
|
||||
const lowerName = entity.metadata.name.toLowerCase();
|
||||
|
||||
return {
|
||||
id: entity.metadata.uid!,
|
||||
location_id: locationId || null,
|
||||
etag: entity.metadata.etag!,
|
||||
generation: entity.metadata.generation!,
|
||||
full_name: `${lowerKind}:${lowerNamespace}/${lowerName}`,
|
||||
api_version: entity.apiVersion,
|
||||
kind: entity.kind,
|
||||
name: entity.metadata.name,
|
||||
|
||||
@@ -25,6 +25,7 @@ export type DbEntitiesRow = {
|
||||
namespace: string | null;
|
||||
etag: string;
|
||||
generation: number;
|
||||
full_name: string;
|
||||
metadata: string;
|
||||
spec: string | null;
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -60,7 +60,10 @@ const WidgetContent = ({
|
||||
metadata={{
|
||||
status: (
|
||||
<>
|
||||
<WorkflowRunStatus status={lastRun.status} />
|
||||
<WorkflowRunStatus
|
||||
status={lastRun.status}
|
||||
conclusion={lastRun.conclusion}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
message: lastRun.message,
|
||||
|
||||
@@ -106,7 +106,10 @@ const StepView = ({ step }: { step: Step }) => {
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<WorkflowRunStatus status={step.status.toUpperCase()} />
|
||||
<WorkflowRunStatus
|
||||
status={step.status.toUpperCase()}
|
||||
conclusion={step.conclusion.toUpperCase()}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
@@ -204,7 +207,10 @@ export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => {
|
||||
<Typography noWrap>Status</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<WorkflowRunStatus status={details.value?.status} />
|
||||
<WorkflowRunStatus
|
||||
status={details.value?.status}
|
||||
conclusion={details.value?.conclusion}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
|
||||
@@ -14,13 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { StatusPending, StatusRunning, StatusOK } from '@backstage/core';
|
||||
import {
|
||||
StatusPending,
|
||||
StatusRunning,
|
||||
StatusOK,
|
||||
StatusWarning,
|
||||
StatusAborted,
|
||||
StatusError,
|
||||
} from '@backstage/core';
|
||||
import React from 'react';
|
||||
|
||||
export const WorkflowRunStatus = ({
|
||||
status,
|
||||
conclusion,
|
||||
}: {
|
||||
status: string | undefined;
|
||||
conclusion: string | undefined;
|
||||
}) => {
|
||||
if (status === undefined) return null;
|
||||
switch (status.toLowerCase()) {
|
||||
@@ -37,11 +46,32 @@ export const WorkflowRunStatus = ({
|
||||
</>
|
||||
);
|
||||
case 'completed':
|
||||
return (
|
||||
<>
|
||||
<StatusOK /> Completed
|
||||
</>
|
||||
);
|
||||
switch (conclusion?.toLowerCase()) {
|
||||
case 'skipped' || 'canceled':
|
||||
return (
|
||||
<>
|
||||
<StatusAborted /> Aborted
|
||||
</>
|
||||
);
|
||||
case 'timed_out':
|
||||
return (
|
||||
<>
|
||||
<StatusWarning /> Timed out
|
||||
</>
|
||||
);
|
||||
case 'failure':
|
||||
return (
|
||||
<>
|
||||
<StatusError /> Error
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
<StatusOK /> Completed
|
||||
</>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -46,6 +46,7 @@ export type WorkflowRun = {
|
||||
};
|
||||
};
|
||||
status: string;
|
||||
conclusion: string;
|
||||
onReRunClick: () => void;
|
||||
};
|
||||
|
||||
@@ -84,7 +85,7 @@ const generatedColumns: TableColumn[] = [
|
||||
|
||||
render: (row: Partial<WorkflowRun>) => (
|
||||
<Box display="flex" alignItems="center">
|
||||
<WorkflowRunStatus status={row.status} />
|
||||
<WorkflowRunStatus status={row.status} conclusion={row.conclusion} />
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -87,6 +87,7 @@ export function useWorkflowRuns({
|
||||
},
|
||||
},
|
||||
status: run.status,
|
||||
conclusion: run.conclusion,
|
||||
url: run.url,
|
||||
githubUrl: run.html_url,
|
||||
}));
|
||||
|
||||
@@ -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`
|
||||
|
||||
+8
-3
@@ -43,6 +43,7 @@ describe('MultiTenantConfigClusterLocator', () => {
|
||||
{
|
||||
name: 'cluster1',
|
||||
url: 'http://localhost:8080',
|
||||
authProvider: 'serviceAccount',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -60,6 +61,7 @@ describe('MultiTenantConfigClusterLocator', () => {
|
||||
name: 'cluster1',
|
||||
serviceAccountToken: undefined,
|
||||
url: 'http://localhost:8080',
|
||||
authProvider: 'serviceAccount',
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -70,13 +72,14 @@ describe('MultiTenantConfigClusterLocator', () => {
|
||||
clusters: [
|
||||
{
|
||||
name: 'cluster1',
|
||||
serviceAccountToken: undefined,
|
||||
serviceAccountToken: 'token',
|
||||
url: 'http://localhost:8080',
|
||||
authProvider: 'serviceAccount',
|
||||
},
|
||||
{
|
||||
name: 'cluster2',
|
||||
serviceAccountToken: undefined,
|
||||
url: 'http://localhost:8081',
|
||||
authProvider: 'google',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -92,13 +95,15 @@ describe('MultiTenantConfigClusterLocator', () => {
|
||||
expect(result).toStrictEqual([
|
||||
{
|
||||
name: 'cluster1',
|
||||
serviceAccountToken: undefined,
|
||||
serviceAccountToken: 'token',
|
||||
url: 'http://localhost:8080',
|
||||
authProvider: 'serviceAccount',
|
||||
},
|
||||
{
|
||||
name: 'cluster2',
|
||||
serviceAccountToken: undefined,
|
||||
url: 'http://localhost:8081',
|
||||
authProvider: 'google',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -28,12 +28,15 @@ export class MultiTenantConfigClusterLocator
|
||||
}
|
||||
|
||||
static fromConfig(config: Config[]): MultiTenantConfigClusterLocator {
|
||||
// TODO: Add validation that authProvider is required and serviceAccountToken
|
||||
// is required if authProvider is serviceAccount
|
||||
return new MultiTenantConfigClusterLocator(
|
||||
config.map(c => {
|
||||
return {
|
||||
name: c.getString('name'),
|
||||
url: c.getString('url'),
|
||||
serviceAccountToken: c.getOptionalString('serviceAccountToken'),
|
||||
authProvider: c.getString('authProvider'),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
|
||||
export type ClusterLocatorMethod = 'configMultiTenant' | 'http';
|
||||
export type AuthProviderType = 'google' | 'serviceAccount';
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 { KubernetesAuthTranslator } from './types';
|
||||
import { AuthRequestBody, ClusterDetails } from '../types/types';
|
||||
|
||||
export class GoogleKubernetesAuthTranslator
|
||||
implements KubernetesAuthTranslator {
|
||||
async decorateClusterDetailsWithAuth(
|
||||
clusterDetails: ClusterDetails,
|
||||
requestBody: AuthRequestBody,
|
||||
): Promise<ClusterDetails> {
|
||||
const clusterDetailsWithAuthToken: ClusterDetails = Object.assign(
|
||||
{},
|
||||
clusterDetails,
|
||||
);
|
||||
const authToken: string | undefined = requestBody.auth?.google;
|
||||
|
||||
if (authToken) {
|
||||
clusterDetailsWithAuthToken.serviceAccountToken = authToken;
|
||||
} else {
|
||||
throw new Error(
|
||||
'Google token not found under auth.google in request body',
|
||||
);
|
||||
}
|
||||
return clusterDetailsWithAuthToken;
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 { KubernetesAuthTranslator } from './types';
|
||||
import { GoogleKubernetesAuthTranslator } from './GoogleKubernetesAuthTranslator';
|
||||
import { KubernetesAuthTranslatorGenerator } from './KubernetesAuthTranslatorGenerator';
|
||||
import { ServiceAccountKubernetesAuthTranslator } from './ServiceAccountKubernetesAuthTranslator';
|
||||
|
||||
describe('getKubernetesAuthTranslatorInstance', () => {
|
||||
const sut = KubernetesAuthTranslatorGenerator;
|
||||
|
||||
it('can return an auth translator for google auth', () => {
|
||||
const authTranslator: KubernetesAuthTranslator = sut.getKubernetesAuthTranslatorInstance(
|
||||
'google',
|
||||
);
|
||||
expect(authTranslator instanceof GoogleKubernetesAuthTranslator).toBe(true);
|
||||
});
|
||||
|
||||
it('can return an auth translator for serviceAccount auth', () => {
|
||||
const authTranslator: KubernetesAuthTranslator = sut.getKubernetesAuthTranslatorInstance(
|
||||
'serviceAccount',
|
||||
);
|
||||
expect(
|
||||
authTranslator instanceof ServiceAccountKubernetesAuthTranslator,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('throws an error when asked for an auth translator for an unsupported auth type', () => {
|
||||
expect(() => sut.getKubernetesAuthTranslatorInstance('linode')).toThrow(
|
||||
'authProvider "linode" has no KubernetesAuthTranslator associated with it',
|
||||
);
|
||||
});
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 { KubernetesAuthTranslator } from './types';
|
||||
import { GoogleKubernetesAuthTranslator } from './GoogleKubernetesAuthTranslator';
|
||||
import { ServiceAccountKubernetesAuthTranslator } from './ServiceAccountKubernetesAuthTranslator';
|
||||
|
||||
export class KubernetesAuthTranslatorGenerator {
|
||||
static getKubernetesAuthTranslatorInstance(
|
||||
authProvider: String,
|
||||
): KubernetesAuthTranslator {
|
||||
switch (authProvider) {
|
||||
case 'google': {
|
||||
return new GoogleKubernetesAuthTranslator();
|
||||
}
|
||||
case 'serviceAccount': {
|
||||
return new ServiceAccountKubernetesAuthTranslator();
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`authProvider "${authProvider}" has no KubernetesAuthTranslator associated with it`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -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 { KubernetesAuthTranslator } from './types';
|
||||
import { AuthRequestBody, ClusterDetails } from '../types/types';
|
||||
|
||||
export class ServiceAccountKubernetesAuthTranslator
|
||||
implements KubernetesAuthTranslator {
|
||||
async decorateClusterDetailsWithAuth(
|
||||
clusterDetails: ClusterDetails,
|
||||
// To ignore TS6133 linting error where it detects 'requestBody' is declared but its value is never read.
|
||||
// @ts-ignore-start
|
||||
requestBody: AuthRequestBody, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
// @ts-ignore-end
|
||||
): Promise<ClusterDetails> {
|
||||
return clusterDetails;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 { AuthRequestBody, ClusterDetails } from '../types/types';
|
||||
|
||||
export interface KubernetesAuthTranslator {
|
||||
decorateClusterDetailsWithAuth(
|
||||
clusterDetails: ClusterDetails,
|
||||
requestBody: AuthRequestBody,
|
||||
): Promise<ClusterDetails>;
|
||||
}
|
||||
@@ -33,6 +33,7 @@ describe('KubernetesClientProvider', () => {
|
||||
name: 'cluster-name',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'TOKEN',
|
||||
authProvider: 'serviceAccount',
|
||||
});
|
||||
|
||||
expect(result.basePath).toBe('http://localhost:9999');
|
||||
@@ -55,12 +56,14 @@ describe('KubernetesClientProvider', () => {
|
||||
name: 'cluster-name',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'TOKEN',
|
||||
authProvider: 'serviceAccount',
|
||||
});
|
||||
|
||||
const result2 = sut.getCoreClientByClusterDetails({
|
||||
name: 'cluster-name',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'TOKEN',
|
||||
authProvider: 'serviceAccount',
|
||||
});
|
||||
|
||||
expect(result1.basePath).toBe('http://localhost:9999');
|
||||
@@ -89,6 +92,7 @@ describe('KubernetesClientProvider', () => {
|
||||
name: 'cluster-name',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'TOKEN',
|
||||
authProvider: 'serviceAccount',
|
||||
});
|
||||
|
||||
expect(result.basePath).toBe('http://localhost:9999');
|
||||
@@ -111,12 +115,14 @@ describe('KubernetesClientProvider', () => {
|
||||
name: 'cluster-name',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'TOKEN',
|
||||
authProvider: 'serviceAccount',
|
||||
});
|
||||
|
||||
const result2 = sut.getAppsClientByClusterDetails({
|
||||
name: 'cluster-name',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: 'TOKEN',
|
||||
authProvider: 'serviceAccount',
|
||||
});
|
||||
|
||||
expect(result1.basePath).toBe('http://localhost:9999');
|
||||
|
||||
@@ -62,7 +62,8 @@ describe('KubernetesClientProvider', () => {
|
||||
{
|
||||
name: 'cluster1',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: undefined,
|
||||
serviceAccountToken: 'token',
|
||||
authProvider: 'serviceAccount',
|
||||
},
|
||||
new Set(['pods', 'services']),
|
||||
);
|
||||
@@ -124,7 +125,8 @@ describe('KubernetesClientProvider', () => {
|
||||
{
|
||||
name: 'cluster1',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: undefined,
|
||||
serviceAccountToken: 'token',
|
||||
authProvider: 'serviceAccount',
|
||||
},
|
||||
new Set(['pods', 'services']),
|
||||
);
|
||||
@@ -172,7 +174,8 @@ describe('KubernetesClientProvider', () => {
|
||||
{
|
||||
name: 'cluster1',
|
||||
url: 'http://localhost:9999',
|
||||
serviceAccountToken: undefined,
|
||||
serviceAccountToken: 'token',
|
||||
authProvider: 'serviceAccount',
|
||||
},
|
||||
new Set<any>(['foo']),
|
||||
),
|
||||
|
||||
@@ -74,6 +74,7 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
|
||||
Promise.resolve([
|
||||
{
|
||||
name: 'test-cluster',
|
||||
authProvider: 'serviceAccount',
|
||||
},
|
||||
]),
|
||||
);
|
||||
@@ -89,6 +90,7 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
|
||||
getClusterByServiceId,
|
||||
},
|
||||
getVoidLogger(),
|
||||
{},
|
||||
);
|
||||
|
||||
expect(getClusterByServiceId.mock.calls.length).toBe(1);
|
||||
@@ -142,9 +144,11 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
|
||||
Promise.resolve([
|
||||
{
|
||||
name: 'test-cluster',
|
||||
authProvider: 'serviceAccount',
|
||||
},
|
||||
{
|
||||
name: 'other-cluster',
|
||||
authProvider: 'google',
|
||||
},
|
||||
]),
|
||||
);
|
||||
@@ -160,6 +164,11 @@ describe('handleGetKubernetesObjectsByServiceId', () => {
|
||||
getClusterByServiceId,
|
||||
},
|
||||
getVoidLogger(),
|
||||
{
|
||||
auth: {
|
||||
google: 'google_token_123',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(getClusterByServiceId.mock.calls.length).toBe(1);
|
||||
|
||||
@@ -16,17 +16,22 @@
|
||||
|
||||
import { Logger } from 'winston';
|
||||
import {
|
||||
AuthRequestBody,
|
||||
ClusterDetails,
|
||||
KubernetesClusterLocator,
|
||||
KubernetesFetcher,
|
||||
KubernetesObjectTypes,
|
||||
ObjectsByServiceIdResponse,
|
||||
} from '..';
|
||||
} from '../types/types';
|
||||
import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types';
|
||||
import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator/KubernetesAuthTranslatorGenerator';
|
||||
|
||||
export type GetKubernetesObjectsByServiceIdHandler = (
|
||||
serviceId: string,
|
||||
fetcher: KubernetesFetcher,
|
||||
clusterLocator: KubernetesClusterLocator,
|
||||
logger: Logger,
|
||||
requestBody: AuthRequestBody,
|
||||
objectsToFetch?: Set<KubernetesObjectTypes>,
|
||||
) => Promise<ObjectsByServiceIdResponse>;
|
||||
|
||||
@@ -46,18 +51,35 @@ export const handleGetKubernetesObjectsByServiceId: GetKubernetesObjectsByServic
|
||||
fetcher,
|
||||
clusterLocator,
|
||||
logger,
|
||||
requestBody,
|
||||
objectsToFetch = DEFAULT_OBJECTS,
|
||||
) => {
|
||||
const clusterDetails = await clusterLocator.getClusterByServiceId(serviceId);
|
||||
const clusterDetails: ClusterDetails[] = await clusterLocator.getClusterByServiceId(
|
||||
serviceId,
|
||||
);
|
||||
|
||||
// Execute all of these async actions simultaneously/without blocking sequentially as no common object is modified by them
|
||||
const promises: Promise<ClusterDetails>[] = clusterDetails.map(cd => {
|
||||
const kubernetesAuthTranslator: KubernetesAuthTranslator = KubernetesAuthTranslatorGenerator.getKubernetesAuthTranslatorInstance(
|
||||
cd.authProvider,
|
||||
);
|
||||
return kubernetesAuthTranslator.decorateClusterDetailsWithAuth(
|
||||
cd,
|
||||
requestBody,
|
||||
);
|
||||
});
|
||||
const clusterDetailsDecoratedForAuth: ClusterDetails[] = await Promise.all(
|
||||
promises,
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`serviceId=${serviceId} clusterDetails=[${clusterDetails
|
||||
`serviceId=${serviceId} clusterDetails=[${clusterDetailsDecoratedForAuth
|
||||
.map(c => c.name)
|
||||
.join(', ')}]`,
|
||||
);
|
||||
|
||||
return Promise.all(
|
||||
clusterDetails.map(cd => {
|
||||
clusterDetailsDecoratedForAuth.map(cd => {
|
||||
return fetcher
|
||||
.fetchObjectsByServiceId(serviceId, cd, objectsToFetch)
|
||||
.then(result => {
|
||||
|
||||
@@ -54,8 +54,8 @@ describe('router', () => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('GET /services/:serviceId', () => {
|
||||
it('happy path: lists kubernetes objects', async () => {
|
||||
describe('post /services/:serviceId', () => {
|
||||
it('happy path: lists kubernetes objects without auth in request body', async () => {
|
||||
const result = {
|
||||
clusterOne: {
|
||||
pods: [
|
||||
@@ -69,7 +69,34 @@ describe('router', () => {
|
||||
} as any;
|
||||
handleGetByServiceId.mockReturnValueOnce(Promise.resolve(result));
|
||||
|
||||
const response = await request(app).get('/services/test-service');
|
||||
const response = await request(app).post('/services/test-service');
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(result);
|
||||
});
|
||||
|
||||
it('happy path: lists kubernetes objects with auth in request body', async () => {
|
||||
const result = {
|
||||
clusterOne: {
|
||||
pods: [
|
||||
{
|
||||
metadata: {
|
||||
name: 'pod1',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any;
|
||||
handleGetByServiceId.mockReturnValueOnce(Promise.resolve(result));
|
||||
|
||||
const response = await request(app)
|
||||
.post('/services/test-service')
|
||||
.send({
|
||||
auth: {
|
||||
google: 'google_token_123',
|
||||
},
|
||||
})
|
||||
.set('Content-Type', 'application/json');
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(result);
|
||||
@@ -78,7 +105,7 @@ describe('router', () => {
|
||||
it('internal error: lists kubernetes objects', async () => {
|
||||
handleGetByServiceId.mockRejectedValue(Error('some internal error'));
|
||||
|
||||
const response = await request(app).get('/services/test-service');
|
||||
const response = await request(app).post('/services/test-service');
|
||||
|
||||
expect(response.status).toEqual(500);
|
||||
expect(response.body).toEqual({ error: 'some internal error' });
|
||||
|
||||
@@ -26,7 +26,11 @@ import {
|
||||
GetKubernetesObjectsByServiceIdHandler,
|
||||
handleGetKubernetesObjectsByServiceId,
|
||||
} from './getKubernetesObjectsByServiceIdHandler';
|
||||
import { KubernetesClusterLocator, KubernetesFetcher } from '..';
|
||||
import {
|
||||
AuthRequestBody,
|
||||
KubernetesClusterLocator,
|
||||
KubernetesFetcher,
|
||||
} from '../types/types';
|
||||
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
@@ -62,15 +66,16 @@ export const makeRouter = (
|
||||
router.use(express.json());
|
||||
|
||||
// TODO error handling
|
||||
router.get('/services/:serviceId', async (req, res) => {
|
||||
router.post('/services/:serviceId', async (req, res) => {
|
||||
const serviceId = req.params.serviceId;
|
||||
|
||||
const requestBody: AuthRequestBody = req.body;
|
||||
try {
|
||||
const response = await handleGetByServiceId(
|
||||
serviceId,
|
||||
fetcher,
|
||||
clusterLocator,
|
||||
logger,
|
||||
requestBody,
|
||||
);
|
||||
res.send(response);
|
||||
} catch (e) {
|
||||
|
||||
@@ -27,8 +27,14 @@ import {
|
||||
export interface ClusterDetails {
|
||||
name: string;
|
||||
url: string;
|
||||
// TODO this will eventually be configured by the auth translation work
|
||||
serviceAccountToken: string | undefined;
|
||||
authProvider: string;
|
||||
serviceAccountToken?: string | undefined;
|
||||
}
|
||||
|
||||
export interface AuthRequestBody {
|
||||
auth?: {
|
||||
google?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ClusterObjects {
|
||||
|
||||
@@ -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>
|
||||
```
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.24",
|
||||
"@backstage/config": "^0.1.1-alpha.24",
|
||||
"@backstage/core": "^0.1.1-alpha.24",
|
||||
"@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.24",
|
||||
"@backstage/theme": "^0.1.1-alpha.24",
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
import { DiscoveryApi } from '@backstage/core';
|
||||
import { KubernetesApi } from './types';
|
||||
import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend';
|
||||
import {
|
||||
AuthRequestBody,
|
||||
ObjectsByServiceIdResponse,
|
||||
} from '@backstage/plugin-kubernetes-backend';
|
||||
|
||||
export class KubernetesBackendClient implements KubernetesApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
@@ -25,9 +28,18 @@ export class KubernetesBackendClient implements KubernetesApi {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
}
|
||||
|
||||
private async getRequired(path: string): Promise<any> {
|
||||
private async getRequired(
|
||||
path: string,
|
||||
requestBody: AuthRequestBody,
|
||||
): Promise<any> {
|
||||
const url = `${await this.discoveryApi.getBaseUrl('kubernetes')}${path}`;
|
||||
const response = await fetch(url);
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
@@ -40,7 +52,8 @@ export class KubernetesBackendClient implements KubernetesApi {
|
||||
|
||||
async getObjectsByServiceId(
|
||||
serviceId: String,
|
||||
requestBody: AuthRequestBody,
|
||||
): Promise<ObjectsByServiceIdResponse> {
|
||||
return await this.getRequired(`/services/${serviceId}`);
|
||||
return await this.getRequired(`/services/${serviceId}`, requestBody);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
*/
|
||||
|
||||
import { createApiRef } from '@backstage/core';
|
||||
import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend';
|
||||
import {
|
||||
AuthRequestBody,
|
||||
ObjectsByServiceIdResponse,
|
||||
} from '@backstage/plugin-kubernetes-backend';
|
||||
|
||||
export const kubernetesApiRef = createApiRef<KubernetesApi>({
|
||||
id: 'plugin.kubernetes.service',
|
||||
@@ -24,5 +27,8 @@ export const kubernetesApiRef = createApiRef<KubernetesApi>({
|
||||
});
|
||||
|
||||
export interface KubernetesApi {
|
||||
getObjectsByServiceId(serviceId: String): Promise<ObjectsByServiceIdResponse>;
|
||||
getObjectsByServiceId(
|
||||
serviceId: String,
|
||||
requestBody: AuthRequestBody,
|
||||
): Promise<ObjectsByServiceIdResponse>;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import { Grid, TabProps } from '@material-ui/core';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
CardTab,
|
||||
configApiRef,
|
||||
Content,
|
||||
Page,
|
||||
pageTheme,
|
||||
@@ -28,10 +30,12 @@ import {
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { kubernetesApiRef } from '../../api/types';
|
||||
import {
|
||||
AuthRequestBody,
|
||||
ClusterObjects,
|
||||
FetchResponse,
|
||||
ObjectsByServiceIdResponse,
|
||||
} from '@backstage/plugin-kubernetes-backend';
|
||||
import { kubernetesAuthProvidersApiRef } from '../../kubernetes-auth-provider/types';
|
||||
import { DeploymentTables } from '../DeploymentTables';
|
||||
import { DeploymentTriple } from '../../types/types';
|
||||
import {
|
||||
@@ -105,16 +109,40 @@ export const KubernetesContent = ({ entity }: KubernetesContentProps) => {
|
||||
>(undefined);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
const configApi = useApi(configApiRef);
|
||||
const clusters: Config[] = configApi.getConfigArray('kubernetes.clusters');
|
||||
const allAuthProviders: string[] = clusters.map(c =>
|
||||
c.getString('authProvider'),
|
||||
);
|
||||
const authProviders: string[] = [...new Set(allAuthProviders)];
|
||||
|
||||
const kubernetesAuthProvidersApi = useApi(kubernetesAuthProvidersApiRef);
|
||||
|
||||
useEffect(() => {
|
||||
kubernetesApi
|
||||
.getObjectsByServiceId(entity.metadata.name)
|
||||
.then(result => {
|
||||
setKubernetesObjects(result);
|
||||
})
|
||||
.catch(e => {
|
||||
setError(e.message);
|
||||
});
|
||||
}, [entity.metadata.name, kubernetesApi]);
|
||||
(async () => {
|
||||
// For each auth type, invoke decorateRequestBodyForAuth on corresponding KubernetesAuthProvider
|
||||
let requestBody: AuthRequestBody = {};
|
||||
for (const authProviderStr of authProviders) {
|
||||
// Multiple asyncs done sequentially instead of all at once to prevent same requestBody from being modified simultaneously
|
||||
requestBody = await kubernetesAuthProvidersApi.decorateRequestBodyForAuth(
|
||||
authProviderStr,
|
||||
requestBody,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Add validation on contents/format of requestBody
|
||||
kubernetesApi
|
||||
.getObjectsByServiceId(entity.metadata.name, requestBody)
|
||||
.then(result => {
|
||||
setKubernetesObjects(result);
|
||||
})
|
||||
.catch(e => {
|
||||
setError(e.message);
|
||||
});
|
||||
})();
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
}, [entity.metadata.name, kubernetesApi, kubernetesAuthProvidersApi]);
|
||||
/* eslint-enable react-hooks/exhaustive-deps */
|
||||
|
||||
const clustersWithErrors =
|
||||
kubernetesObjects?.items.filter(r => r.errors.length > 0) ?? [];
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 { OAuthApi } from '@backstage/core';
|
||||
import { KubernetesAuthProvider } from './types';
|
||||
import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
|
||||
|
||||
export class GoogleKubernetesAuthProvider implements KubernetesAuthProvider {
|
||||
authProvider: OAuthApi;
|
||||
|
||||
constructor(authProvider: OAuthApi) {
|
||||
this.authProvider = authProvider;
|
||||
}
|
||||
|
||||
async decorateRequestBodyForAuth(
|
||||
requestBody: AuthRequestBody,
|
||||
): Promise<AuthRequestBody> {
|
||||
const googleAuthToken: string = await this.authProvider.getAccessToken(
|
||||
'https://www.googleapis.com/auth/cloud-platform',
|
||||
);
|
||||
if ('auth' in requestBody) {
|
||||
requestBody.auth!.google = googleAuthToken;
|
||||
} else {
|
||||
requestBody.auth = { google: googleAuthToken };
|
||||
}
|
||||
return requestBody;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 { OAuthApi } from '@backstage/core';
|
||||
import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
|
||||
import { KubernetesAuthProvider, KubernetesAuthProvidersApi } from './types';
|
||||
import { GoogleKubernetesAuthProvider } from './GoogleKubernetesAuthProvider';
|
||||
import { ServiceAccountKubernetesAuthProvider } from './ServiceAccountKubernetesAuthProvider';
|
||||
|
||||
export class KubernetesAuthProviders implements KubernetesAuthProvidersApi {
|
||||
private readonly kubernetesAuthProviderMap: Map<
|
||||
string,
|
||||
KubernetesAuthProvider
|
||||
>;
|
||||
|
||||
constructor(options: { googleAuthApi: OAuthApi }) {
|
||||
this.kubernetesAuthProviderMap = new Map<string, KubernetesAuthProvider>();
|
||||
this.kubernetesAuthProviderMap.set(
|
||||
'google',
|
||||
new GoogleKubernetesAuthProvider(options.googleAuthApi),
|
||||
);
|
||||
this.kubernetesAuthProviderMap.set(
|
||||
'serviceAccount',
|
||||
new ServiceAccountKubernetesAuthProvider(),
|
||||
);
|
||||
}
|
||||
|
||||
async decorateRequestBodyForAuth(
|
||||
authProvider: string,
|
||||
requestBody: AuthRequestBody,
|
||||
): Promise<AuthRequestBody> {
|
||||
const kubernetesAuthProvider:
|
||||
| KubernetesAuthProvider
|
||||
| undefined = this.kubernetesAuthProviderMap.get(authProvider);
|
||||
if (kubernetesAuthProvider) {
|
||||
return await kubernetesAuthProvider.decorateRequestBodyForAuth(
|
||||
requestBody,
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
`authProvider "${authProvider}" has no KubernetesAuthProvider defined for it`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 { KubernetesAuthProvider } from './types';
|
||||
import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
|
||||
|
||||
export class ServiceAccountKubernetesAuthProvider
|
||||
implements KubernetesAuthProvider {
|
||||
async decorateRequestBodyForAuth(
|
||||
requestBody: AuthRequestBody,
|
||||
): Promise<AuthRequestBody> {
|
||||
// No-op, with service account for auth, cluster config/details should already have serviceAccountToken
|
||||
return requestBody;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 { createApiRef } from '@backstage/core';
|
||||
import { AuthRequestBody } from '@backstage/plugin-kubernetes-backend';
|
||||
|
||||
export interface KubernetesAuthProvider {
|
||||
decorateRequestBodyForAuth(
|
||||
requestBody: AuthRequestBody,
|
||||
): Promise<AuthRequestBody>;
|
||||
}
|
||||
|
||||
export const kubernetesAuthProvidersApiRef = createApiRef<
|
||||
KubernetesAuthProvidersApi
|
||||
>({
|
||||
id: 'plugin.kubernetes-auth-providers.service',
|
||||
description: 'Used by the Kubernetes plugin to fetch KubernetesAuthProviders',
|
||||
});
|
||||
|
||||
export interface KubernetesAuthProvidersApi {
|
||||
decorateRequestBodyForAuth(
|
||||
authProvider: string,
|
||||
requestBody: AuthRequestBody,
|
||||
): Promise<AuthRequestBody>;
|
||||
}
|
||||
@@ -18,9 +18,12 @@ import {
|
||||
createPlugin,
|
||||
createRouteRef,
|
||||
discoveryApiRef,
|
||||
googleAuthApiRef,
|
||||
} from '@backstage/core';
|
||||
import { KubernetesBackendClient } from './api/KubernetesBackendClient';
|
||||
import { kubernetesApiRef } from './api/types';
|
||||
import { kubernetesAuthProvidersApiRef } from './kubernetes-auth-provider/types';
|
||||
import { KubernetesAuthProviders } from './kubernetes-auth-provider/KubernetesAuthProviders';
|
||||
|
||||
export const rootCatalogKubernetesRouteRef = createRouteRef({
|
||||
path: '*',
|
||||
@@ -36,5 +39,12 @@ export const plugin = createPlugin({
|
||||
factory: ({ discoveryApi }) =>
|
||||
new KubernetesBackendClient({ discoveryApi }),
|
||||
}),
|
||||
createApiFactory({
|
||||
api: kubernetesAuthProvidersApiRef,
|
||||
deps: { googleAuthApi: googleAuthApiRef },
|
||||
factory: ({ googleAuthApi }) => {
|
||||
return new KubernetesAuthProviders({ googleAuthApi });
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -66,7 +66,11 @@ export const TechDocsPageHeader = ({
|
||||
<HeaderLabel
|
||||
label=""
|
||||
value={
|
||||
<a href={locationMetadata.target} target="_blank">
|
||||
<a
|
||||
href={locationMetadata.target}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<GitHubIcon style={{ marginTop: '-25px', fill: '#fff' }} />
|
||||
</a>
|
||||
}
|
||||
|
||||
@@ -69,7 +69,10 @@ const WelcomePage = () => {
|
||||
the project and we have already begun work on various aspects
|
||||
of these phases. The best way to keep track of the progress is
|
||||
through the
|
||||
<Link href="https://github.com/spotify/backstage/milestones">
|
||||
<Link
|
||||
href="https://github.com/spotify/backstage/milestones"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Milestones
|
||||
</Link>
|
||||
.
|
||||
@@ -113,7 +116,10 @@ const WelcomePage = () => {
|
||||
</Typography>
|
||||
<Typography variant="body1" paragraph>
|
||||
We suggest you either check out the documentation for{' '}
|
||||
<Link href="https://github.com/spotify/backstage/blob/master/docs/plugins/create-a-plugin.md">
|
||||
<Link
|
||||
href="https://github.com/spotify/backstage/blob/master/docs/plugins/create-a-plugin.md"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
creating a plugin
|
||||
</Link>{' '}
|
||||
or have a look in the code for the{' '}
|
||||
@@ -121,7 +127,10 @@ const WelcomePage = () => {
|
||||
existing plugins
|
||||
</Link>{' '}
|
||||
in the directory{' '}
|
||||
<Link href="https://github.com/spotify/backstage/tree/master/plugins">
|
||||
<Link
|
||||
href="https://github.com/spotify/backstage/tree/master/plugins"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<code>plugins/</code>
|
||||
</Link>
|
||||
.
|
||||
@@ -135,7 +144,10 @@ const WelcomePage = () => {
|
||||
<Link href="https://backstage.io">backstage.io</Link>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Link href="https://github.com/spotify/backstage/blob/master/docs/plugins/create-a-plugin.md">
|
||||
<Link
|
||||
href="https://github.com/spotify/backstage/blob/master/docs/plugins/create-a-plugin.md"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Create a plugin
|
||||
</Link>
|
||||
</ListItem>
|
||||
|
||||
@@ -1194,7 +1194,7 @@
|
||||
core-js-pure "^3.0.0"
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
version "7.11.2"
|
||||
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
|
||||
integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
|
||||
@@ -4520,6 +4520,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"
|
||||
@@ -5328,6 +5336,11 @@
|
||||
resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.4.tgz#a59e851c1ba16c0513ea123830dd639a0a15cb6a"
|
||||
integrity sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ==
|
||||
|
||||
"@types/raf@^3.4.0":
|
||||
version "3.4.0"
|
||||
resolved "https://registry.npmjs.org/@types/raf/-/raf-3.4.0.tgz#2b72cbd55405e071f1c4d29992638e022b20acc2"
|
||||
integrity sha512-taW5/WYqo36N7V39oYyHP9Ipfd5pNFvGTIQsNGj86xV88YQ7GnI30/yMfKDF7Zgin0m3e+ikX88FvImnK4RjGw==
|
||||
|
||||
"@types/range-parser@*":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
|
||||
@@ -5632,6 +5645,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"
|
||||
@@ -5999,11 +6019,6 @@ JSONStream@^1.0.4, JSONStream@^1.3.4:
|
||||
jsonparse "^1.2.0"
|
||||
through ">=2.2.7 <3"
|
||||
|
||||
abab@^1.0.0:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
|
||||
integrity sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=
|
||||
|
||||
abab@^2.0.0, abab@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a"
|
||||
@@ -6027,13 +6042,6 @@ accepts@^1.3.5, accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
|
||||
mime-types "~2.1.24"
|
||||
negotiator "0.6.2"
|
||||
|
||||
acorn-globals@^1.0.4:
|
||||
version "1.0.9"
|
||||
resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz#55bb5e98691507b74579d0513413217c380c54cf"
|
||||
integrity sha1-VbtemGkVB7dFedBRNBMhfDgMVM8=
|
||||
dependencies:
|
||||
acorn "^2.1.0"
|
||||
|
||||
acorn-globals@^4.1.0:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7"
|
||||
@@ -6065,11 +6073,6 @@ acorn-walk@^7.1.1:
|
||||
resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e"
|
||||
integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==
|
||||
|
||||
acorn@^2.1.0, acorn@^2.4.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz#ab6e7d9d886aaca8b085bc3312b79a198433f0e7"
|
||||
integrity sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=
|
||||
|
||||
acorn@^5.5.3:
|
||||
version "5.7.4"
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
|
||||
@@ -7288,10 +7291,10 @@ balanced-match@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
|
||||
|
||||
base64-arraybuffer@^0.1.5:
|
||||
version "0.1.5"
|
||||
resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
|
||||
integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
|
||||
base64-arraybuffer@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45"
|
||||
integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==
|
||||
|
||||
base64-js@^1.0.2, base64-js@^1.2.0:
|
||||
version "1.3.1"
|
||||
@@ -7980,15 +7983,17 @@ canvas@^2.6.1:
|
||||
node-pre-gyp "^0.11.0"
|
||||
simple-get "^3.0.3"
|
||||
|
||||
canvg@1.5.3:
|
||||
version "1.5.3"
|
||||
resolved "https://registry.npmjs.org/canvg/-/canvg-1.5.3.tgz#aad17915f33368bf8eb80b25d129e3ae922ddc5f"
|
||||
integrity sha512-7Gn2IuQzvUQWPIuZuFHrzsTM0gkPz2RRT9OcbdmA03jeKk8kltrD8gqUzNX15ghY/4PV5bbe5lmD6yDLDY6Ybg==
|
||||
canvg@^3.0.6:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.npmjs.org/canvg/-/canvg-3.0.6.tgz#4f82a34acc433daa06c494fc255420cbbb05f903"
|
||||
integrity sha512-eFUy8R/4DgocR93LF8lr+YUxW4PYblUe/Q1gz2osk/cI5n8AsYdassvln0D9QPhLXQ6Lx7l8hwtT8FLvOn2Ihg==
|
||||
dependencies:
|
||||
jsdom "^8.1.0"
|
||||
"@babel/runtime" "^7.6.3"
|
||||
"@types/raf" "^3.4.0"
|
||||
core-js "3"
|
||||
raf "^3.4.1"
|
||||
rgbcolor "^1.0.1"
|
||||
stackblur-canvas "^1.4.1"
|
||||
xmldom "^0.1.22"
|
||||
stackblur-canvas "^2.0.0"
|
||||
|
||||
capture-exit@^2.0.0:
|
||||
version "2.0.0"
|
||||
@@ -8866,16 +8871,16 @@ core-js-pure@^3.0.0, core-js-pure@^3.0.1:
|
||||
resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a"
|
||||
integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==
|
||||
|
||||
core-js@3, core-js@^3.0.1, core-js@^3.0.4, core-js@^3.5.0, core-js@^3.6.0, core-js@^3.6.5:
|
||||
version "3.6.5"
|
||||
resolved "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
|
||||
integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
|
||||
|
||||
core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.7, core-js@^2.6.10, core-js@^2.6.11, core-js@^2.6.5:
|
||||
version "2.6.11"
|
||||
resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
|
||||
integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
|
||||
|
||||
core-js@^3.0.1, core-js@^3.0.4, core-js@^3.5.0, core-js@^3.6.5:
|
||||
version "3.6.5"
|
||||
resolved "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
|
||||
integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
|
||||
|
||||
core-util-is@1.0.2, core-util-is@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||
@@ -9062,12 +9067,12 @@ css-in-js-utils@^2.0.0:
|
||||
hyphenate-style-name "^1.0.2"
|
||||
isobject "^3.0.1"
|
||||
|
||||
css-line-break@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/css-line-break/-/css-line-break-1.0.1.tgz#19f2063a33e95fb2831b86446c0b80c188af450a"
|
||||
integrity sha1-GfIGOjPpX7KDG4ZEbAuAwYivRQo=
|
||||
css-line-break@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz#d5e9bdd297840099eb0503c7310fd34927a026ef"
|
||||
integrity sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==
|
||||
dependencies:
|
||||
base64-arraybuffer "^0.1.5"
|
||||
base64-arraybuffer "^0.2.0"
|
||||
|
||||
css-loader@^3.5.3:
|
||||
version "3.6.0"
|
||||
@@ -9268,7 +9273,7 @@ csso@^4.0.2:
|
||||
dependencies:
|
||||
css-tree "1.0.0-alpha.37"
|
||||
|
||||
cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0", "cssom@>= 0.3.2 < 0.4.0", cssom@~0.3.6:
|
||||
cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0", cssom@~0.3.6:
|
||||
version "0.3.8"
|
||||
resolved "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
|
||||
integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==
|
||||
@@ -9278,13 +9283,6 @@ cssom@^0.4.4:
|
||||
resolved "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
|
||||
integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==
|
||||
|
||||
"cssstyle@>= 0.2.34 < 0.3.0":
|
||||
version "0.2.37"
|
||||
resolved "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz#541097234cb2513c83ceed3acddc27ff27987d54"
|
||||
integrity sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=
|
||||
dependencies:
|
||||
cssom "0.3.x"
|
||||
|
||||
cssstyle@^1.0.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1"
|
||||
@@ -10092,6 +10090,11 @@ dompurify@^1.0.11:
|
||||
resolved "https://registry.npmjs.org/dompurify/-/dompurify-1.0.11.tgz#fe0f4a40d147f7cebbe31a50a1357539cfc1eb4d"
|
||||
integrity sha512-XywCTXZtc/qCX3iprD1pIklRVk/uhl8BKpkTxr+ZyMVUzSUg7wkQXRBp/euJ5J5moa1QvfpvaPQVP71z1O59dQ==
|
||||
|
||||
dompurify@^2.0.12:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.1.1.tgz#b5aa988676b093a9c836d8b855680a8598af25fe"
|
||||
integrity sha512-NijiNVkS/OL8mdQL1hUbCD6uty/cgFpmNiuFxrmJ5YPH2cXrPKIewoixoji56rbZ6XBPmtM8GA8/sf9unlSuwg==
|
||||
|
||||
dompurify@^2.0.7:
|
||||
version "2.0.12"
|
||||
resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.0.12.tgz#284a2b041e1c60b8e72d7b4d2fadad36141254ae"
|
||||
@@ -10552,7 +10555,7 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
|
||||
resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
|
||||
|
||||
escodegen@^1.14.1, escodegen@^1.6.1, escodegen@^1.9.1:
|
||||
escodegen@^1.14.1, escodegen@^1.9.1:
|
||||
version "1.14.3"
|
||||
resolved "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503"
|
||||
integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==
|
||||
@@ -11347,10 +11350,6 @@ file-loader@^6.0.0:
|
||||
loader-utils "^2.0.0"
|
||||
schema-utils "^2.7.1"
|
||||
|
||||
file-saver@eligrey/FileSaver.js#1.3.8:
|
||||
version "1.3.8"
|
||||
resolved "https://codeload.github.com/eligrey/FileSaver.js/tar.gz/e865e37af9f9947ddcced76b549e27dc45c1cb2e"
|
||||
|
||||
file-system-cache@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.npmjs.org/file-system-cache/-/file-system-cache-1.0.5.tgz#84259b36a2bbb8d3d6eb1021d3132ffe64cfff4f"
|
||||
@@ -12795,12 +12794,12 @@ html-webpack-plugin@^4.2.1, html-webpack-plugin@^4.3.0:
|
||||
tapable "^1.1.3"
|
||||
util.promisify "1.0.0"
|
||||
|
||||
html2canvas@1.0.0-alpha.12:
|
||||
version "1.0.0-alpha.12"
|
||||
resolved "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-alpha.12.tgz#3b1992e3c9b3f56063c35fd620494f37eba88513"
|
||||
integrity sha1-OxmS48mz9WBjw1/WIElPN+uohRM=
|
||||
html2canvas@^1.0.0-rc.5:
|
||||
version "1.0.0-rc.7"
|
||||
resolved "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-rc.7.tgz#70c159ce0e63954a91169531894d08ad5627ac98"
|
||||
integrity sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA==
|
||||
dependencies:
|
||||
css-line-break "1.0.1"
|
||||
css-line-break "1.1.1"
|
||||
|
||||
htmlparser2@^3.3.0:
|
||||
version "3.10.1"
|
||||
@@ -13009,7 +13008,7 @@ hyphenate-style-name@^1.0.2, hyphenate-style-name@^1.0.3:
|
||||
resolved "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48"
|
||||
integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ==
|
||||
|
||||
iconv-lite@0.4.24, iconv-lite@^0.4.13, iconv-lite@^0.4.21, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
|
||||
iconv-lite@0.4.24, iconv-lite@^0.4.21, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||
@@ -14538,29 +14537,6 @@ jsdom@^16.2.2:
|
||||
ws "^7.2.3"
|
||||
xml-name-validator "^3.0.0"
|
||||
|
||||
jsdom@^8.1.0:
|
||||
version "8.5.0"
|
||||
resolved "https://registry.npmjs.org/jsdom/-/jsdom-8.5.0.tgz#d4d8f5dbf2768635b62a62823b947cf7071ebc98"
|
||||
integrity sha1-1Nj12/J2hjW2KmKCO5R89wcevJg=
|
||||
dependencies:
|
||||
abab "^1.0.0"
|
||||
acorn "^2.4.0"
|
||||
acorn-globals "^1.0.4"
|
||||
array-equal "^1.0.0"
|
||||
cssom ">= 0.3.0 < 0.4.0"
|
||||
cssstyle ">= 0.2.34 < 0.3.0"
|
||||
escodegen "^1.6.1"
|
||||
iconv-lite "^0.4.13"
|
||||
nwmatcher ">= 1.3.7 < 2.0.0"
|
||||
parse5 "^1.5.1"
|
||||
request "^2.55.0"
|
||||
sax "^1.1.4"
|
||||
symbol-tree ">= 3.1.0 < 4.0.0"
|
||||
tough-cookie "^2.2.0"
|
||||
webidl-conversions "^3.0.1"
|
||||
whatwg-url "^2.0.1"
|
||||
xml-name-validator ">= 2.0.1 < 3.0.0"
|
||||
|
||||
jsesc@^2.5.1:
|
||||
version "2.5.2"
|
||||
resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
|
||||
@@ -14759,22 +14735,23 @@ jsonwebtoken@^8.5.1:
|
||||
ms "^2.1.1"
|
||||
semver "^5.6.0"
|
||||
|
||||
jspdf-autotable@3.5.3:
|
||||
version "3.5.3"
|
||||
resolved "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-3.5.3.tgz#2f73adb07f340e7dbf22950e3e6c8bf853991479"
|
||||
integrity sha512-K+cNWW3x6w0R/1B5m6PYOm6v8CTTDXy/g32lZouc7SuC6zhvzMN2dauhk6dDYxPD0pky0oyPIJFwSJ/tV8PAeg==
|
||||
jspdf-autotable@3.5.9:
|
||||
version "3.5.9"
|
||||
resolved "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-3.5.9.tgz#8a625ef2aead44271da95e9f649843c401536925"
|
||||
integrity sha512-ZRfiI5P7leJuWmvC0jGVXu227m68C2Jfz1dkDckshmDYDeVFCGxwIBYdCUXJ8Eb2CyFQC2ok82fEWO+xRDovDQ==
|
||||
|
||||
jspdf@1.5.3:
|
||||
version "1.5.3"
|
||||
resolved "https://registry.npmjs.org/jspdf/-/jspdf-1.5.3.tgz#5a12c011479defabef5735de55c913060ed219f2"
|
||||
integrity sha512-J9X76xnncMw+wIqb15HeWfPMqPwYxSpPY8yWPJ7rAZN/ZDzFkjCSZObryCyUe8zbrVRNiuCnIeQteCzMn7GnWw==
|
||||
jspdf@2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/jspdf/-/jspdf-2.1.0.tgz#2322f8644bc41845b3abe20db4c3ca0adeadb84c"
|
||||
integrity sha512-NQygqZEKhSw+nExySJxB72Ge/027YEyIM450Vh/hgay/H9cgZNnkXXOQPRspe9EuCW4sq92zg8hpAXyyBdnaIQ==
|
||||
dependencies:
|
||||
canvg "1.5.3"
|
||||
file-saver eligrey/FileSaver.js#1.3.8
|
||||
html2canvas "1.0.0-alpha.12"
|
||||
omggif "1.0.7"
|
||||
promise-polyfill "8.1.0"
|
||||
stackblur-canvas "2.2.0"
|
||||
atob "^2.1.2"
|
||||
btoa "^1.2.1"
|
||||
optionalDependencies:
|
||||
canvg "^3.0.6"
|
||||
core-js "^3.6.0"
|
||||
dompurify "^2.0.12"
|
||||
html2canvas "^1.0.0-rc.5"
|
||||
|
||||
jsprim@^1.2.2:
|
||||
version "1.4.1"
|
||||
@@ -15762,10 +15739,10 @@ material-colors@^1.2.1:
|
||||
resolved "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
|
||||
integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==
|
||||
|
||||
material-table@1.68.0:
|
||||
version "1.68.0"
|
||||
resolved "https://registry.npmjs.org/material-table/-/material-table-1.68.0.tgz#275c3d9a885c40ae4bc5a7461c00e877f92397b9"
|
||||
integrity sha512-dyJJaVsS3m+i6sn71AvYcVdA1P9X1XiUOM2PekfvEeeMtkdQb66oChGkk77ndYi3Ja6j4DovGVNrgeVLwXLZiw==
|
||||
material-table@^1.69.1:
|
||||
version "1.69.1"
|
||||
resolved "https://registry.npmjs.org/material-table/-/material-table-1.69.1.tgz#8d1c8b23207f18bd3328cae1b5775ede284682e6"
|
||||
integrity sha512-7MA8kMtr8ToPE6gNUbOGIb4g+RGOLWK8s9gXZYNwFtg6fGAjWEJ+iqBrMmdq7fkMmTRcyOd7/sC/5OPPY8CNGg==
|
||||
dependencies:
|
||||
"@date-io/date-fns" "^1.1.0"
|
||||
"@material-ui/pickers" "^3.2.2"
|
||||
@@ -15774,8 +15751,8 @@ material-table@1.68.0:
|
||||
debounce "^1.2.0"
|
||||
fast-deep-equal "2.0.1"
|
||||
filefy "0.1.10"
|
||||
jspdf "1.5.3"
|
||||
jspdf-autotable "3.5.3"
|
||||
jspdf "2.1.0"
|
||||
jspdf-autotable "3.5.9"
|
||||
prop-types "^15.6.2"
|
||||
react-beautiful-dnd "^13.0.0"
|
||||
react-double-scrollbar "0.0.15"
|
||||
@@ -16836,11 +16813,6 @@ number-is-nan@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
|
||||
integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
|
||||
|
||||
"nwmatcher@>= 1.3.7 < 2.0.0":
|
||||
version "1.4.4"
|
||||
resolved "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.4.tgz#2285631f34a95f0d0395cd900c96ed39b58f346e"
|
||||
integrity sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ==
|
||||
|
||||
nwsapi@^2.0.7, nwsapi@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
|
||||
@@ -17000,11 +16972,6 @@ oidc-token-hash@^5.0.0:
|
||||
resolved "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.0.tgz#acdfb1f4310f58e64d5d74a4e8671a426986e888"
|
||||
integrity sha512-8Yr4CZSv+Tn8ZkN3iN2i2w2G92mUKClp4z7EGUfdsERiYSbj7P4i/NHm72ft+aUdsiFx9UdIPSTwbyzQ6C4URg==
|
||||
|
||||
omggif@1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.npmjs.org/omggif/-/omggif-1.0.7.tgz#59d2eecb0263de84635b3feb887c0c9973f1e49d"
|
||||
integrity sha1-WdLuywJj3oRjWz/riHwMmXPx5J0=
|
||||
|
||||
on-finished@^2.3.0, on-finished@~2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
||||
@@ -17540,11 +17507,6 @@ parse5@5.1.1:
|
||||
resolved "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
|
||||
integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
|
||||
|
||||
parse5@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94"
|
||||
integrity sha1-m387DeMr543CQBsXVzzK8Pb1nZQ=
|
||||
|
||||
parseurl@^1.3.2, parseurl@~1.3.2, parseurl@~1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||
@@ -18597,11 +18559,6 @@ promise-inflight@^1.0.1:
|
||||
resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
|
||||
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
|
||||
|
||||
promise-polyfill@8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.1.0.tgz#30059da54d1358ce905ac581f287e184aedf995d"
|
||||
integrity sha512-OzSf6gcCUQ01byV4BgwyUCswlaQQ6gzXc23aLQWhicvfX9kfsUiUhgt3CCQej8jDnl8/PhGF31JdHX2/MzF3WA==
|
||||
|
||||
promise-polyfill@^8.1.3:
|
||||
version "8.1.3"
|
||||
resolved "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.1.3.tgz#8c99b3cf53f3a91c68226ffde7bde81d7f904116"
|
||||
@@ -20074,7 +20031,7 @@ request-promise-native@^1.0.5, request-promise-native@^1.0.8:
|
||||
stealthy-require "^1.1.1"
|
||||
tough-cookie "^2.3.3"
|
||||
|
||||
request@^2.55.0, request@^2.85.0, request@^2.87.0, request@^2.88.0, request@^2.88.2:
|
||||
request@^2.85.0, request@^2.87.0, request@^2.88.0, request@^2.88.2:
|
||||
version "2.88.2"
|
||||
resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
|
||||
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
|
||||
@@ -20472,7 +20429,7 @@ sanitize-html@^1.27.0:
|
||||
srcset "^2.0.1"
|
||||
xtend "^4.0.1"
|
||||
|
||||
sax@>=0.6.0, sax@^1.1.4, sax@^1.2.4, sax@~1.2.4:
|
||||
sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||
@@ -21238,15 +21195,10 @@ stack-utils@^2.0.2:
|
||||
dependencies:
|
||||
escape-string-regexp "^2.0.0"
|
||||
|
||||
stackblur-canvas@2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.2.0.tgz#cacc5924a0744b3e183eb2e6c1d8559c1a17c26e"
|
||||
integrity sha512-5Gf8dtlf8k6NbLzuly2NkGrkS/Ahh+I5VUjO7TnFizdJtgpfpLLEdQlLe9umbcnZlitU84kfYjXE67xlSXfhfQ==
|
||||
|
||||
stackblur-canvas@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-1.4.1.tgz#849aa6f94b272ff26f6471fa4130ed1f7e47955b"
|
||||
integrity sha1-hJqm+UsnL/JvZHH6QTDtH35HlVs=
|
||||
stackblur-canvas@^2.0.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.4.0.tgz#2b2eba910cb46f6feae918e1c402f863d602c01b"
|
||||
integrity sha512-Z+HixfgYV0ss3C342DxPwc+UvN1SYWqoz7Wsi3xEDWEnaBkSCL3Ey21gF4io+WlLm8/RIrSnCrDBIEcH4O+q5Q==
|
||||
|
||||
stackframe@^1.1.1:
|
||||
version "1.1.1"
|
||||
@@ -21868,7 +21820,7 @@ symbol-observable@^1.0.4, symbol-observable@^1.1.0, symbol-observable@^1.2.0:
|
||||
resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
|
||||
|
||||
"symbol-tree@>= 3.1.0 < 4.0.0", symbol-tree@^3.2.2, symbol-tree@^3.2.4:
|
||||
symbol-tree@^3.2.2, symbol-tree@^3.2.4:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
|
||||
@@ -22348,7 +22300,7 @@ touch@^3.1.0:
|
||||
dependencies:
|
||||
nopt "~1.0.10"
|
||||
|
||||
tough-cookie@^2.2.0, tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0:
|
||||
tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
|
||||
integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
|
||||
@@ -22379,11 +22331,6 @@ tr46@^2.0.2:
|
||||
dependencies:
|
||||
punycode "^2.1.1"
|
||||
|
||||
tr46@~0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
|
||||
|
||||
traverse@~0.6.6:
|
||||
version "0.6.6"
|
||||
resolved "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"
|
||||
@@ -23317,11 +23264,6 @@ webapi-parser@^0.5.0:
|
||||
dependencies:
|
||||
ajv "6.5.2"
|
||||
|
||||
webidl-conversions@^3.0.0, webidl-conversions@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
|
||||
|
||||
webidl-conversions@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
|
||||
@@ -23524,14 +23466,6 @@ whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0:
|
||||
resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
|
||||
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
|
||||
|
||||
whatwg-url@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-2.0.1.tgz#5396b2043f020ee6f704d9c45ea8519e724de659"
|
||||
integrity sha1-U5ayBD8CDub3BNnEXqhRnnJN5lk=
|
||||
dependencies:
|
||||
tr46 "~0.0.3"
|
||||
webidl-conversions "^3.0.0"
|
||||
|
||||
whatwg-url@^6.4.1:
|
||||
version "6.5.0"
|
||||
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
|
||||
@@ -23846,11 +23780,6 @@ xml-encryption@^1.0.0:
|
||||
xmldom "~0.1.15"
|
||||
xpath "0.0.27"
|
||||
|
||||
"xml-name-validator@>= 2.0.1 < 3.0.0":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635"
|
||||
integrity sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=
|
||||
|
||||
xml-name-validator@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
|
||||
@@ -23879,7 +23808,7 @@ xmldom@0.1.27:
|
||||
resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
|
||||
integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk=
|
||||
|
||||
xmldom@0.1.x, xmldom@^0.1.22, xmldom@~0.1.15:
|
||||
xmldom@0.1.x, xmldom@~0.1.15:
|
||||
version "0.1.31"
|
||||
resolved "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff"
|
||||
integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==
|
||||
|
||||
Reference in New Issue
Block a user