diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt
index 21045f04dd..c9774ae237 100644
--- a/.github/styles/vocab.txt
+++ b/.github/styles/vocab.txt
@@ -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
diff --git a/backstage_overview.png b/backstage_overview.png
deleted file mode 100644
index ae32ae6628..0000000000
Binary files a/backstage_overview.png and /dev/null differ
diff --git a/docs/getting-started/create-an-app.md b/docs/getting-started/create-an-app.md
index 831e8af145..9be0e24452 100644
--- a/docs/getting-started/create-an-app.md
+++ b/docs/getting-started/create-an-app.md
@@ -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
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index c2483b40ee..393ffe2631 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -49,6 +49,11 @@ import {
import { Entity } from '@backstage/catalog-model';
import { Button, Grid } from '@material-ui/core';
import { EmptyState } from '@backstage/core';
+import {
+ EmbeddedRouter as LighthouseRouter,
+ LastLighthouseAuditCard,
+ isPluginApplicableToEntity as isLighthouseAvailable,
+} from '@backstage/plugin-lighthouse/';
const CICDSwitcher = ({ entity }: { entity: Entity }) => {
// This component is just an example of how you can implement your company's logic in entity page.
@@ -115,6 +120,11 @@ const OverviewContent = ({ entity }: { entity: Entity }) => (
+ {isLighthouseAvailable(entity) && (
+
+
+
+ )}
);
@@ -165,6 +175,11 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
title="CI/CD"
element={}
/>
+ }
+ />
(
-
+
);
export const MediumProgress = () => (
-
+
);
export const LowProgress = () => (
-
+
);
export const InverseLowProgress = () => (
-
+
);
export const AbsoluteProgress = () => (
-
+
);
diff --git a/packages/core/src/components/ProgressBars/GaugeProgress.test.jsx b/packages/core/src/components/ProgressBars/Gauge.test.jsx
similarity index 82%
rename from packages/core/src/components/ProgressBars/GaugeProgress.test.jsx
rename to packages/core/src/components/ProgressBars/Gauge.test.jsx
index 778abdf12c..709de7b0c4 100644
--- a/packages/core/src/components/ProgressBars/GaugeProgress.test.jsx
+++ b/packages/core/src/components/ProgressBars/Gauge.test.jsx
@@ -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('', () => {
+describe('', () => {
it('renders without exploding', () => {
const { getByText } = render(
- wrapInTestApp(),
+ wrapInTestApp(),
);
getByText('10%');
});
it('handles fractional prop', () => {
const { getByText } = render(
- wrapInTestApp(),
+ wrapInTestApp(),
);
getByText('10%');
});
it('handles max prop', () => {
const { getByText } = render(
- wrapInTestApp(),
+ wrapInTestApp(),
);
getByText('1%');
});
it('handles unit prop', () => {
const { getByText } = render(
- wrapInTestApp(),
+ wrapInTestApp(),
);
getByText('10m');
});
diff --git a/packages/core/src/components/ProgressBars/GaugeProgress.tsx b/packages/core/src/components/ProgressBars/Gauge.tsx
similarity index 98%
rename from packages/core/src/components/ProgressBars/GaugeProgress.tsx
rename to packages/core/src/components/ProgressBars/Gauge.tsx
index 14776ed431..4c339bf8e1 100644
--- a/packages/core/src/components/ProgressBars/GaugeProgress.tsx
+++ b/packages/core/src/components/ProgressBars/Gauge.tsx
@@ -77,7 +77,7 @@ export function getProgressColor(
return palette.status.ok;
}
-export const GaugeProgress: FC = props => {
+export const Gauge: FC = props => {
const classes = useStyles(props);
const theme = useTheme();
const { value, fractional, inverse, unit, max } = {
diff --git a/packages/core/src/components/ProgressBars/GaugeCard.tsx b/packages/core/src/components/ProgressBars/GaugeCard.tsx
index fc7055f705..4eb4b2e075 100644
--- a/packages/core/src/components/ProgressBars/GaugeCard.tsx
+++ b/packages/core/src/components/ProgressBars/GaugeCard.tsx
@@ -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 => {
deepLink={deepLink}
variant={variant}
>
-
+
);
diff --git a/packages/core/src/components/ProgressBars/LinearGauge.tsx b/packages/core/src/components/ProgressBars/LinearGauge.tsx
index 73163b345f..2b8e77838d 100644
--- a/packages/core/src/components/ProgressBars/LinearGauge.tsx
+++ b/packages/core/src/components/ProgressBars/LinearGauge.tsx
@@ -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 = {
/**
diff --git a/packages/core/src/components/ProgressBars/index.ts b/packages/core/src/components/ProgressBars/index.ts
index c7131c8831..4463aea29b 100644
--- a/packages/core/src/components/ProgressBars/index.ts
+++ b/packages/core/src/components/ProgressBars/index.ts
@@ -15,5 +15,5 @@
*/
export { GaugeCard } from './GaugeCard';
-export { GaugeProgress } from './GaugeProgress';
+export { Gauge } from './Gauge';
export { LinearGauge } from './LinearGauge';
diff --git a/packages/core/src/layout/ErrorPage/ErrorPage.tsx b/packages/core/src/layout/ErrorPage/ErrorPage.tsx
index 71a24652ba..3d80955cc9 100644
--- a/packages/core/src/layout/ErrorPage/ErrorPage.tsx
+++ b/packages/core/src/layout/ErrorPage/ErrorPage.tsx
@@ -65,7 +65,12 @@ export const ErrorPage = ({
Go back
... or if you think this is a bug, please file an{' '}
- issue.
+
+ issue.
+
diff --git a/packages/create-app/templates/default-app/catalog-info.yaml.hbs b/packages/create-app/templates/default-app/catalog-info.yaml.hbs
new file mode 100644
index 0000000000..a370902020
--- /dev/null
+++ b/packages/create-app/templates/default-app/catalog-info.yaml.hbs
@@ -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
\ No newline at end of file
diff --git a/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js b/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js
new file mode 100644
index 0000000000..26cc98e74e
--- /dev/null
+++ b/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js
@@ -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');
+ });
+};
diff --git a/plugins/catalog-backend/src/database/CommonDatabase.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts
index 9d80b33940..5327384593 100644
--- a/plugins/catalog-backend/src/database/CommonDatabase.ts
+++ b/plugins/catalog-backend/src/database/CommonDatabase.ts
@@ -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,
diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts
index 8aaf212583..c0f5777743 100644
--- a/plugins/catalog-backend/src/database/types.ts
+++ b/plugins/catalog-backend/src/database/types.ts
@@ -25,6 +25,7 @@ export type DbEntitiesRow = {
namespace: string | null;
etag: string;
generation: number;
+ full_name: string;
metadata: string;
spec: string | null;
};
diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts
index a6d86e8102..762f4924c0 100644
--- a/plugins/catalog/src/index.ts
+++ b/plugins/catalog/src/index.ts
@@ -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';
diff --git a/plugins/github-actions/src/components/Cards/Cards.tsx b/plugins/github-actions/src/components/Cards/Cards.tsx
index 28783efdea..7e4d5f5fb6 100644
--- a/plugins/github-actions/src/components/Cards/Cards.tsx
+++ b/plugins/github-actions/src/components/Cards/Cards.tsx
@@ -60,7 +60,10 @@ const WidgetContent = ({
metadata={{
status: (
<>
-
+
>
),
message: lastRun.message,
diff --git a/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx b/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx
index fd2cc69376..7714ad6cb6 100644
--- a/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx
+++ b/plugins/github-actions/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx
@@ -106,7 +106,10 @@ const StepView = ({ step }: { step: Step }) => {
/>
-
+
);
@@ -204,7 +207,10 @@ export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => {
Status
-
+
diff --git a/plugins/github-actions/src/components/WorkflowRunStatus/WorkflowRunStatus.tsx b/plugins/github-actions/src/components/WorkflowRunStatus/WorkflowRunStatus.tsx
index 089c319802..b1a71ea3ed 100644
--- a/plugins/github-actions/src/components/WorkflowRunStatus/WorkflowRunStatus.tsx
+++ b/plugins/github-actions/src/components/WorkflowRunStatus/WorkflowRunStatus.tsx
@@ -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 (
- <>
- Completed
- >
- );
+ switch (conclusion?.toLowerCase()) {
+ case 'skipped' || 'canceled':
+ return (
+ <>
+ Aborted
+ >
+ );
+ case 'timed_out':
+ return (
+ <>
+ Timed out
+ >
+ );
+ case 'failure':
+ return (
+ <>
+ Error
+ >
+ );
+ default:
+ return (
+ <>
+ Completed
+ >
+ );
+ }
default:
return (
<>
diff --git a/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx b/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx
index 4e99be6926..fcde5fefbb 100644
--- a/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx
+++ b/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx
@@ -46,6 +46,7 @@ export type WorkflowRun = {
};
};
status: string;
+ conclusion: string;
onReRunClick: () => void;
};
@@ -84,7 +85,7 @@ const generatedColumns: TableColumn[] = [
render: (row: Partial) => (
-
+
),
},
diff --git a/plugins/github-actions/src/components/useWorkflowRuns.ts b/plugins/github-actions/src/components/useWorkflowRuns.ts
index 25ede3f997..d0be852beb 100644
--- a/plugins/github-actions/src/components/useWorkflowRuns.ts
+++ b/plugins/github-actions/src/components/useWorkflowRuns.ts
@@ -87,6 +87,7 @@ export function useWorkflowRuns({
},
},
status: run.status,
+ conclusion: run.conclusion,
url: run.url,
githubUrl: run.html_url,
}));
diff --git a/plugins/kubernetes-backend/README.md b/plugins/kubernetes-backend/README.md
index 0246da24ed..fbbde1fe65 100644
--- a/plugins/kubernetes-backend/README.md
+++ b/plugins/kubernetes-backend/README.md
@@ -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:
+ 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
diff --git a/plugins/kubernetes-backend/examples/dice-roller/README.md b/plugins/kubernetes-backend/examples/dice-roller/README.md
index 47fc345c73..f97f760c9f 100644
--- a/plugins/kubernetes-backend/examples/dice-roller/README.md
+++ b/plugins/kubernetes-backend/examples/dice-roller/README.md
@@ -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:
name: minikube
serviceAccountToken:
+ 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`
diff --git a/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts
index 34788d16cb..dd77a11bae 100644
--- a/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts
+++ b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts
@@ -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',
},
]);
});
diff --git a/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts
index 3d9e2dd8b4..5a1f0f3839 100644
--- a/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts
+++ b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts
@@ -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'),
};
}),
);
diff --git a/plugins/kubernetes-backend/src/cluster-locator/types.ts b/plugins/kubernetes-backend/src/cluster-locator/types.ts
index 9b793da558..dae8eb6d60 100644
--- a/plugins/kubernetes-backend/src/cluster-locator/types.ts
+++ b/plugins/kubernetes-backend/src/cluster-locator/types.ts
@@ -15,3 +15,4 @@
*/
export type ClusterLocatorMethod = 'configMultiTenant' | 'http';
+export type AuthProviderType = 'google' | 'serviceAccount';
diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts
new file mode 100644
index 0000000000..7f9b2f0bae
--- /dev/null
+++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/GoogleKubernetesAuthTranslator.ts
@@ -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 {
+ 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;
+ }
+}
diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts
new file mode 100644
index 0000000000..3b27eb012a
--- /dev/null
+++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts
@@ -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',
+ );
+ });
+});
diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts
new file mode 100644
index 0000000000..f7cfc92112
--- /dev/null
+++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts
@@ -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`,
+ );
+ }
+ }
+ }
+}
diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/ServiceAccountKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/ServiceAccountKubernetesAuthTranslator.ts
new file mode 100644
index 0000000000..ecf2f12b72
--- /dev/null
+++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/ServiceAccountKubernetesAuthTranslator.ts
@@ -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 {
+ return clusterDetails;
+ }
+}
diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/types.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/types.ts
new file mode 100644
index 0000000000..f89e04456f
--- /dev/null
+++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/types.ts
@@ -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;
+}
diff --git a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts
index a3dc67716a..409d41b783 100644
--- a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts
+++ b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts
@@ -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');
diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts
index 8f390f309b..6294b3a7ed 100644
--- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts
+++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts
@@ -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(['foo']),
),
diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts
index 56da3235cc..4218c64e80 100644
--- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts
+++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts
@@ -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);
diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts
index 1690b8be40..f0b21cbc0c 100644
--- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts
+++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts
@@ -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,
) => Promise;
@@ -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.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 => {
diff --git a/plugins/kubernetes-backend/src/service/router.test.ts b/plugins/kubernetes-backend/src/service/router.test.ts
index 7731fe9abb..4e9a206eb4 100644
--- a/plugins/kubernetes-backend/src/service/router.test.ts
+++ b/plugins/kubernetes-backend/src/service/router.test.ts
@@ -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' });
diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts
index bdb14bb74d..9e9b4ab2f5 100644
--- a/plugins/kubernetes-backend/src/service/router.ts
+++ b/plugins/kubernetes-backend/src/service/router.ts
@@ -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) {
diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts
index 0cb42eec1a..eb2817f268 100644
--- a/plugins/kubernetes-backend/src/types/types.ts
+++ b/plugins/kubernetes-backend/src/types/types.ts
@@ -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 {
diff --git a/plugins/kubernetes/README.md b/plugins/kubernetes/README.md
index 678c9a96a6..1ad478bd89 100644
--- a/plugins/kubernetes/README.md
+++ b/plugins/kubernetes/README.md
@@ -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':
+```
diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json
index 5dc9231c2a..2adefebbd6 100644
--- a/plugins/kubernetes/package.json
+++ b/plugins/kubernetes/package.json
@@ -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",
diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts
index cbb3b03a38..86bb830046 100644
--- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts
+++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts
@@ -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 {
+ private async getRequired(
+ path: string,
+ requestBody: AuthRequestBody,
+ ): Promise {
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 {
- return await this.getRequired(`/services/${serviceId}`);
+ return await this.getRequired(`/services/${serviceId}`, requestBody);
}
}
diff --git a/plugins/kubernetes/src/api/types.ts b/plugins/kubernetes/src/api/types.ts
index 5ec3cda6f8..8e44d58e6a 100644
--- a/plugins/kubernetes/src/api/types.ts
+++ b/plugins/kubernetes/src/api/types.ts
@@ -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({
id: 'plugin.kubernetes.service',
@@ -24,5 +27,8 @@ export const kubernetesApiRef = createApiRef({
});
export interface KubernetesApi {
- getObjectsByServiceId(serviceId: String): Promise;
+ getObjectsByServiceId(
+ serviceId: String,
+ requestBody: AuthRequestBody,
+ ): Promise;
}
diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx
index 6119884c43..013c7ad002 100644
--- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx
+++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx
@@ -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(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) ?? [];
diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts
new file mode 100644
index 0000000000..7de5c7c0a9
--- /dev/null
+++ b/plugins/kubernetes/src/kubernetes-auth-provider/GoogleKubernetesAuthProvider.ts
@@ -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 {
+ 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;
+ }
+}
diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts
new file mode 100644
index 0000000000..ac909d6695
--- /dev/null
+++ b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts
@@ -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();
+ this.kubernetesAuthProviderMap.set(
+ 'google',
+ new GoogleKubernetesAuthProvider(options.googleAuthApi),
+ );
+ this.kubernetesAuthProviderMap.set(
+ 'serviceAccount',
+ new ServiceAccountKubernetesAuthProvider(),
+ );
+ }
+
+ async decorateRequestBodyForAuth(
+ authProvider: string,
+ requestBody: AuthRequestBody,
+ ): Promise {
+ 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`,
+ );
+ }
+}
diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/ServiceAccountKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/ServiceAccountKubernetesAuthProvider.ts
new file mode 100644
index 0000000000..3ac5a9494b
--- /dev/null
+++ b/plugins/kubernetes/src/kubernetes-auth-provider/ServiceAccountKubernetesAuthProvider.ts
@@ -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 {
+ // No-op, with service account for auth, cluster config/details should already have serviceAccountToken
+ return requestBody;
+ }
+}
diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/types.ts b/plugins/kubernetes/src/kubernetes-auth-provider/types.ts
new file mode 100644
index 0000000000..24fee0f94c
--- /dev/null
+++ b/plugins/kubernetes/src/kubernetes-auth-provider/types.ts
@@ -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;
+}
+
+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;
+}
diff --git a/plugins/kubernetes/src/plugin.ts b/plugins/kubernetes/src/plugin.ts
index b7ad9615f5..cff9e1468b 100644
--- a/plugins/kubernetes/src/plugin.ts
+++ b/plugins/kubernetes/src/plugin.ts
@@ -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 });
+ },
+ }),
],
});
diff --git a/plugins/lighthouse/README.md b/plugins/lighthouse/README.md
index b5fecb85f8..19244f3cf0 100644
--- a/plugins/lighthouse/README.md
+++ b/plugins/lighthouse/README.md
@@ -57,3 +57,66 @@ Then configure the lighthouse service url in your [`app-config.yaml`](https://gi
lighthouse:
baseUrl: http://your-service-url
```
+
+### Integration with the Catalog
+
+The lighthouse plugin can be integrated into the catalog so that lighthouse audit information relating to a component
+can be displayed within that component's entity page. In order to link an Entity to its lighthouse audits the entity
+must be annotated as follows:
+
+```yaml
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ # ...
+ annotations:
+ # ...
+ lighthouse.com/website-url: # A single website url e.g. https://backstage.io/
+```
+
+> NOTE: The lighthouse plugin only supports one website url per component at this time.
+
+Add a lighthouse tab to the EntityPage:
+
+```tsx
+// packages/app/src/components/catalog/EntityPage.tsx
+import { EmbeddedRouter as LighthouseRouter } from '@backstage/plugin-lighthouse';
+
+// ...
+const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
+
+ // ...
+ }
+ />
+
+);
+```
+
+> NOTE: The embedded router renders page content without a header section allowing it to be rendered within a
+> catalog plugin page.
+
+Add a Lighthouse card to the overview tab on the EntityPage:
+
+```tsx
+// packages/app/src/components/catalog/EntityPage.tsx
+import {
+ LastLighthouseAuditCard,
+ isPluginApplicableToEntity as isLighthouseAvailable,
+} from '@backstage/plugin-lighthouse';
+
+// ...
+
+const OverviewContent = ({ entity }: { entity: Entity }) => (
+
+ // ...
+ {isLighthouseAvailable(entity) && (
+
+
+
+ )}
+
+);
+```
diff --git a/plugins/lighthouse/constants.ts b/plugins/lighthouse/constants.ts
new file mode 100644
index 0000000000..7a60e2be67
--- /dev/null
+++ b/plugins/lighthouse/constants.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export const LIGHTHOUSE_WEBSITE_URL_ANNOTATION = 'lighthouse.com/website-url';
diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json
index 0b30e48689..7446f09ed4 100644
--- a/plugins/lighthouse/package.json
+++ b/plugins/lighthouse/package.json
@@ -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",
diff --git a/plugins/lighthouse/src/Router.tsx b/plugins/lighthouse/src/Router.tsx
index 46843504fd..df481483c3 100644
--- a/plugins/lighthouse/src/Router.tsx
+++ b/plugins/lighthouse/src/Router.tsx
@@ -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 = () => (
@@ -28,3 +35,24 @@ export const Router = () => (
} />
);
+
+export const EmbeddedRouter = ({ entity }: { entity: Entity }) =>
+ !isPluginApplicableToEntity(entity) ? (
+
+ ) : (
+
+ } />
+ }
+ />
+ }
+ />
+
+ );
diff --git a/plugins/lighthouse/src/api.ts b/plugins/lighthouse/src/api.ts
index 5b6cd9aae7..03b42a38b9 100644
--- a/plugins/lighthouse/src/api.ts
+++ b/plugins/lighthouse/src/api.ts
@@ -104,6 +104,7 @@ export type LighthouseApi = {
getWebsiteList: (listOptions: LASListRequest) => Promise;
getWebsiteForAuditId: (auditId: string) => Promise;
triggerAudit: (payload: TriggerAuditPayload) => Promise;
+ getWebsiteByUrl: (websiteUrl: string) => Promise;
};
export const lighthouseApiRef = createApiRef({
@@ -150,4 +151,10 @@ export class LighthouseRestApi implements LighthouseApi {
},
});
}
+
+ async getWebsiteByUrl(websiteUrl: string): Promise {
+ return this.fetch(
+ `/v1/websites/${encodeURIComponent(websiteUrl)}`,
+ );
+ }
}
diff --git a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx
new file mode 100644
index 0000000000..7f10914d9c
--- /dev/null
+++ b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { render } from '@testing-library/react';
+import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core';
+import {
+ lighthouseApiRef,
+ LighthouseRestApi,
+ WebsiteListResponse,
+} from '../../api';
+import mockFetch from 'jest-fetch-mock';
+
+import * as data from '../../__fixtures__/website-list-response.json';
+import { EntityContext } from '@backstage/plugin-catalog';
+import { Entity } from '@backstage/catalog-model';
+import { AuditListForEntity } from './AuditListForEntity';
+import { lightTheme } from '@backstage/theme';
+import { ThemeProvider } from '@material-ui/core';
+import { MemoryRouter } from 'react-router-dom';
+import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity';
+
+jest.mock('../../hooks/useWebsiteForEntity', () => ({
+ useWebsiteForEntity: jest.fn(),
+}));
+
+const websiteListResponse = data as WebsiteListResponse;
+const entityWebsite = websiteListResponse.items[0];
+
+describe('', () => {
+ let apis: ApiRegistry;
+
+ const mockErrorApi: jest.Mocked = {
+ post: jest.fn(),
+ error$: jest.fn(),
+ };
+
+ beforeEach(() => {
+ apis = ApiRegistry.from([
+ [lighthouseApiRef, new LighthouseRestApi('http://lighthouse')],
+ [errorApiRef, mockErrorApi],
+ ]);
+ mockFetch.mockResponse(JSON.stringify(entityWebsite));
+ (useWebsiteForEntity as jest.Mock).mockReturnValue({
+ value: entityWebsite,
+ loading: false,
+ error: null,
+ });
+ });
+
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'software',
+ annotations: {
+ 'lighthouse.com/website-url': entityWebsite.url,
+ },
+ },
+ spec: {
+ owner: 'guest',
+ type: 'Website',
+ lifecycle: 'development',
+ },
+ };
+
+ const subject = (value = {}) =>
+ render(
+
+
+
+
+
+
+
+
+ ,
+ );
+
+ it('renders the audit list for the entity', async () => {
+ const { findByText } = subject();
+ expect(await findByText(entityWebsite.url)).toBeInTheDocument();
+ });
+
+ describe('where the data is loading', () => {
+ beforeEach(() => {
+ (useWebsiteForEntity as jest.Mock).mockReturnValue({
+ value: null,
+ loading: true,
+ error: null,
+ });
+ });
+
+ it('renders a Progress element', async () => {
+ const { findByTestId } = subject();
+ expect(await findByTestId('progress')).toBeInTheDocument();
+ });
+ });
+
+ describe('where there is an error loading data', () => {
+ beforeEach(() => {
+ (useWebsiteForEntity as jest.Mock).mockReturnValue({
+ value: null,
+ loading: false,
+ error: 'error',
+ });
+ });
+
+ it('renders nothing', async () => {
+ const { queryByTestId } = subject();
+ expect(await queryByTestId('AuditListTable')).toBeNull();
+ });
+ });
+
+ describe('where there is not data', () => {
+ beforeEach(() => {
+ (useWebsiteForEntity as jest.Mock).mockReturnValue({
+ value: null,
+ loading: false,
+ error: null,
+ });
+ });
+
+ it('renders nothing', async () => {
+ const { queryByTestId } = subject();
+ expect(await queryByTestId('AuditListTable')).toBeNull();
+ });
+ });
+});
diff --git a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.tsx b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.tsx
new file mode 100644
index 0000000000..616fd0fa8a
--- /dev/null
+++ b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.tsx
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { AuditListTable } from './AuditListTable';
+import { Progress } from '@backstage/core';
+import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity';
+
+export const AuditListForEntity = () => {
+ const { value, loading, error } = useWebsiteForEntity();
+ if (loading) {
+ return ;
+ }
+ if (error || !value) {
+ return null;
+ }
+
+ return ;
+};
diff --git a/plugins/lighthouse/src/components/AuditView/index.tsx b/plugins/lighthouse/src/components/AuditView/index.tsx
index d0e743a77f..54bd179418 100644
--- a/plugins/lighthouse/src/components/AuditView/index.tsx
+++ b/plugins/lighthouse/src/components/AuditView/index.tsx
@@ -124,7 +124,7 @@ const AuditView: FC<{ audit?: Audit }> = ({ audit }: { audit?: Audit }) => {
);
};
-const ConnectedAuditView: FC<{}> = () => {
+export const AuditViewContent: FC<{}> = () => {
const lighthouseApi = useApi(lighthouseApiRef);
const params = useParams() as { id: string };
const classes = useStyles();
@@ -173,32 +173,35 @@ const ConnectedAuditView: FC<{}> = () => {
}
return (
-
-
-
- navigate(`../../${createAuditButtonUrl}`)}
>
-
-
-
- {content}
-
-
+ Create New Audit
+
+
+
+ {content}
+ >
);
};
+const ConnectedAuditView = () => (
+
+
+
+
+
+
+);
+
export default ConnectedAuditView;
diff --git a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx
new file mode 100644
index 0000000000..20b3b9c072
--- /dev/null
+++ b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { render } from '@testing-library/react';
+import {
+ AuditCompleted,
+ LighthouseCategoryId,
+ WebsiteListResponse,
+} from '../../api';
+import { EntityContext } from '@backstage/plugin-catalog';
+import { Entity } from '@backstage/catalog-model';
+import { LastLighthouseAuditCard } from './LastLighthouseAuditCard';
+import { lightTheme } from '@backstage/theme';
+import { ThemeProvider } from '@material-ui/core';
+import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity';
+import { MemoryRouter } from 'react-router-dom';
+import * as data from '../../__fixtures__/website-list-response.json';
+
+jest.mock('../../hooks/useWebsiteForEntity', () => ({
+ useWebsiteForEntity: jest.fn(),
+}));
+
+const websiteListResponse = data as WebsiteListResponse;
+let entityWebsite = websiteListResponse.items[2];
+
+describe('', () => {
+ const asPercentage = (fraction: number) => `${fraction * 100}%`;
+
+ beforeEach(() => {
+ (useWebsiteForEntity as jest.Mock).mockReturnValue({
+ value: entityWebsite,
+ loading: false,
+ error: null,
+ });
+ });
+
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'software',
+ annotations: {
+ 'lighthouse.com/website-url': entityWebsite.url,
+ },
+ },
+ spec: {
+ owner: 'guest',
+ type: 'Website',
+ lifecycle: 'development',
+ },
+ };
+
+ const subject = (value = {}) =>
+ render(
+
+
+
+
+
+
+ ,
+ );
+
+ describe('where the last audit completed successfully', () => {
+ const audit = entityWebsite.lastAudit as AuditCompleted;
+
+ it('renders the performance data for the audit', async () => {
+ const { findByText } = subject();
+ expect(await findByText(audit.url)).toBeInTheDocument();
+ expect(await findByText(audit.status)).toBeInTheDocument();
+ for (const category of Object.keys(audit.categories)) {
+ const { score } = audit.categories[category as LighthouseCategoryId];
+ expect(await findByText(asPercentage(score))).toBeInTheDocument();
+ }
+ });
+
+ describe('where a category score is not a number', () => {
+ beforeEach(() => {
+ entityWebsite = { ...entityWebsite };
+ (entityWebsite.lastAudit as AuditCompleted).categories.accessibility.score = NaN;
+ });
+
+ afterEach(() => {
+ entityWebsite = websiteListResponse.items[2];
+ });
+
+ it('renders the performance data for the audit', async () => {
+ const { findByText } = subject();
+ expect(await findByText('N/A')).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('where the last audit is in running', () => {
+ const audit = websiteListResponse.items[0].lastAudit as AuditCompleted;
+
+ beforeEach(() => {
+ (useWebsiteForEntity as jest.Mock).mockReturnValue({
+ value: websiteListResponse.items[0],
+ loading: false,
+ error: null,
+ });
+ });
+
+ it('renders the url and status of the audit', async () => {
+ const { findByText } = subject();
+ expect(await findByText(audit.url)).toBeInTheDocument();
+ expect(await findByText(audit.status)).toBeInTheDocument();
+ });
+ });
+
+ describe('where the data is loading', () => {
+ beforeEach(() => {
+ (useWebsiteForEntity as jest.Mock).mockReturnValue({
+ value: null,
+ loading: true,
+ error: null,
+ });
+ });
+
+ it('renders a Progress element', async () => {
+ const { findByTestId } = subject();
+ expect(await findByTestId('progress')).toBeInTheDocument();
+ });
+ });
+
+ describe('where there is an error loading data', () => {
+ beforeEach(() => {
+ (useWebsiteForEntity as jest.Mock).mockReturnValue({
+ value: null,
+ loading: false,
+ error: 'error',
+ });
+ });
+
+ it('renders nothing', async () => {
+ const { queryByTestId } = subject();
+ expect(await queryByTestId('AuditListTable')).toBeNull();
+ });
+ });
+ //
+ describe('where there is no data', () => {
+ beforeEach(() => {
+ (useWebsiteForEntity as jest.Mock).mockReturnValue({
+ value: null,
+ loading: false,
+ error: null,
+ });
+ });
+
+ it('renders nothing', async () => {
+ const { queryByTestId } = subject();
+ expect(await queryByTestId('AuditListTable')).toBeNull();
+ });
+ });
+});
diff --git a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx
new file mode 100644
index 0000000000..d3bc362877
--- /dev/null
+++ b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx
@@ -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 (
+ <>
+
+ {scoreAsPercentage}%
+ >
+ );
+ case scoreAsPercentage >= 50 && scoreAsPercentage < 90:
+ return (
+ <>
+
+ {scoreAsPercentage}%
+ >
+ );
+ case scoreAsPercentage < 50:
+ return (
+ <>
+
+ {scoreAsPercentage}%
+ >
+ );
+ default:
+ return N/A;
+ }
+};
+
+const LighthouseAuditStatus: FC<{ audit: Audit }> = ({ audit }) => (
+ <>
+
+ {audit.status.toUpperCase()}
+ >
+);
+
+const LighthouseAuditSummary: FC<{ audit: Audit; dense?: boolean }> = ({
+ audit,
+ dense = false,
+}) => {
+ const { url } = audit;
+ const flattenedCategoryData: Record = {};
+ if (audit.status === 'COMPLETED') {
+ const categories = (audit as AuditCompleted).categories;
+ const categoryIds = Object.keys(categories) as LighthouseCategoryId[];
+ categoryIds.forEach((id: LighthouseCategoryId) => {
+ const { title, score } = categories[id];
+
+ flattenedCategoryData[title] = (
+
+ );
+ });
+ }
+ const tableData = {
+ url,
+ status: ,
+ ...flattenedCategoryData,
+ };
+
+ return ;
+};
+
+export const LastLighthouseAuditCard: FC<{ dense?: boolean }> = ({
+ dense = false,
+}) => {
+ const { value: website, loading, error } = useWebsiteForEntity();
+
+ let content;
+ if (loading) {
+ content = ;
+ }
+ if (error) {
+ content = null;
+ }
+ if (website) {
+ content = (
+
+ );
+ }
+ return {content};
+};
diff --git a/plugins/lighthouse/src/components/Cards/index.ts b/plugins/lighthouse/src/components/Cards/index.ts
new file mode 100644
index 0000000000..7595619808
--- /dev/null
+++ b/plugins/lighthouse/src/components/Cards/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { LastLighthouseAuditCard } from './LastLighthouseAuditCard';
diff --git a/plugins/lighthouse/src/components/CreateAudit/index.tsx b/plugins/lighthouse/src/components/CreateAudit/index.tsx
index 36bd97485a..ad46b06020 100644
--- a/plugins/lighthouse/src/components/CreateAudit/index.tsx
+++ b/plugins/lighthouse/src/components/CreateAudit/index.tsx
@@ -53,7 +53,7 @@ const useStyles = makeStyles(theme => ({
},
}));
-const CreateAudit: FC<{}> = () => {
+export const CreateAuditContent: FC<{}> = () => {
const errorApi = useApi(errorApiRef);
const lighthouseApi = useApi(lighthouseApiRef);
const classes = useStyles();
@@ -94,88 +94,91 @@ const CreateAudit: FC<{}> = () => {
]);
return (
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+ >
);
};
+const CreateAudit = () => (
+
+
+
+
+
+
+);
+
export default CreateAudit;
diff --git a/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx
new file mode 100644
index 0000000000..868e26f04c
--- /dev/null
+++ b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { renderHook } from '@testing-library/react-hooks';
+import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core-api';
+import { lighthouseApiRef, WebsiteListResponse } from '../api';
+import { useWebsiteForEntity } from './useWebsiteForEntity';
+import { EntityContext } from '@backstage/plugin-catalog';
+import { Entity } from '@backstage/catalog-model';
+import * as data from '../__fixtures__/website-list-response.json';
+
+const websiteListResponse = data as WebsiteListResponse;
+const website = websiteListResponse.items[0];
+
+const mockErrorApi: jest.Mocked = {
+ post: jest.fn(),
+ error$: jest.fn(),
+};
+
+const mockLighthouseApi: jest.Mocked> = {
+ getWebsiteByUrl: jest.fn(),
+};
+
+describe('useWebsiteForEntity', () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'software',
+ annotations: {
+ 'lighthouse.com/website-url': website.url,
+ },
+ },
+ spec: {
+ owner: 'guest',
+ type: 'Website',
+ lifecycle: 'development',
+ },
+ };
+
+ const wrapper: React.FC<{}> = ({ children }) => {
+ return (
+
+
+ {children}
+
+
+ );
+ };
+
+ const subject = () =>
+ renderHook(useWebsiteForEntity, {
+ wrapper,
+ });
+
+ beforeEach(() => {
+ (mockLighthouseApi.getWebsiteByUrl as jest.Mock).mockResolvedValue(website);
+ });
+
+ it('returns the lighthouse information for the website url in annotations ', async () => {
+ const { result, waitForNextUpdate } = subject();
+ await waitForNextUpdate();
+ expect(result.current?.value).toBe(website);
+ });
+
+ describe('where there is an error', () => {
+ const error = new Error('useWebsiteForEntity unit test');
+
+ beforeEach(() => {
+ (mockLighthouseApi.getWebsiteByUrl as jest.Mock).mockRejectedValueOnce(
+ error,
+ );
+ });
+
+ it('posts the error to the error api and returns the error to the caller', async () => {
+ const { result, waitForNextUpdate } = subject();
+ await waitForNextUpdate();
+ expect(result.current?.error).toBe(error);
+ expect(mockErrorApi.post).toHaveBeenCalledWith(error);
+ });
+ });
+});
diff --git a/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts b/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts
new file mode 100644
index 0000000000..c52e38f473
--- /dev/null
+++ b/plugins/lighthouse/src/hooks/useWebsiteForEntity.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { useEntity } from '@backstage/plugin-catalog';
+import { LIGHTHOUSE_WEBSITE_URL_ANNOTATION } from '../../constants';
+import { errorApiRef, useApi } from '@backstage/core-api';
+import { lighthouseApiRef } from '../api';
+import { useAsync } from 'react-use';
+
+// For the sake of simplicity we assume that an entity has only one website url. This is to avoid encoding a list
+// type in an annotation which is a plain string.
+export const useWebsiteForEntity = () => {
+ const { entity } = useEntity();
+ const websiteUrl =
+ entity.metadata.annotations?.[LIGHTHOUSE_WEBSITE_URL_ANNOTATION] ?? '';
+ const lighthouseApi = useApi(lighthouseApiRef);
+ const errorApi = useApi(errorApiRef);
+ const response = useAsync(() => lighthouseApi.getWebsiteByUrl(websiteUrl), [
+ websiteUrl,
+ ]);
+ if (response.error) {
+ errorApi.post(response.error);
+ }
+ return response;
+};
diff --git a/plugins/lighthouse/src/index.ts b/plugins/lighthouse/src/index.ts
index bebdaaf713..64fe2f8cc0 100644
--- a/plugins/lighthouse/src/index.ts
+++ b/plugins/lighthouse/src/index.ts
@@ -15,5 +15,6 @@
*/
export { plugin } from './plugin';
-export { Router } from './Router';
+export { Router, isPluginApplicableToEntity, EmbeddedRouter } from './Router';
export * from './api';
+export * from './components/Cards';
diff --git a/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx b/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx
index b01ace4994..8084378ca1 100644
--- a/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx
+++ b/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx
@@ -66,7 +66,11 @@ export const TechDocsPageHeader = ({
+
}
diff --git a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx
index 3ceb0af58d..1afdb53ad4 100644
--- a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx
+++ b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx
@@ -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
-
+
Milestones
.
@@ -113,7 +116,10 @@ const WelcomePage = () => {
We suggest you either check out the documentation for{' '}
-
+
creating a plugin
{' '}
or have a look in the code for the{' '}
@@ -121,7 +127,10 @@ const WelcomePage = () => {
existing plugins
{' '}
in the directory{' '}
-
+
plugins/
.
@@ -135,7 +144,10 @@ const WelcomePage = () => {
backstage.io
-
+
Create a plugin
diff --git a/yarn.lock b/yarn.lock
index b9f5733acd..8b9b16ebe7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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==