From 44a81f7ceac58344df00095d0b3e388b6f4b8229 Mon Sep 17 00:00:00 2001 From: Remi Date: Mon, 30 Nov 2020 23:37:13 +0100 Subject: [PATCH 01/86] feat(storybook): add drawer --- .../src/components/Drawer/Drawer.stories.tsx | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 packages/core/src/components/Drawer/Drawer.stories.tsx diff --git a/packages/core/src/components/Drawer/Drawer.stories.tsx b/packages/core/src/components/Drawer/Drawer.stories.tsx new file mode 100644 index 0000000000..9bb60c78a4 --- /dev/null +++ b/packages/core/src/components/Drawer/Drawer.stories.tsx @@ -0,0 +1,165 @@ +/* + * 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, { useState } from 'react'; +import { + Drawer, + Button, + Typography, + makeStyles, + IconButton, +} from '@material-ui/core'; +import Close from '@material-ui/icons/Close'; + +export default { + title: 'Layout/Drawer', + component: Drawer, +}; + +const useDrawerStyles = makeStyles({ + paper: { + width: '50%', + justifyContent: 'space-between', + padding: '20px', + }, +}); + +const useDrawerContentStyles = makeStyles({ + header: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + }, + icon: { + fontSize: 20, + }, + content: { + height: '80%', + backgroundColor: '#EEEEEE', + }, + secondaryAction: { + marginLeft: '20px', + }, +}); + +/* Example content wrapped inside the Drawer component */ +const DrawerContent = ({ + toggleDrawer, +}: { + toggleDrawer: (isOpen: boolean) => void; +}) => { + const classes = useDrawerContentStyles(); + + return ( + <> +
+ Side Panel Title + toggleDrawer(false)} + color="inherit" + > + + +
+
+
+ + +
+ + ); +}; + +/* Default drawer can toggle open or closed. + * It can be cancelled by clicking the overlay + * or pressing the esc key. + */ +export const DefaultDrawer = () => { + const [isOpen, toggleDrawer] = useState(false); + const classes = useDrawerStyles(); + + return ( + <> + + toggleDrawer(false)} + > + + + + ); +}; + +/* Persistent drawer works like the default one - + * except that the content sits on the same level + * as the main content and you can't cancel it by + * clicking the overlay or pressing the esc key. + * + * Set the Drawer variant props: 'persistent' + */ +export const PersistentDrawer = () => { + const [isOpen, toggleDrawer] = useState(false); + const classes = useDrawerStyles(); + + return ( + <> + + toggleDrawer(false)} + > + + + + ); +}; From b2a07d2dcb0117555247863c3998572e00d8c61e Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 3 Dec 2020 15:31:23 +0100 Subject: [PATCH 02/86] Avoid loading data from Jenkins twice. Don't load data when navigating thought the pages as all data from all pages is already loaded. --- .changeset/brave-zoos-fail.md | 5 +++ plugins/jenkins/src/components/useBuilds.ts | 45 +++++++++------------ 2 files changed, 23 insertions(+), 27 deletions(-) create mode 100644 .changeset/brave-zoos-fail.md diff --git a/.changeset/brave-zoos-fail.md b/.changeset/brave-zoos-fail.md new file mode 100644 index 0000000000..dfb18fe71c --- /dev/null +++ b/.changeset/brave-zoos-fail.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-jenkins': patch +--- + +Avoid loading data from Jenkins twice. Don't load data when navigating thought the pages as all data from all pages is already loaded. diff --git a/plugins/jenkins/src/components/useBuilds.ts b/plugins/jenkins/src/components/useBuilds.ts index 62e3719dbd..db7c7ddd55 100644 --- a/plugins/jenkins/src/components/useBuilds.ts +++ b/plugins/jenkins/src/components/useBuilds.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { errorApiRef, useApi } from '@backstage/core'; -import { useCallback, useEffect, useState } from 'react'; +import { useState } from 'react'; import { useAsyncRetry } from 'react-use'; import { jenkinsApiRef } from '../api'; @@ -26,21 +26,6 @@ export function useBuilds(owner: string, repo: string, branch?: string) { const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(5); - const getBuilds = useCallback(async () => { - try { - let build; - if (branch) { - build = await api.getLastBuild(`${owner}/${repo}/${branch}`); - } else { - build = await api.getFolder(`${owner}/${repo}`); - } - return build; - } catch (e) { - errorApi.post(e); - return Promise.reject(e); - } - }, [api, branch, errorApi, owner, repo]); - const restartBuild = async (buildName: string) => { try { await api.retry(buildName); @@ -49,18 +34,24 @@ export function useBuilds(owner: string, repo: string, branch?: string) { } }; - useEffect(() => { - getBuilds().then(b => { - const size = Array.isArray(b) ? b?.[0].build_num! : 1; - setTotal(size); - }); - }, [repo, getBuilds]); + const { loading, value: builds, retry } = useAsyncRetry(async () => { + try { + let builds; + if (branch) { + builds = await api.getLastBuild(`${owner}/${repo}/${branch}`); + } else { + builds = await api.getFolder(`${owner}/${repo}`); + } - const { loading, value: builds, retry } = useAsyncRetry( - () => - getBuilds().then(retrievedBuilds => retrievedBuilds ?? [], restartBuild), - [page, pageSize, getBuilds], - ); + const size = Array.isArray(builds) ? builds?.[0].build_num! : 1; + setTotal(size); + + return builds || []; + } catch (e) { + errorApi.post(e); + throw e; + } + }, [api, errorApi, owner, repo, branch]); const projectName = `${owner}/${repo}`; return [ From 9e2e0d433f92cd1df2561aef4d6caeb5f7947112 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 27 Nov 2020 16:38:59 +0100 Subject: [PATCH 03/86] Add more test data The additional testdata is already using providesApis and consumesApis --- packages/catalog-model/examples/all-apis.yaml | 2 ++ packages/catalog-model/examples/all-components.yaml | 2 ++ .../examples/apis/wayback-archive-api.yaml | 11 +++++++++++ .../examples/apis/wayback-search-api.yaml | 11 +++++++++++ .../components/wayback-archive-component.yaml | 11 +++++++++++ .../components/wayback-search-component.yaml | 13 +++++++++++++ 6 files changed, 50 insertions(+) create mode 100644 packages/catalog-model/examples/apis/wayback-archive-api.yaml create mode 100644 packages/catalog-model/examples/apis/wayback-search-api.yaml create mode 100644 packages/catalog-model/examples/components/wayback-archive-component.yaml create mode 100644 packages/catalog-model/examples/components/wayback-search-component.yaml diff --git a/packages/catalog-model/examples/all-apis.yaml b/packages/catalog-model/examples/all-apis.yaml index 0278fc3078..db7e5e31e1 100644 --- a/packages/catalog-model/examples/all-apis.yaml +++ b/packages/catalog-model/examples/all-apis.yaml @@ -11,3 +11,5 @@ spec: - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/apis/spotify-api.yaml - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/apis/streetlights-api.yaml - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/apis/swapi-graphql.yaml + - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/apis/wayback-archive-api.yaml + - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/apis/wayback-search-api.yaml diff --git a/packages/catalog-model/examples/all-components.yaml b/packages/catalog-model/examples/all-components.yaml index 06c44b59d7..6160af3db3 100644 --- a/packages/catalog-model/examples/all-components.yaml +++ b/packages/catalog-model/examples/all-components.yaml @@ -15,3 +15,5 @@ spec: - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/playback-lib-component.yaml - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/www-artist-component.yaml - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/shuffle-api-component.yaml + - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/wayback-archive-component.yaml + - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/wayback-search-component.yaml diff --git a/packages/catalog-model/examples/apis/wayback-archive-api.yaml b/packages/catalog-model/examples/apis/wayback-archive-api.yaml new file mode 100644 index 0000000000..82610eae24 --- /dev/null +++ b/packages/catalog-model/examples/apis/wayback-archive-api.yaml @@ -0,0 +1,11 @@ +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: wayback-archive + description: Archive API for the wayback machine +spec: + type: openapi + lifecycle: production + owner: archive@example.com + definition: + $text: https://github.com/APIs-guru/openapi-directory/blob/master/APIs/archive.org/wayback/1.0.0/openapi.yaml diff --git a/packages/catalog-model/examples/apis/wayback-search-api.yaml b/packages/catalog-model/examples/apis/wayback-search-api.yaml new file mode 100644 index 0000000000..6eb5cae54b --- /dev/null +++ b/packages/catalog-model/examples/apis/wayback-search-api.yaml @@ -0,0 +1,11 @@ +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: wayback-search + description: Search API for the wayback machine +spec: + type: openapi + lifecycle: production + owner: archive@example.com + definition: + $text: https://github.com/APIs-guru/openapi-directory/blob/master/APIs/archive.org/search/1.0.0/openapi.yaml diff --git a/packages/catalog-model/examples/components/wayback-archive-component.yaml b/packages/catalog-model/examples/components/wayback-archive-component.yaml new file mode 100644 index 0000000000..ea5ef10a17 --- /dev/null +++ b/packages/catalog-model/examples/components/wayback-archive-component.yaml @@ -0,0 +1,11 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: wayback-archive + description: Archive of the wayback machine +spec: + type: service + lifecycle: production + owner: archive@example.com + providesApis: + - wayback-archive diff --git a/packages/catalog-model/examples/components/wayback-search-component.yaml b/packages/catalog-model/examples/components/wayback-search-component.yaml new file mode 100644 index 0000000000..def136ff2f --- /dev/null +++ b/packages/catalog-model/examples/components/wayback-search-component.yaml @@ -0,0 +1,13 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: wayback-search + description: Search of the wayback machine +spec: + type: service + lifecycle: production + owner: archive@example.com + providesApis: + - wayback-search + consumesApis: + - wayback-archive From 246799c7f233b539d9974063abed402da2ffccec Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 26 Nov 2020 16:33:41 +0100 Subject: [PATCH 04/86] Show consumers and providers for APIs --- .changeset/seven-tips-begin.md | 6 + .../app/src/components/catalog/EntityPage.tsx | 26 +++- .../app/src/components/catalog/EntityPage.tsx | 46 ++++--- .../catalog/EntityPageApi/EntityPageApi.tsx | 51 ------- plugins/api-docs/src/catalog/Router.tsx | 40 ------ .../src/components/ApisCards/ApisTable.tsx | 84 ++++++++++++ .../ApisCards/ConsumedApisCard.test.tsx | 127 ++++++++++++++++++ .../components/ApisCards/ConsumedApisCard.tsx | 85 ++++++++++++ .../ApisCards/ProvidedApisCard.test.tsx | 127 ++++++++++++++++++ .../components/ApisCards/ProvidedApisCard.tsx | 85 ++++++++++++ .../ApisCards}/index.ts | 3 +- .../ComponentsCards/ComponentsTable.tsx | 87 ++++++++++++ .../ConsumingComponentsCard.test.tsx | 125 +++++++++++++++++ .../ConsumingComponentsCard.tsx | 88 ++++++++++++ .../ProvidingComponentsCard.test.tsx | 125 +++++++++++++++++ .../ProvidingComponentsCard.tsx | 88 ++++++++++++ .../ComponentsCards}/index.ts | 3 +- .../MissingConsumesApisEmptyState.test.tsx} | 23 ++-- .../MissingConsumesApisEmptyState.tsx | 81 +++++++++++ .../MissingProvidesApisEmptyState.test.tsx | 28 ++++ .../MissingProvidesApisEmptyState.tsx} | 4 +- .../EmptyState}/index.ts | 3 +- plugins/api-docs/src/components/index.ts | 16 +-- .../src/components/useComponentApiEntities.ts | 83 ------------ .../src/components/useRelatedEntities.ts | 51 +++++++ plugins/api-docs/src/index.ts | 1 - plugins/api-docs/src/routes.ts | 6 - 27 files changed, 1263 insertions(+), 229 deletions(-) create mode 100644 .changeset/seven-tips-begin.md delete mode 100644 plugins/api-docs/src/catalog/EntityPageApi/EntityPageApi.tsx delete mode 100644 plugins/api-docs/src/catalog/Router.tsx create mode 100644 plugins/api-docs/src/components/ApisCards/ApisTable.tsx create mode 100644 plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx create mode 100644 plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx create mode 100644 plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx create mode 100644 plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx rename plugins/api-docs/src/{catalog/EntityPageApi => components/ApisCards}/index.ts (84%) create mode 100644 plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx create mode 100644 plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx create mode 100644 plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx create mode 100644 plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx create mode 100644 plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx rename plugins/api-docs/src/{catalog => components/ComponentsCards}/index.ts (81%) rename plugins/api-docs/src/components/{useComponentApiNames.ts => EmptyState/MissingConsumesApisEmptyState.test.tsx} (57%) create mode 100644 plugins/api-docs/src/components/EmptyState/MissingConsumesApisEmptyState.tsx create mode 100644 plugins/api-docs/src/components/EmptyState/MissingProvidesApisEmptyState.test.tsx rename plugins/api-docs/src/{catalog/MissingImplementsApisEmptyState/MissingImplementsApisEmptyState.tsx => components/EmptyState/MissingProvidesApisEmptyState.tsx} (95%) rename plugins/api-docs/src/{catalog/MissingImplementsApisEmptyState => components/EmptyState}/index.ts (78%) delete mode 100644 plugins/api-docs/src/components/useComponentApiEntities.ts create mode 100644 plugins/api-docs/src/components/useRelatedEntities.ts diff --git a/.changeset/seven-tips-begin.md b/.changeset/seven-tips-begin.md new file mode 100644 index 0000000000..b9ebc4f159 --- /dev/null +++ b/.changeset/seven-tips-begin.md @@ -0,0 +1,6 @@ +--- +'@backstage/create-app': patch +'@backstage/plugin-api-docs': patch +--- + +Add tables with consumes and provides relationships to the API and component entity pages. diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 068019c622..f46e65d010 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -17,7 +17,10 @@ import { ApiEntity, Entity } from '@backstage/catalog-model'; import { EmptyState } from '@backstage/core'; import { ApiDefinitionCard, - Router as ApiDocsRouter, + ConsumedApisCard, + ConsumingComponentsCard, + ProvidedApisCard, + ProvidingComponentsCard, } from '@backstage/plugin-api-docs'; import { AboutCard, @@ -167,6 +170,17 @@ const ComponentOverviewContent = ({ entity }: { entity: Entity }) => ( ); +const ComponentApisContent = ({ entity }: { entity: Entity }) => ( + + + + + + + + +); + const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( ( } + element={} /> ( + + + + + + + + ); diff --git a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx index 7d65b3261a..608a4f5f76 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx @@ -13,26 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - Router as GitHubActionsRouter, - isPluginApplicableToEntity as isGitHubActionsAvailable, -} from '@backstage/plugin-github-actions'; -import { - Router as CircleCIRouter, - isPluginApplicableToEntity as isCircleCIAvailable, -} from '@backstage/plugin-circleci'; -import { Router as ApiDocsRouter } from '@backstage/plugin-api-docs'; -import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs'; - -import React from 'react'; -import { - EntityPageLayout, - useEntity, - AboutCard, -} from '@backstage/plugin-catalog'; import { Entity } from '@backstage/catalog-model'; -import { Grid } from '@material-ui/core'; import { WarningPanel } from '@backstage/core'; +import { ConsumedApisCard, ProvidedApisCard } from '@backstage/plugin-api-docs'; +import { + AboutCard, EntityPageLayout, + useEntity +} from '@backstage/plugin-catalog'; +import { + isPluginApplicableToEntity as isCircleCIAvailable, Router as CircleCIRouter +} from '@backstage/plugin-circleci'; +import { + isPluginApplicableToEntity as isGitHubActionsAvailable, Router as GitHubActionsRouter +} from '@backstage/plugin-github-actions'; +import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs'; +import { Grid } from '@material-ui/core'; +import React from 'react'; + const CICDSwitcher = ({ entity }: { entity: Entity }) => { // This component is just an example of how you can implement your company's logic in entity page. @@ -60,6 +57,17 @@ const OverviewContent = ({ entity }: { entity: Entity }) => ( ); +const ComponentApisContent = ({ entity }: { entity: Entity }) => ( + + + + + + + + +); + const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( ( } + element={} /> { - const apiNames = useComponentApiNames(entity as ComponentEntity); - - const { apiEntities, loading } = useComponentApiEntities({ - entity: entity as ComponentEntity, - }); - - if (loading) { - return ; - } - - return ( - - {apiNames.map(api => ( - - - - ))} - - ); -}; diff --git a/plugins/api-docs/src/catalog/Router.tsx b/plugins/api-docs/src/catalog/Router.tsx deleted file mode 100644 index 64c074fc46..0000000000 --- a/plugins/api-docs/src/catalog/Router.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 { Entity, RELATION_PROVIDES_API } from '@backstage/catalog-model'; -import { Route, Routes } from 'react-router'; -import { catalogRoute } from '../routes'; -import { EntityPageApi } from './EntityPageApi'; -import { MissingImplementsApisEmptyState } from './MissingImplementsApisEmptyState'; - -const isPluginApplicableToEntity = (entity: Entity) => { - // TODO: Also support RELATION_CONSUMES_API - return entity.relations?.some(r => r.type === RELATION_PROVIDES_API); -}; - -export const Router = ({ entity }: { entity: Entity }) => - !isPluginApplicableToEntity(entity) ? ( - - ) : ( - - } - /> - ) - - ); diff --git a/plugins/api-docs/src/components/ApisCards/ApisTable.tsx b/plugins/api-docs/src/components/ApisCards/ApisTable.tsx new file mode 100644 index 0000000000..7db62433eb --- /dev/null +++ b/plugins/api-docs/src/components/ApisCards/ApisTable.tsx @@ -0,0 +1,84 @@ +/* + * 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 { ApiEntity } from '@backstage/catalog-model'; +import { Table, TableColumn } from '@backstage/core'; +import React from 'react'; +import { ApiTypeTitle } from '../ApiDefinitionCard'; +import { EntityLink } from '../EntityLink'; + +const columns: TableColumn[] = [ + { + title: 'Name', + field: 'metadata.name', + highlight: true, + render: (entity: any) => ( + {entity.metadata.name} + ), + }, + { + title: 'Owner', + field: 'spec.owner', + }, + { + title: 'Lifecycle', + field: 'spec.lifecycle', + }, + { + title: 'Type', + field: 'spec.type', + render: (entity: ApiEntity) => , + }, + { + title: 'Description', + field: 'metadata.description', + width: 'auto', + }, +]; + +type Props = { + title: string; + variant?: string; + entities: (ApiEntity | undefined)[]; +}; + +export const ApisTable = ({ entities, title, variant = 'gridItem' }: Props) => { + const tableStyle: React.CSSProperties = { + minWidth: '0', + width: '100%', + }; + + if (variant === 'gridItem') { + tableStyle.height = 'calc(100% - 10px)'; + } + + return ( + + columns={columns} + title={title} + style={tableStyle} + options={{ + // TODO: Toolbar padding if off compared to other cards, should be: padding: 16px 24px; + search: false, + paging: false, + actionsColumnIndex: -1, + padding: 'dense', + }} + // TODO: For now we skip all APIs that we can't find without a warning! + data={entities.filter(e => e !== undefined) as ApiEntity[]} + /> + ); +}; diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx new file mode 100644 index 0000000000..a4a1e15511 --- /dev/null +++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx @@ -0,0 +1,127 @@ +/* + * 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 { Entity, RELATION_CONSUMES_API } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; +import { renderInTestApp } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { ApiDocsConfig, apiDocsConfigRef } from '../../config'; +import { ConsumedApisCard } from './ConsumedApisCard'; + +describe('', () => { + const apiDocsConfig: jest.Mocked = { + getApiDefinitionWidget: jest.fn(), + } as any; + const catalogApi: jest.Mocked = { + getLocationById: jest.fn(), + getEntityByName: jest.fn(), + getEntities: jest.fn(), + addLocation: jest.fn(), + getLocationByEntity: jest.fn(), + removeEntityByUid: jest.fn(), + } as any; + let Wrapper: React.ComponentType; + + beforeEach(() => { + const apis = ApiRegistry.with(catalogApiRef, catalogApi).with( + apiDocsConfigRef, + apiDocsConfig, + ); + + Wrapper = ({ children }: { children?: React.ReactNode }) => ( + {children} + ); + }); + + afterEach(() => jest.resetAllMocks()); + + it('shows empty list if no relations', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'my-name', + namespace: 'my-namespace', + }, + relations: [], + }; + + const { getByText } = await renderInTestApp( + + + , + ); + + expect(getByText(/Consumed APIs/i)).toBeInTheDocument(); + expect(getByText(/No APIs consumed by this entity/i)).toBeInTheDocument(); + }); + + it('shows consumed APIs', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'my-name', + namespace: 'my-namespace', + }, + relations: [ + { + target: { + kind: 'API', + namespace: 'my-namespace', + name: 'target-name', + }, + type: RELATION_CONSUMES_API, + }, + ], + }; + catalogApi.getEntityByName.mockResolvedValue({ + apiVersion: 'v1', + kind: 'API', + metadata: { + name: 'target-name', + namespace: 'my-namespace', + }, + spec: { + type: 'openapi', + owner: 'Test', + lifecycle: 'production', + definition: '...', + }, + }); + apiDocsConfig.getApiDefinitionWidget.mockReturnValue({ + type: 'openapi', + title: 'OpenAPI', + component: () =>
, + }); + + const { getByText } = await renderInTestApp( + + + , + ); + + await waitFor(() => { + expect(getByText(/Consumed APIs/i)).toBeInTheDocument(); + expect(getByText(/target-name/i)).toBeInTheDocument(); + expect(getByText(/OpenAPI/)).toBeInTheDocument(); + expect(getByText(/Test/i)).toBeInTheDocument(); + expect(getByText(/production/i)).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx new file mode 100644 index 0000000000..0bd4919554 --- /dev/null +++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx @@ -0,0 +1,85 @@ +/* + * 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 { + ApiEntity, + Entity, + RELATION_CONSUMES_API, +} from '@backstage/catalog-model'; +import { EmptyState, InfoCard, Progress } from '@backstage/core'; +import React, { PropsWithChildren } from 'react'; +import { ApisTable } from './ApisTable'; +import { MissingConsumesApisEmptyState } from '../EmptyState'; +import { useRelatedEntities } from '../useRelatedEntities'; + +const ApisCard = ({ + children, + variant = 'gridItem', +}: PropsWithChildren<{ variant?: string }>) => { + return ( + + {children} + + ); +}; + +type Props = { + entity: Entity; + variant?: string; +}; + +export const ConsumedApisCard = ({ entity, variant = 'gridItem' }: Props) => { + const { entities, loading, error } = useRelatedEntities( + entity, + RELATION_CONSUMES_API, + ); + + if (loading) { + return ( + + + + ); + } + + if (error) { + return ( + + + + ); + } + + if (!entities || entities.length === 0) { + return ( + + + + ); + } + + return ( + + ); +}; diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx new file mode 100644 index 0000000000..1f42ff9060 --- /dev/null +++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx @@ -0,0 +1,127 @@ +/* + * 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 { Entity, RELATION_PROVIDES_API } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; +import { renderInTestApp } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { ApiDocsConfig, apiDocsConfigRef } from '../../config'; +import { ProvidedApisCard } from './ProvidedApisCard'; + +describe('', () => { + const apiDocsConfig: jest.Mocked = { + getApiDefinitionWidget: jest.fn(), + } as any; + const catalogApi: jest.Mocked = { + getLocationById: jest.fn(), + getEntityByName: jest.fn(), + getEntities: jest.fn(), + addLocation: jest.fn(), + getLocationByEntity: jest.fn(), + removeEntityByUid: jest.fn(), + } as any; + let Wrapper: React.ComponentType; + + beforeEach(() => { + const apis = ApiRegistry.with(catalogApiRef, catalogApi).with( + apiDocsConfigRef, + apiDocsConfig, + ); + + Wrapper = ({ children }: { children?: React.ReactNode }) => ( + {children} + ); + }); + + afterEach(() => jest.resetAllMocks()); + + it('shows empty list if no relations', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'my-name', + namespace: 'my-namespace', + }, + relations: [], + }; + + const { getByText } = await renderInTestApp( + + + , + ); + + expect(getByText(/Provided APIs/i)).toBeInTheDocument(); + expect(getByText(/No APIs provided by this entity/i)).toBeInTheDocument(); + }); + + it('shows consumed APIs', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'my-name', + namespace: 'my-namespace', + }, + relations: [ + { + target: { + kind: 'API', + namespace: 'my-namespace', + name: 'target-name', + }, + type: RELATION_PROVIDES_API, + }, + ], + }; + catalogApi.getEntityByName.mockResolvedValue({ + apiVersion: 'v1', + kind: 'API', + metadata: { + name: 'target-name', + namespace: 'my-namespace', + }, + spec: { + type: 'openapi', + owner: 'Test', + lifecycle: 'production', + definition: '...', + }, + }); + apiDocsConfig.getApiDefinitionWidget.mockReturnValue({ + type: 'openapi', + title: 'OpenAPI', + component: () =>
, + }); + + const { getByText } = await renderInTestApp( + + + , + ); + + await waitFor(() => { + expect(getByText(/Provided APIs/i)).toBeInTheDocument(); + expect(getByText(/target-name/i)).toBeInTheDocument(); + expect(getByText(/OpenAPI/)).toBeInTheDocument(); + expect(getByText(/Test/i)).toBeInTheDocument(); + expect(getByText(/production/i)).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx new file mode 100644 index 0000000000..618f2dc1f6 --- /dev/null +++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx @@ -0,0 +1,85 @@ +/* + * 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 { + ApiEntity, + Entity, + RELATION_PROVIDES_API, +} from '@backstage/catalog-model'; +import { EmptyState, InfoCard, Progress } from '@backstage/core'; +import React, { PropsWithChildren } from 'react'; +import { ApisTable } from './ApisTable'; +import { MissingProvidesApisEmptyState } from '../EmptyState'; +import { useRelatedEntities } from '../useRelatedEntities'; + +const ApisCard = ({ + children, + variant = 'gridItem', +}: PropsWithChildren<{ variant?: string }>) => { + return ( + + {children} + + ); +}; + +type Props = { + entity: Entity; + variant?: string; +}; + +export const ProvidedApisCard = ({ entity, variant = 'gridItem' }: Props) => { + const { entities, loading, error } = useRelatedEntities( + entity, + RELATION_PROVIDES_API, + ); + + if (loading) { + return ( + + + + ); + } + + if (error) { + return ( + + + + ); + } + + if (!entities || entities.length === 0) { + return ( + + + + ); + } + + return ( + + ); +}; diff --git a/plugins/api-docs/src/catalog/EntityPageApi/index.ts b/plugins/api-docs/src/components/ApisCards/index.ts similarity index 84% rename from plugins/api-docs/src/catalog/EntityPageApi/index.ts rename to plugins/api-docs/src/components/ApisCards/index.ts index 1d382e01de..2a01a1dc6e 100644 --- a/plugins/api-docs/src/catalog/EntityPageApi/index.ts +++ b/plugins/api-docs/src/components/ApisCards/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ -export { EntityPageApi } from './EntityPageApi'; +export { ConsumedApisCard } from './ConsumedApisCard'; +export { ProvidedApisCard } from './ProvidedApisCard'; diff --git a/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx b/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx new file mode 100644 index 0000000000..1b62a56d10 --- /dev/null +++ b/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx @@ -0,0 +1,87 @@ +/* + * 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 { ComponentEntity } from '@backstage/catalog-model'; +import { Table, TableColumn } from '@backstage/core'; +import React from 'react'; +import { EntityLink } from '../EntityLink'; + +const columns: TableColumn[] = [ + { + title: 'Name', + field: 'metadata.name', + highlight: true, + render: (entity: any) => ( + {entity.metadata.name} + ), + }, + { + title: 'Owner', + field: 'spec.owner', + }, + { + title: 'Lifecycle', + field: 'spec.lifecycle', + }, + { + title: 'Type', + field: 'spec.type', + }, + { + title: 'Description', + field: 'metadata.description', + width: 'auto', + }, +]; + +type Props = { + title: string; + variant?: string; + entities: (ComponentEntity | undefined)[]; +}; + +// TODO: In theory this could also be systems! +export const ComponentsTable = ({ + entities, + title, + variant = 'gridItem', +}: Props) => { + const tableStyle: React.CSSProperties = { + minWidth: '0', + width: '100%', + }; + + if (variant === 'gridItem') { + tableStyle.height = 'calc(100% - 10px)'; + } + + return ( + + columns={columns} + title={title} + style={tableStyle} + options={{ + // TODO: Toolbar padding if off compared to other cards, should be: padding: 16px 24px; + search: false, + paging: false, + actionsColumnIndex: -1, + padding: 'dense', + }} + // TODO: For now we skip all APIs that we can't find without a warning! + data={entities.filter(e => e !== undefined) as ComponentEntity[]} + /> + ); +}; diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx new file mode 100644 index 0000000000..606ff7e77b --- /dev/null +++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx @@ -0,0 +1,125 @@ +/* + * 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 { Entity, RELATION_API_CONSUMED_BY } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; +import { renderInTestApp } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { ConsumingComponentsCard } from './ConsumingComponentsCard'; + +describe('', () => { + const catalogApi: jest.Mocked = { + getLocationById: jest.fn(), + getEntityByName: jest.fn(), + getEntities: jest.fn(), + addLocation: jest.fn(), + getLocationByEntity: jest.fn(), + removeEntityByUid: jest.fn(), + } as any; + let Wrapper: React.ComponentType; + + beforeEach(() => { + const apis = ApiRegistry.with(catalogApiRef, catalogApi); + + Wrapper = ({ children }: { children?: React.ReactNode }) => ( + {children} + ); + }); + + afterEach(() => jest.resetAllMocks()); + + it('shows empty list if no relations', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'API', + metadata: { + name: 'my-name', + namespace: 'my-namespace', + }, + spec: { + type: 'openapi', + owner: 'Test', + lifecycle: 'production', + definition: '...', + }, + relations: [], + }; + + const { getByText } = await renderInTestApp( + + + , + ); + + expect(getByText(/Consumers/i)).toBeInTheDocument(); + expect(getByText(/No APIs consumed by this entity/i)).toBeInTheDocument(); + }); + + it('shows consuming components', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'API', + metadata: { + name: 'my-name', + namespace: 'my-namespace', + }, + spec: { + type: 'openapi', + owner: 'Test', + lifecycle: 'production', + definition: '...', + }, + relations: [ + { + target: { + kind: 'Component', + namespace: 'my-namespace', + name: 'target-name', + }, + type: RELATION_API_CONSUMED_BY, + }, + ], + }; + catalogApi.getEntityByName.mockResolvedValue({ + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'target-name', + namespace: 'my-namespace', + }, + spec: { + type: 'service', + owner: 'Test', + lifecycle: 'production', + }, + }); + + const { getByText } = await renderInTestApp( + + + , + ); + + await waitFor(() => { + expect(getByText(/Consumers/i)).toBeInTheDocument(); + expect(getByText(/target-name/i)).toBeInTheDocument(); + expect(getByText(/Test/i)).toBeInTheDocument(); + expect(getByText(/production/i)).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx new file mode 100644 index 0000000000..0431367aa2 --- /dev/null +++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx @@ -0,0 +1,88 @@ +/* + * 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 { + ComponentEntity, + Entity, + RELATION_API_CONSUMED_BY, +} from '@backstage/catalog-model'; +import { EmptyState, InfoCard, Progress } from '@backstage/core'; +import React, { PropsWithChildren } from 'react'; +import { MissingConsumesApisEmptyState } from '../EmptyState'; +import { useRelatedEntities } from '../useRelatedEntities'; +import { ComponentsTable } from './ComponentsTable'; + +const ComponentsCard = ({ + children, + variant = 'gridItem', +}: PropsWithChildren<{ variant?: string }>) => { + return ( + + {children} + + ); +}; + +type Props = { + entity: Entity; + variant?: string; +}; + +export const ConsumingComponentsCard = ({ + entity, + variant = 'gridItem', +}: Props) => { + const { entities, loading, error } = useRelatedEntities( + entity, + RELATION_API_CONSUMED_BY, + ); + + if (loading) { + return ( + + + + ); + } + + if (error) { + return ( + + + + ); + } + + if (!entities || entities.length === 0) { + return ( + + + + ); + } + + return ( + + ); +}; diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx new file mode 100644 index 0000000000..d1cec1722a --- /dev/null +++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx @@ -0,0 +1,125 @@ +/* + * 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 { Entity, RELATION_API_PROVIDED_BY } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; +import { renderInTestApp } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { ProvidingComponentsCard } from './ProvidingComponentsCard'; + +describe('', () => { + const catalogApi: jest.Mocked = { + getLocationById: jest.fn(), + getEntityByName: jest.fn(), + getEntities: jest.fn(), + addLocation: jest.fn(), + getLocationByEntity: jest.fn(), + removeEntityByUid: jest.fn(), + } as any; + let Wrapper: React.ComponentType; + + beforeEach(() => { + const apis = ApiRegistry.with(catalogApiRef, catalogApi); + + Wrapper = ({ children }: { children?: React.ReactNode }) => ( + {children} + ); + }); + + afterEach(() => jest.resetAllMocks()); + + it('shows empty list if no relations', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'API', + metadata: { + name: 'my-name', + namespace: 'my-namespace', + }, + spec: { + type: 'openapi', + owner: 'Test', + lifecycle: 'production', + definition: '...', + }, + relations: [], + }; + + const { getByText } = await renderInTestApp( + + + , + ); + + expect(getByText(/Providers/i)).toBeInTheDocument(); + expect(getByText(/No APIs provided by this entity/i)).toBeInTheDocument(); + }); + + it('shows providing components', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'API', + metadata: { + name: 'my-name', + namespace: 'my-namespace', + }, + spec: { + type: 'openapi', + owner: 'Test', + lifecycle: 'production', + definition: '...', + }, + relations: [ + { + target: { + kind: 'Component', + namespace: 'my-namespace', + name: 'target-name', + }, + type: RELATION_API_PROVIDED_BY, + }, + ], + }; + catalogApi.getEntityByName.mockResolvedValue({ + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'target-name', + namespace: 'my-namespace', + }, + spec: { + type: 'service', + owner: 'Test', + lifecycle: 'production', + }, + }); + + const { getByText } = await renderInTestApp( + + + , + ); + + await waitFor(() => { + expect(getByText(/Providers/i)).toBeInTheDocument(); + expect(getByText(/target-name/i)).toBeInTheDocument(); + expect(getByText(/Test/i)).toBeInTheDocument(); + expect(getByText(/production/i)).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx new file mode 100644 index 0000000000..9e405a3af3 --- /dev/null +++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx @@ -0,0 +1,88 @@ +/* + * 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 { + ComponentEntity, + Entity, + RELATION_API_PROVIDED_BY, +} from '@backstage/catalog-model'; +import { EmptyState, InfoCard, Progress } from '@backstage/core'; +import React, { PropsWithChildren } from 'react'; +import { MissingProvidesApisEmptyState } from '../EmptyState'; +import { useRelatedEntities } from '../useRelatedEntities'; +import { ComponentsTable } from './ComponentsTable'; + +const ComponentsCard = ({ + children, + variant = 'gridItem', +}: PropsWithChildren<{ variant?: string }>) => { + return ( + + {children} + + ); +}; + +type Props = { + entity: Entity; + variant?: string; +}; + +export const ProvidingComponentsCard = ({ + entity, + variant = 'gridItem', +}: Props) => { + const { entities, loading, error } = useRelatedEntities( + entity, + RELATION_API_PROVIDED_BY, + ); + + if (loading) { + return ( + + + + ); + } + + if (error) { + return ( + + + + ); + } + + if (!entities || entities.length === 0) { + return ( + + + + ); + } + + return ( + + ); +}; diff --git a/plugins/api-docs/src/catalog/index.ts b/plugins/api-docs/src/components/ComponentsCards/index.ts similarity index 81% rename from plugins/api-docs/src/catalog/index.ts rename to plugins/api-docs/src/components/ComponentsCards/index.ts index 4c177df914..e1c0e87198 100644 --- a/plugins/api-docs/src/catalog/index.ts +++ b/plugins/api-docs/src/components/ComponentsCards/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ -export { Router } from './Router'; +export { ConsumingComponentsCard } from './ConsumingComponentsCard'; +export { ProvidingComponentsCard } from './ProvidingComponentsCard'; diff --git a/plugins/api-docs/src/components/useComponentApiNames.ts b/plugins/api-docs/src/components/EmptyState/MissingConsumesApisEmptyState.test.tsx similarity index 57% rename from plugins/api-docs/src/components/useComponentApiNames.ts rename to plugins/api-docs/src/components/EmptyState/MissingConsumesApisEmptyState.test.tsx index 1303967895..de753713c3 100644 --- a/plugins/api-docs/src/components/useComponentApiNames.ts +++ b/plugins/api-docs/src/components/EmptyState/MissingConsumesApisEmptyState.test.tsx @@ -14,16 +14,15 @@ * limitations under the License. */ -import { - ComponentEntity, - RELATION_PROVIDES_API, -} from '@backstage/catalog-model'; +import { renderInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { MissingConsumesApisEmptyState } from './MissingConsumesApisEmptyState'; -export const useComponentApiNames = (entity: ComponentEntity) => { - // TODO: This code doesn't handle namespaces and kinds correctly, but will be removed soon - return ( - entity.relations - ?.filter(r => r.type === RELATION_PROVIDES_API) - ?.map(r => r.target.name) || [] - ); -}; +describe('', () => { + it('renders without exploding', async () => { + const { getByText } = await renderInTestApp( + , + ); + expect(getByText(/consumesApis:/i)).toBeInTheDocument(); + }); +}); diff --git a/plugins/api-docs/src/components/EmptyState/MissingConsumesApisEmptyState.tsx b/plugins/api-docs/src/components/EmptyState/MissingConsumesApisEmptyState.tsx new file mode 100644 index 0000000000..3e71168dde --- /dev/null +++ b/plugins/api-docs/src/components/EmptyState/MissingConsumesApisEmptyState.tsx @@ -0,0 +1,81 @@ +/* + * 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 { Button, makeStyles, Typography } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; +import { CodeSnippet, EmptyState } from '@backstage/core'; + +const COMPONENT_YAML = `# Example +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: example +spec: + type: service + lifecycle: production + owner: guest + consumesApis: + - example-api +`; + +const useStyles = makeStyles(theme => ({ + code: { + borderRadius: 6, + margin: `${theme.spacing(2)}px 0px`, + background: theme.palette.type === 'dark' ? '#444' : '#fff', + }, +})); + +export const MissingConsumesApisEmptyState = () => { + const classes = useStyles(); + return ( + + Components can consume APIs that are displayed on this page. You need + to fill the consumesApis field to enable this tool. + + } + action={ + <> + + Link an API to your component as shown in the highlighted example + below: + +
+ +
+ + + } + /> + ); +}; diff --git a/plugins/api-docs/src/components/EmptyState/MissingProvidesApisEmptyState.test.tsx b/plugins/api-docs/src/components/EmptyState/MissingProvidesApisEmptyState.test.tsx new file mode 100644 index 0000000000..b539753a95 --- /dev/null +++ b/plugins/api-docs/src/components/EmptyState/MissingProvidesApisEmptyState.test.tsx @@ -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 { renderInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { MissingProvidesApisEmptyState } from './MissingProvidesApisEmptyState'; + +describe('', () => { + it('renders without exploding', async () => { + const { getByText } = await renderInTestApp( + , + ); + expect(getByText(/providesApis:/i)).toBeInTheDocument(); + }); +}); diff --git a/plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/MissingImplementsApisEmptyState.tsx b/plugins/api-docs/src/components/EmptyState/MissingProvidesApisEmptyState.tsx similarity index 95% rename from plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/MissingImplementsApisEmptyState.tsx rename to plugins/api-docs/src/components/EmptyState/MissingProvidesApisEmptyState.tsx index fbb8810088..9bf3465a34 100644 --- a/plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/MissingImplementsApisEmptyState.tsx +++ b/plugins/api-docs/src/components/EmptyState/MissingProvidesApisEmptyState.tsx @@ -40,12 +40,12 @@ const useStyles = makeStyles(theme => ({ }, })); -export const MissingImplementsApisEmptyState = () => { +export const MissingProvidesApisEmptyState = () => { const classes = useStyles(); return ( Components can implement APIs that are displayed on this page. You diff --git a/plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/index.ts b/plugins/api-docs/src/components/EmptyState/index.ts similarity index 78% rename from plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/index.ts rename to plugins/api-docs/src/components/EmptyState/index.ts index 1b7d35c0a2..d195c43eb1 100644 --- a/plugins/api-docs/src/catalog/MissingImplementsApisEmptyState/index.ts +++ b/plugins/api-docs/src/components/EmptyState/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ -export { MissingImplementsApisEmptyState } from './MissingImplementsApisEmptyState'; +export { MissingConsumesApisEmptyState } from './MissingConsumesApisEmptyState'; +export { MissingProvidesApisEmptyState } from './MissingProvidesApisEmptyState'; diff --git a/plugins/api-docs/src/components/index.ts b/plugins/api-docs/src/components/index.ts index cf9b189091..cfd985f47c 100644 --- a/plugins/api-docs/src/components/index.ts +++ b/plugins/api-docs/src/components/index.ts @@ -14,13 +14,9 @@ * limitations under the License. */ -export type { ApiDefinitionWidget } from './ApiDefinitionCard'; -export { - ApiDefinitionCard, - defaultDefinitionWidgets, -} from './ApiDefinitionCard'; -export { AsyncApiDefinitionWidget } from './AsyncApiDefinitionWidget'; -export { OpenApiDefinitionWidget } from './OpenApiDefinitionWidget'; -export { PlainApiDefinitionWidget } from './PlainApiDefinitionWidget'; -export { useComponentApiNames } from './useComponentApiNames'; -export { useComponentApiEntities } from './useComponentApiEntities'; +export * from './ApiDefinitionCard'; +export * from './ApisCards'; +export * from './AsyncApiDefinitionWidget'; +export * from './ComponentsCards'; +export * from './OpenApiDefinitionWidget'; +export * from './PlainApiDefinitionWidget'; diff --git a/plugins/api-docs/src/components/useComponentApiEntities.ts b/plugins/api-docs/src/components/useComponentApiEntities.ts deleted file mode 100644 index 9e5cbd968e..0000000000 --- a/plugins/api-docs/src/components/useComponentApiEntities.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 { useAsyncRetry } from 'react-use'; -import { errorApiRef, useApi } from '@backstage/core'; -import { - ApiEntity, - ComponentEntity, - parseEntityName, -} from '@backstage/catalog-model'; -import { catalogApiRef } from '@backstage/plugin-catalog'; -import { useComponentApiNames } from './useComponentApiNames'; - -export function useComponentApiEntities({ - entity, -}: { - entity: ComponentEntity; -}): { - loading: boolean; - apiEntities?: Map; - error?: Error; - retry: () => void; -} { - const catalogApi = useApi(catalogApiRef); - const errorApi = useApi(errorApiRef); - - const apiNames = useComponentApiNames(entity); - - const { loading, value: apiEntities, retry, error } = useAsyncRetry< - Map - >(async () => { - const resultMap = new Map(); - - await Promise.all( - apiNames.map(async name => { - try { - const apiEntityName = parseEntityName(name, { - defaultNamespace: entity.metadata.namespace, - defaultKind: 'API', - }); - - if (apiEntityName.kind !== 'API') { - throw new Error( - `Referenced entity of kind "${apiEntityName.kind}" as an API`, - ); - } - - const api = (await catalogApi.getEntityByName(apiEntityName)) as - | ApiEntity - | undefined; - - if (api) { - resultMap.set(api.metadata.name, api); - } - } catch (e) { - errorApi.post(e); - } - }), - ); - - return resultMap; - }, [catalogApi, entity]); - - return { - apiEntities, - loading, - error, - retry, - }; -} diff --git a/plugins/api-docs/src/components/useRelatedEntities.ts b/plugins/api-docs/src/components/useRelatedEntities.ts new file mode 100644 index 0000000000..847ec30578 --- /dev/null +++ b/plugins/api-docs/src/components/useRelatedEntities.ts @@ -0,0 +1,51 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { useApi } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog'; +import { useAsyncRetry } from 'react-use'; + +// TODO: Maybe this hook is interesting for others too? +export function useRelatedEntities( + entity: Entity, + type: string, +): { + entities: (Entity | undefined)[] | undefined; + loading: boolean; + error: Error | undefined; +} { + const catalogApi = useApi(catalogApiRef); + const { loading, value, error } = useAsyncRetry< + (Entity | undefined)[] + >(async () => { + const relations = + entity.relations && entity.relations.filter(r => r.type === type); + + if (!relations) { + return []; + } + + return await Promise.all( + relations?.map(r => catalogApi.getEntityByName(r.target)), + ); + }, [entity, type]); + + return { + entities: value, + loading, + error, + }; +} diff --git a/plugins/api-docs/src/index.ts b/plugins/api-docs/src/index.ts index dbb32cee7b..f09aeb1038 100644 --- a/plugins/api-docs/src/index.ts +++ b/plugins/api-docs/src/index.ts @@ -14,6 +14,5 @@ * limitations under the License. */ -export * from './catalog'; export * from './components'; export { plugin } from './plugin'; diff --git a/plugins/api-docs/src/routes.ts b/plugins/api-docs/src/routes.ts index 6adff78e47..64277b9ae8 100644 --- a/plugins/api-docs/src/routes.ts +++ b/plugins/api-docs/src/routes.ts @@ -23,9 +23,3 @@ export const rootRoute = createRouteRef({ path: '/api-docs', title: 'APIs', }); - -export const catalogRoute = createRouteRef({ - icon: NoIcon, - path: '', - title: 'API', -}); From 1a79777265de3b6ab9d0a2ae75d979919e30821f Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 3 Dec 2020 17:59:27 +0100 Subject: [PATCH 05/86] Only add the testdata to the new relative locations --- packages/catalog-model/examples-relative/all-apis.yaml | 2 ++ packages/catalog-model/examples-relative/all-components.yaml | 2 ++ .../apis/wayback-archive-api.yaml | 0 .../apis/wayback-search-api.yaml | 0 .../components/wayback-archive-component.yaml | 0 .../components/wayback-search-component.yaml | 0 packages/catalog-model/examples/all-apis.yaml | 2 -- packages/catalog-model/examples/all-components.yaml | 2 -- 8 files changed, 4 insertions(+), 4 deletions(-) rename packages/catalog-model/{examples => examples-relative}/apis/wayback-archive-api.yaml (100%) rename packages/catalog-model/{examples => examples-relative}/apis/wayback-search-api.yaml (100%) rename packages/catalog-model/{examples => examples-relative}/components/wayback-archive-component.yaml (100%) rename packages/catalog-model/{examples => examples-relative}/components/wayback-search-component.yaml (100%) diff --git a/packages/catalog-model/examples-relative/all-apis.yaml b/packages/catalog-model/examples-relative/all-apis.yaml index 20752faf97..e23f1b3656 100644 --- a/packages/catalog-model/examples-relative/all-apis.yaml +++ b/packages/catalog-model/examples-relative/all-apis.yaml @@ -10,3 +10,5 @@ spec: - ./apis/spotify-api.yaml - ./apis/streetlights-api.yaml - ./apis/swapi-graphql.yaml + - ./apis/wayback-archive-api.yaml + - ./apis/wayback-search-api.yaml diff --git a/packages/catalog-model/examples-relative/all-components.yaml b/packages/catalog-model/examples-relative/all-components.yaml index f29a7378a0..5db5825b20 100644 --- a/packages/catalog-model/examples-relative/all-components.yaml +++ b/packages/catalog-model/examples-relative/all-components.yaml @@ -14,3 +14,5 @@ spec: - ./components/playback-lib-component.yaml - ./components/www-artist-component.yaml - ./components/shuffle-api-component.yaml + - ./components/wayback-archive-component.yaml + - ./components/wayback-search-component.yaml diff --git a/packages/catalog-model/examples/apis/wayback-archive-api.yaml b/packages/catalog-model/examples-relative/apis/wayback-archive-api.yaml similarity index 100% rename from packages/catalog-model/examples/apis/wayback-archive-api.yaml rename to packages/catalog-model/examples-relative/apis/wayback-archive-api.yaml diff --git a/packages/catalog-model/examples/apis/wayback-search-api.yaml b/packages/catalog-model/examples-relative/apis/wayback-search-api.yaml similarity index 100% rename from packages/catalog-model/examples/apis/wayback-search-api.yaml rename to packages/catalog-model/examples-relative/apis/wayback-search-api.yaml diff --git a/packages/catalog-model/examples/components/wayback-archive-component.yaml b/packages/catalog-model/examples-relative/components/wayback-archive-component.yaml similarity index 100% rename from packages/catalog-model/examples/components/wayback-archive-component.yaml rename to packages/catalog-model/examples-relative/components/wayback-archive-component.yaml diff --git a/packages/catalog-model/examples/components/wayback-search-component.yaml b/packages/catalog-model/examples-relative/components/wayback-search-component.yaml similarity index 100% rename from packages/catalog-model/examples/components/wayback-search-component.yaml rename to packages/catalog-model/examples-relative/components/wayback-search-component.yaml diff --git a/packages/catalog-model/examples/all-apis.yaml b/packages/catalog-model/examples/all-apis.yaml index db7e5e31e1..0278fc3078 100644 --- a/packages/catalog-model/examples/all-apis.yaml +++ b/packages/catalog-model/examples/all-apis.yaml @@ -11,5 +11,3 @@ spec: - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/apis/spotify-api.yaml - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/apis/streetlights-api.yaml - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/apis/swapi-graphql.yaml - - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/apis/wayback-archive-api.yaml - - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/apis/wayback-search-api.yaml diff --git a/packages/catalog-model/examples/all-components.yaml b/packages/catalog-model/examples/all-components.yaml index 6160af3db3..06c44b59d7 100644 --- a/packages/catalog-model/examples/all-components.yaml +++ b/packages/catalog-model/examples/all-components.yaml @@ -15,5 +15,3 @@ spec: - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/playback-lib-component.yaml - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/www-artist-component.yaml - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/shuffle-api-component.yaml - - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/wayback-archive-component.yaml - - https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/wayback-search-component.yaml From 15710d8a1756392716e038d8ef6f59785e709640 Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Thu, 3 Dec 2020 16:40:27 -0500 Subject: [PATCH 06/86] Remove references to P1M duration --- .../cost-insights/src/api/CostInsightsApi.ts | 8 ++++---- .../components/CostGrowth/CostGrowth.test.tsx | 6 +++--- .../PeriodSelect/PeriodSelect.test.tsx | 1 - .../components/PeriodSelect/PeriodSelect.tsx | 9 +-------- .../ProductInsightsCard.test.tsx | 4 ++-- plugins/cost-insights/src/types/Duration.ts | 7 +++---- .../cost-insights/src/utils/change.test.ts | 2 +- plugins/cost-insights/src/utils/currency.ts | 1 - .../cost-insights/src/utils/duration.test.ts | 1 - plugins/cost-insights/src/utils/duration.ts | 11 ----------- .../src/utils/formatters.test.ts | 2 -- plugins/cost-insights/src/utils/formatters.ts | 19 ------------------- plugins/cost-insights/src/utils/mockData.ts | 2 +- 13 files changed, 15 insertions(+), 58 deletions(-) diff --git a/plugins/cost-insights/src/api/CostInsightsApi.ts b/plugins/cost-insights/src/api/CostInsightsApi.ts index 9baad5ffab..7c130e4ceb 100644 --- a/plugins/cost-insights/src/api/CostInsightsApi.ts +++ b/plugins/cost-insights/src/api/CostInsightsApi.ts @@ -38,7 +38,7 @@ export type ProductInsightsOptions = { group: string; /** - * A time duration, such as P1M. See the Duration type for a detailed explanation + * A time duration, such as P3M. See the Duration type for a detailed explanation * of how the durations are interpreted in Cost Insights. */ duration: Duration; @@ -90,7 +90,7 @@ export type CostInsightsApi = { * reduction) and compare it to metrics important to the business. * * @param group The group id from getUserGroups or query parameters - * @param intervals An ISO 8601 repeating interval string, such as R2/P1M/2020-09-01 + * @param intervals An ISO 8601 repeating interval string, such as R2/P30D/2020-09-01 * https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals */ getGroupDailyCost(group: string, intervals: string): Promise; @@ -108,7 +108,7 @@ export type CostInsightsApi = { * (or reduction) and compare it to metrics important to the business. * * @param project The project id from getGroupProjects or query parameters - * @param intervals An ISO 8601 repeating interval string, such as R2/P1M/2020-09-01 + * @param intervals An ISO 8601 repeating interval string, such as R2/P30D/2020-09-01 * https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals */ getProjectDailyCost(project: string, intervals: string): Promise; @@ -119,7 +119,7 @@ export type CostInsightsApi = { * (or reduction) of a project or group's daily costs. * * @param metric A metric from the cost-insights configuration in app-config.yaml. - * @param intervals An ISO 8601 repeating interval string, such as R2/P1M/2020-09-01 + * @param intervals An ISO 8601 repeating interval string, such as R2/P30D/2020-09-01 * https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals */ getDailyMetricData(metric: string, intervals: string): Promise; diff --git a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx index 9c76d6d998..12668140b6 100644 --- a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx +++ b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx @@ -54,7 +54,7 @@ describe.each` it(`formats ${engineers.unit}s correctly for ${expected}`, async () => { const { getByText } = await renderInTestApp( - + , ); expect(getByText(expected)).toBeInTheDocument(); @@ -73,7 +73,7 @@ describe.each` it(`formats ${usd.unit}s correctly for ${expected}`, async () => { const { getByText } = await renderInTestApp( - + , ); expect(getByText(expected)).toBeInTheDocument(); @@ -92,7 +92,7 @@ describe.each` it(`formats ${carbon.unit}s correctly for ${expected}`, async () => { const { getByText } = await renderInTestApp( - + , ); expect(getByText(expected)).toBeInTheDocument(); diff --git a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx index a3519ff61b..d8535cf791 100644 --- a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx +++ b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx @@ -66,7 +66,6 @@ describe('', () => { describe.each` duration - ${Duration.P1M} ${Duration.P3M} ${Duration.P90D} ${Duration.P30D} diff --git a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.tsx b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.tsx index 4908641d6f..459de68921 100644 --- a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.tsx +++ b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.tsx @@ -17,10 +17,7 @@ import React from 'react'; import { MenuItem, Select, SelectProps } from '@material-ui/core'; import { Duration } from '../../types'; -import { - formatLastTwoLookaheadQuarters, - formatLastTwoMonths, -} from '../../utils/formatters'; +import { formatLastTwoLookaheadQuarters } from '../../utils/formatters'; import { findAlways } from '../../utils/assert'; import { useSelectStyles as useStyles } from '../../utils/styles'; import { useLastCompleteBillingDate } from '../../hooks'; @@ -42,10 +39,6 @@ export function getDefaultOptions( value: Duration.P30D, label: 'Past 60 Days', }, - { - value: Duration.P1M, - label: formatLastTwoMonths(lastCompleteBillingDate), - }, { value: Duration.P3M, label: formatLastTwoLookaheadQuarters(lastCompleteBillingDate), diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx index 385ff82f9b..c9c7618e1e 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx @@ -81,7 +81,7 @@ describe('', () => { const rendered = await renderProductInsightsCardInTestApp( mockProductCost, MockComputeEngine, - Duration.P1M, + Duration.P30D, ); expect( rendered.queryByTestId(`scroll-test-compute-engine`), @@ -113,7 +113,7 @@ describe('', () => { const rendered = await renderProductInsightsCardInTestApp( entity, MockComputeEngine, - Duration.P1M, + Duration.P30D, ); const subheaderRgx = new RegExp(subheader); expect(rendered.getByText(subheaderRgx)).toBeInTheDocument(); diff --git a/plugins/cost-insights/src/types/Duration.ts b/plugins/cost-insights/src/types/Duration.ts index acf707dd1e..c0f03d5c27 100644 --- a/plugins/cost-insights/src/types/Duration.ts +++ b/plugins/cost-insights/src/types/Duration.ts @@ -15,15 +15,14 @@ */ /** - * Time periods for cost comparison; slight abuse of ISO 8601 periods. We take P1M and P3M to mean - * 'last completed [month|quarter]', and P30D/P90D to be '[month|quarter] relative to today'. So if - * it's September 15, P1M represents costs for the month of August and P30D represents August 16 - + * Time periods for cost comparison; slight abuse of ISO 8601 periods. We take P3M to mean + * 'last completed quarter', and P30D/P90D to be '[month|quarter] relative to today'. So if + * it's September 15, P3M represents costs for Q2 and P30D represents August 16 - * September 15. */ export enum Duration { P30D = 'P30D', P90D = 'P90D', - P1M = 'P1M', P3M = 'P3M', } diff --git a/plugins/cost-insights/src/utils/change.test.ts b/plugins/cost-insights/src/utils/change.test.ts index f9e8cc4d6d..7cc03caa0b 100644 --- a/plugins/cost-insights/src/utils/change.test.ts +++ b/plugins/cost-insights/src/utils/change.test.ts @@ -80,7 +80,7 @@ describe('getPreviousPeriodTotalCost', () => { expect( getPreviousPeriodTotalCost( mockGroupDailyCost.aggregation, - Duration.P1M, + Duration.P30D, exclusiveEndDate, ), ).toEqual(100_000); diff --git a/plugins/cost-insights/src/utils/currency.ts b/plugins/cost-insights/src/utils/currency.ts index 663a29a112..f1d67a14e4 100644 --- a/plugins/cost-insights/src/utils/currency.ts +++ b/plugins/cost-insights/src/utils/currency.ts @@ -18,7 +18,6 @@ import { assertNever } from '../utils/assert'; export const rateOf = (cost: number, duration: Duration) => { switch (duration) { - case Duration.P1M: case Duration.P30D: return cost / 12; case Duration.P90D: diff --git a/plugins/cost-insights/src/utils/duration.test.ts b/plugins/cost-insights/src/utils/duration.test.ts index a5509eda07..45d8e5b6a3 100644 --- a/plugins/cost-insights/src/utils/duration.test.ts +++ b/plugins/cost-insights/src/utils/duration.test.ts @@ -23,7 +23,6 @@ describe.each` duration | startDate | endDate ${Duration.P30D} | ${'2020-04-06'} | ${'2020-06-05'} ${Duration.P90D} | ${'2019-12-08'} | ${'2020-06-05'} - ${Duration.P1M} | ${'2020-04-01'} | ${'2020-05-31'} ${Duration.P3M} | ${'2019-10-01'} | ${'2020-03-31'} `('Calculates interval dates correctly', ({ duration, startDate, endDate }) => { it(`Calculates dates correctly for ${duration}`, () => { diff --git a/plugins/cost-insights/src/utils/duration.ts b/plugins/cost-insights/src/utils/duration.ts index 810160c7b6..5a3fb52271 100644 --- a/plugins/cost-insights/src/utils/duration.ts +++ b/plugins/cost-insights/src/utils/duration.ts @@ -37,12 +37,6 @@ export function inclusiveStartDateOf( .utc() .subtract(moment.duration(duration).add(moment.duration(duration))) .format(DEFAULT_DATE_FORMAT); - case Duration.P1M: - return moment(exclusiveEndDate) - .utc() - .startOf('month') - .subtract(moment.duration(duration).add(moment.duration(duration))) - .format(DEFAULT_DATE_FORMAT); case Duration.P3M: return moment(exclusiveEndDate) .utc() @@ -65,11 +59,6 @@ export function exclusiveEndDateOf( .utc() .add(1, 'day') .format(DEFAULT_DATE_FORMAT); - case Duration.P1M: - return moment(inclusiveEndDate) - .utc() - .startOf('month') - .format(DEFAULT_DATE_FORMAT); case Duration.P3M: return moment(inclusiveEndDate) .utc() diff --git a/plugins/cost-insights/src/utils/formatters.test.ts b/plugins/cost-insights/src/utils/formatters.test.ts index db25b6f3b7..a403f1ebba 100644 --- a/plugins/cost-insights/src/utils/formatters.test.ts +++ b/plugins/cost-insights/src/utils/formatters.test.ts @@ -57,8 +57,6 @@ describe('date formatters', () => { describe.each` duration | date | isEndDate | output - ${Duration.P1M} | ${'2020-10-11'} | ${true} | ${'September 2020'} - ${Duration.P1M} | ${'2020-10-11'} | ${false} | ${'August 2020'} ${Duration.P3M} | ${'2020-10-11'} | ${true} | ${'Q3 2020'} ${Duration.P3M} | ${'2020-10-11'} | ${false} | ${'Q2 2020'} ${Duration.P30D} | ${'2020-10-11'} | ${true} | ${'Last 30 Days'} diff --git a/plugins/cost-insights/src/utils/formatters.ts b/plugins/cost-insights/src/utils/formatters.ts index 9c3b2d643e..454f9301a4 100644 --- a/plugins/cost-insights/src/utils/formatters.ts +++ b/plugins/cost-insights/src/utils/formatters.ts @@ -104,19 +104,6 @@ export function formatLastTwoLookaheadQuarters(inclusiveEndDate: string) { return `${start} vs ${end}`; } -export function formatLastTwoMonths(inclusiveEndDate: string) { - const exclusiveEndDate = moment(inclusiveEndDate) - .add(1, 'day') - .format(DEFAULT_DATE_FORMAT); - const start = moment(inclusiveStartDateOf(Duration.P1M, exclusiveEndDate)) - .utc() - .format('MMMM'); - const end = moment(inclusiveEndDateOf(Duration.P1M, inclusiveEndDate)) - .utc() - .format('MMMM'); - return `${start} vs ${end}`; -} - const formatRelativePeriod = ( duration: Duration, date: string, @@ -137,12 +124,6 @@ export function formatPeriod( isEndDate: boolean, ) { switch (duration) { - case Duration.P1M: - return monthOf( - isEndDate - ? inclusiveEndDateOf(duration, date) - : inclusiveStartDateOf(duration, date), - ); case Duration.P3M: return quarterOf( isEndDate diff --git a/plugins/cost-insights/src/utils/mockData.ts b/plugins/cost-insights/src/utils/mockData.ts index 76bdf5f08f..6862d11141 100644 --- a/plugins/cost-insights/src/utils/mockData.ts +++ b/plugins/cost-insights/src/utils/mockData.ts @@ -143,7 +143,7 @@ export const MockProductTypes: Record = { export const MockProductFilters: ProductFilters = Object.keys( MockProductTypes, -).map(productType => ({ duration: Duration.P1M, productType })); +).map(productType => ({ duration: Duration.P30D, productType })); export const MockProducts: Product[] = Object.keys(MockProductTypes).map( productType => From bb689a2c4c2691bd2209b721b4e103b61c8a4daa Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 4 Dec 2020 11:06:21 +0100 Subject: [PATCH 07/86] Fix typo Co-authored-by: Adam Harvey --- .changeset/brave-zoos-fail.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/brave-zoos-fail.md b/.changeset/brave-zoos-fail.md index dfb18fe71c..3d24215cac 100644 --- a/.changeset/brave-zoos-fail.md +++ b/.changeset/brave-zoos-fail.md @@ -2,4 +2,4 @@ '@backstage/plugin-jenkins': patch --- -Avoid loading data from Jenkins twice. Don't load data when navigating thought the pages as all data from all pages is already loaded. +Avoid loading data from Jenkins twice. Don't load data when navigating through the pages as all data from all pages is already loaded. From 0f88771685e80e4be129cef6a8d57a1adf7f3054 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 4 Dec 2020 14:34:46 +0100 Subject: [PATCH 08/86] Improve loading speed of the CI/CD page Improve loading speed of the CI/CD page. Only request the necessary fields from Jenkins to keep the request size low. In addition everything is loaded in a single request, instead of requesting each job and build individually. As this (and also the previous behavior) can lead to a big amount of data, this limits the amount of jobs to 50. For each job, only the latest build is loaded. Loading the full build history of a job can lead to excessive load on the Jenkins instance. --- .changeset/purple-radios-complain.md | 11 ++++++ plugins/jenkins/src/api/JenkinsApi.ts | 48 +++++++++++++++++++-------- 2 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 .changeset/purple-radios-complain.md diff --git a/.changeset/purple-radios-complain.md b/.changeset/purple-radios-complain.md new file mode 100644 index 0000000000..2fde60ddff --- /dev/null +++ b/.changeset/purple-radios-complain.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-jenkins': patch +--- + +Improve loading speed of the CI/CD page. +Only request the necessary fields from Jenkins to keep the request size low. +In addition everything is loaded in a single request, instead of requesting +each job and build individually. As this (and also the previous behavior) can +lead to a big amount of data, this limits the amount of jobs to 50. +For each job, only the latest build is loaded. Loading the full build history +of a job can lead to excessive load on the Jenkins instance. diff --git a/plugins/jenkins/src/api/JenkinsApi.ts b/plugins/jenkins/src/api/JenkinsApi.ts index 0fa48a1d17..8dbde9160d 100644 --- a/plugins/jenkins/src/api/JenkinsApi.ts +++ b/plugins/jenkins/src/api/JenkinsApi.ts @@ -107,26 +107,48 @@ export class JenkinsApi { async getFolder(folderName: string) { const client = await this.getClient(); - const folder = await client.job.get(folderName); + const folder = await client.job.get({ + name: folderName, + // Filter only be the information we need, instead of loading all fields. + // Limit to only show the latest build for each job and only load 50 jobs + // at all. + // Whitespaces are only included for readablity here and stripped out + // before sending to Jenkins + tree: `jobs[ + actions[*], + builds[ + number, + url, + fullDisplayName, + building, + result, + actions[ + *[ + *[ + *[ + * + ] + ] + ] + ] + ]{0,1}, + jobs{0,1}, + name + ]{0,50} + `.replace(/\s/g, ''), + }); const results = []; - for (const jobSummary of folder.jobs) { - const jobDetails = await client.job.get({ - name: `${folderName}/${jobSummary.name}`, - depth: 1, - }); + for (const jobDetails of folder.jobs) { const jobScmInfo = this.extractScmDetailsFromJob(jobDetails); if (jobDetails.jobs) { // skipping folders inside folders for now } else { for (const buildDetails of jobDetails.builds) { - const build = await client.build.get({ - name: `${folderName}/${jobSummary.name}`, - number: buildDetails.number, - depth: 1, - }); - - const ciTable = this.mapJenkinsBuildToCITable(build, jobScmInfo); + const ciTable = this.mapJenkinsBuildToCITable( + buildDetails, + jobScmInfo, + ); results.push(ciTable); } } From c81456a175057de85d68e83c39b91c8173318fb7 Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Fri, 4 Dec 2020 10:18:17 -0500 Subject: [PATCH 09/86] Update quarter end date logic --- .../components/PeriodSelect/PeriodSelect.test.tsx | 4 ++-- plugins/cost-insights/src/utils/duration.ts | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx index d8535cf791..ae513c04e0 100644 --- a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx +++ b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx @@ -73,8 +73,9 @@ describe('', () => { it(`Should select ${duration}`, async () => { const mockOnSelect = jest.fn(); const mockAggregation = + // Can't select an option that's already the default DefaultPageFilters.duration === duration - ? Duration.P1M + ? Duration.P30D : DefaultPageFilters.duration; const rendered = await renderInTestApp( @@ -88,7 +89,6 @@ describe('', () => { const button = getByRole(periodSelect, 'button'); UserEvent.click(button); - await waitFor(() => rendered.getByText('Past 60 Days')); UserEvent.click(rendered.getByTestId(`period-select-option-${duration}`)); expect(mockOnSelect).toHaveBeenLastCalledWith(duration); }); diff --git a/plugins/cost-insights/src/utils/duration.ts b/plugins/cost-insights/src/utils/duration.ts index 5a3fb52271..2ca2817324 100644 --- a/plugins/cost-insights/src/utils/duration.ts +++ b/plugins/cost-insights/src/utils/duration.ts @@ -60,10 +60,7 @@ export function exclusiveEndDateOf( .add(1, 'day') .format(DEFAULT_DATE_FORMAT); case Duration.P3M: - return moment(inclusiveEndDate) - .utc() - .startOf('quarter') - .format(DEFAULT_DATE_FORMAT); + return quarterEndDate(inclusiveEndDate); default: return assertNever(duration); } @@ -83,3 +80,12 @@ export function inclusiveEndDateOf( export function intervalsOf(duration: Duration, inclusiveEndDate: string) { return `R2/${duration}/${exclusiveEndDateOf(duration, inclusiveEndDate)}`; } + +function quarterEndDate(inclusiveEndDate: string): string { + const endDate = moment(inclusiveEndDate).utc(); + const endOfQuarter = endDate.endOf('quarter').format(DEFAULT_DATE_FORMAT); + if (endOfQuarter === inclusiveEndDate) { + return inclusiveEndDate; + } + return endDate.startOf('quarter').format(DEFAULT_DATE_FORMAT); +} From 88ef11b45d285d6b37a89efd6259b359953ed3d7 Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Fri, 4 Dec 2020 10:20:00 -0500 Subject: [PATCH 10/86] Add changeset --- .changeset/cost-insights-wild-pumpkins-tie.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cost-insights-wild-pumpkins-tie.md diff --git a/.changeset/cost-insights-wild-pumpkins-tie.md b/.changeset/cost-insights-wild-pumpkins-tie.md new file mode 100644 index 0000000000..6fdc7c0a5a --- /dev/null +++ b/.changeset/cost-insights-wild-pumpkins-tie.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': minor +--- + +Remove calendar MoM period option and fix quarter end date logic From b81c973f2eeb52dd47d2f1f45c28064201578f91 Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Fri, 4 Dec 2020 10:23:31 -0500 Subject: [PATCH 11/86] Add one day to get exclusive date --- plugins/cost-insights/src/utils/duration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cost-insights/src/utils/duration.ts b/plugins/cost-insights/src/utils/duration.ts index 2ca2817324..0d9d6aa179 100644 --- a/plugins/cost-insights/src/utils/duration.ts +++ b/plugins/cost-insights/src/utils/duration.ts @@ -85,7 +85,7 @@ function quarterEndDate(inclusiveEndDate: string): string { const endDate = moment(inclusiveEndDate).utc(); const endOfQuarter = endDate.endOf('quarter').format(DEFAULT_DATE_FORMAT); if (endOfQuarter === inclusiveEndDate) { - return inclusiveEndDate; + return endDate.add(1, 'day').format(DEFAULT_DATE_FORMAT); } return endDate.startOf('quarter').format(DEFAULT_DATE_FORMAT); } From 8d28d2c98448f3a77add6997c31fa2535b932dc3 Mon Sep 17 00:00:00 2001 From: Remi Date: Fri, 4 Dec 2020 16:35:18 +0100 Subject: [PATCH 12/86] feat(storybook-drawer): use theme spacing --- .../src/components/Drawer/Drawer.stories.tsx | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/packages/core/src/components/Drawer/Drawer.stories.tsx b/packages/core/src/components/Drawer/Drawer.stories.tsx index 9bb60c78a4..b399cfed8e 100644 --- a/packages/core/src/components/Drawer/Drawer.stories.tsx +++ b/packages/core/src/components/Drawer/Drawer.stories.tsx @@ -21,6 +21,8 @@ import { Typography, makeStyles, IconButton, + createStyles, + Theme, } from '@material-ui/core'; import Close from '@material-ui/icons/Close'; @@ -29,31 +31,35 @@ export default { component: Drawer, }; -const useDrawerStyles = makeStyles({ - paper: { - width: '50%', - justifyContent: 'space-between', - padding: '20px', - }, -}); +const useDrawerStyles = makeStyles((theme: Theme) => + createStyles({ + paper: { + width: '50%', + justifyContent: 'space-between', + padding: theme.spacing(2.5), + }, + }), +); -const useDrawerContentStyles = makeStyles({ - header: { - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - }, - icon: { - fontSize: 20, - }, - content: { - height: '80%', - backgroundColor: '#EEEEEE', - }, - secondaryAction: { - marginLeft: '20px', - }, -}); +const useDrawerContentStyles = makeStyles((theme: Theme) => + createStyles({ + header: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + }, + icon: { + fontSize: 20, + }, + content: { + height: '80%', + backgroundColor: '#EEEEEE', + }, + secondaryAction: { + marginLeft: theme.spacing(2.5), + }, + }), +); /* Example content wrapped inside the Drawer component */ const DrawerContent = ({ From 12bbd748c58e1f783d30f2b1dd6f2c30b63e6dca Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 4 Dec 2020 17:00:39 +0100 Subject: [PATCH 13/86] Remove prometheus depenedency from backend-common --- .../rare-lemons-hug.md | 66 ++++++++++++++----- packages/backend-common/package.json | 2 - .../src/service/lib/ServiceBuilderImpl.ts | 8 --- .../src/service/lib/metrics.test.ts | 37 ----------- yarn.lock | 34 +--------- 5 files changed, 49 insertions(+), 98 deletions(-) rename packages/backend-common/src/service/lib/metrics.ts => .changeset/rare-lemons-hug.md (50%) delete mode 100644 packages/backend-common/src/service/lib/metrics.test.ts diff --git a/packages/backend-common/src/service/lib/metrics.ts b/.changeset/rare-lemons-hug.md similarity index 50% rename from packages/backend-common/src/service/lib/metrics.ts rename to .changeset/rare-lemons-hug.md index 37d441f53c..22933bf1c4 100644 --- a/packages/backend-common/src/service/lib/metrics.ts +++ b/.changeset/rare-lemons-hug.md @@ -1,21 +1,34 @@ -/* - * 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 prom from 'prom-client'; -import promBundle from 'express-prom-bundle'; +--- +'@backstage/backend-common': minor +--- + +Removes the Prometheus integration from `backend-common`. + +Rational behind this change is to keep the metrics integration of Backstage +generic. Instead of directly relying on Prometheus, Backstage will expose +metrics in a generic way. Integrators can then export the metrics in their +desired format. For example using Prometheus. + +To keep the existing behavior, you need to integrate Prometheus in your +backend: + +First, add a dependency on `express-prom-bundle` and `prom-client` to your backend. + +```diff +// packages/backend/package.json + "dependencies": { ++ "express-prom-bundle": "^6.1.0", ++ "prom-client": "^12.0.0", +``` + +Then, add a handler for metrics and a simple instrumentation for the endpoints. + +```typescript +// packages/backend/src/metrics.ts +import { useHotCleanup } from '@backstage/backend-common'; import { RequestHandler } from 'express'; +import promBundle from 'express-prom-bundle'; +import prom from 'prom-client'; import * as url from 'url'; const rootRegEx = new RegExp('^/([^/]*)/.*'); @@ -38,7 +51,7 @@ export function normalizePath(req: any): string { */ export function metricsHandler(): RequestHandler { // We can only initialize the metrics once and have to clean them up between hot reloads - prom.register.clear(); + useHotCleanup(module, () => prom.register.clear()); return promBundle({ includeMethod: true, @@ -51,3 +64,20 @@ export function metricsHandler(): RequestHandler { promClient: { collectDefaultMetrics: {} }, }); } +``` + +Last, extend your router configuration with the `metricsHandler`: + +```diff ++import { metricsHandler } from './metrics'; + +... + + const service = createServiceBuilder(module) + .loadConfig(config) + .addRouter('', await healthcheck(healthcheckEnv)) ++ .addRouter('', metricsHandler()) + .addRouter('/api', apiRouter); +``` + +Your Prometheus metrics will be available at the `/metrics` endpoint. diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 04e189abca..13b827762c 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -41,7 +41,6 @@ "cors": "^2.8.5", "cross-fetch": "^3.0.6", "express": "^4.17.1", - "express-prom-bundle": "^6.1.0", "express-promise-router": "^3.0.3", "fs-extra": "^9.0.1", "git-url-parse": "^11.4.0", @@ -51,7 +50,6 @@ "logform": "^2.1.1", "minimist": "^1.2.5", "morgan": "^1.10.0", - "prom-client": "^12.0.0", "selfsigned": "^1.10.7", "stoppable": "^1.1.0", "tar": "^6.0.5", diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index b47f4ef7e5..778e25f71c 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -39,7 +39,6 @@ import { readHttpsSettings, } from './config'; import { createHttpServer, createHttpsServer } from './hostFactory'; -import { metricsHandler } from './metrics'; export const DEFAULT_PORT = 7000; // '' is express default, which listens to all interfaces @@ -66,7 +65,6 @@ export class ServiceBuilderImpl implements ServiceBuilder { private corsOptions: cors.CorsOptions | undefined; private cspOptions: Record | undefined; private httpsSettings: HttpsSettings | undefined; - private enableMetrics: boolean = true; private routers: [string, Router][]; // Reference to the module where builder is created - needed for hot module // reloading @@ -109,9 +107,6 @@ export class ServiceBuilderImpl implements ServiceBuilder { this.httpsSettings = httpsSettings; } - // For now, configuration of metrics is a simple boolean and active by default - this.enableMetrics = backendConfig.getOptionalBoolean('metrics') !== false; - return this; } @@ -166,9 +161,6 @@ export class ServiceBuilderImpl implements ServiceBuilder { app.use(cors(corsOptions)); } app.use(compression()); - if (this.enableMetrics) { - app.use(metricsHandler()); - } app.use(requestLoggingHandler()); for (const [root, route] of this.routers) { app.use(root, route); diff --git a/packages/backend-common/src/service/lib/metrics.test.ts b/packages/backend-common/src/service/lib/metrics.test.ts deleted file mode 100644 index 9126423b7e..0000000000 --- a/packages/backend-common/src/service/lib/metrics.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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 { normalizePath } from './metrics'; - -describe('normalizePath', () => { - it('should normalize /path to /path', async () => { - const path = normalizePath({ url: 'http://server/path' }); - - expect(path).toBe('/path'); - }); - - it('should normalize /path/test to /path', async () => { - const path = normalizePath({ url: 'http://server/path/test' }); - - expect(path).toBe('/path'); - }); - - it('should normalize /api/plugin-name/test to /api/plugin-name', async () => { - const path = normalizePath({ url: 'http://server/api/plugin-name/test' }); - - expect(path).toBe('/api/plugin-name'); - }); -}); diff --git a/yarn.lock b/yarn.lock index 44024b5952..71eccec1a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7922,11 +7922,6 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bintrees@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz#0e655c9b9c2435eaab68bf4027226d2b55a34524" - integrity sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ= - bl@^1.0.0: version "1.2.2" resolved "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" @@ -11724,14 +11719,6 @@ expect@^26.5.3: jest-message-util "^26.5.2" jest-regex-util "^26.0.0" -express-prom-bundle@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/express-prom-bundle/-/express-prom-bundle-6.1.0.tgz#8fd72e5bedbbd686b8e7c49e0aecbc1b14b28743" - integrity sha512-krlvp5r6sgJ1IwL6M6/coMrNbAlwtpk+uyivfeyRMCupTK4HzIEQHH0gwrNhLiKyPmSbtZcSmtO6s+PRumRp5g== - dependencies: - on-finished "^2.3.0" - url-value-parser "^2.0.0" - express-promise-router@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/express-promise-router/-/express-promise-router-3.0.3.tgz#5e6d22a5a3f013d71833172fe8d7ab780c3f6b70" @@ -17917,7 +17904,7 @@ 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== -on-finished@^2.3.0, 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" integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= @@ -19514,13 +19501,6 @@ progress@^2.0.0: resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -prom-client@^12.0.0: - version "12.0.0" - resolved "https://registry.npmjs.org/prom-client/-/prom-client-12.0.0.tgz#9689379b19bd3f6ab88a9866124db9da3d76c6ed" - integrity sha512-JbzzHnw0VDwCvoqf8y1WDtq4wSBAbthMB1pcVI/0lzdqHGJI3KBJDXle70XK+c7Iv93Gihqo0a5LlOn+g8+DrQ== - dependencies: - tdigest "^0.1.1" - promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" @@ -22950,13 +22930,6 @@ tarn@^3.0.1: resolved "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz#ebac2c6dbc6977d34d4526e0a7814200386a8aec" integrity sha512-6usSlV9KyHsspvwu2duKH+FMUhqJnAh6J5J/4MITl8s94iSUQTLkJggdiewKv4RyARQccnigV48Z+khiuVZDJw== -tdigest@^0.1.1: - version "0.1.1" - resolved "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz#2e3cb2c39ea449e55d1e6cd91117accca4588021" - integrity sha1-Ljyyw56kSeVdHmzZEReszKRYgCE= - dependencies: - bintrees "1.0.1" - telejson@^5.0.2: version "5.0.2" resolved "https://registry.npmjs.org/telejson/-/telejson-5.0.2.tgz#ed1e64be250cc1c757a53c19e1740b49832b3d51" @@ -23961,11 +23934,6 @@ url-parse@^1.4.3, url-parse@^1.4.7: querystringify "^2.1.1" requires-port "^1.0.0" -url-value-parser@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/url-value-parser/-/url-value-parser-2.0.1.tgz#c8179a095ab9ec1f5aa17ca36af5af396b4e95ed" - integrity sha512-bexECeREBIueboLGM3Y1WaAzQkIn+Tca/Xjmjmfd0S/hFHSCEoFkNh0/D0l9G4K74MkEP/lLFRlYnxX3d68Qgw== - url@^0.11.0, url@~0.11.0: version "0.11.0" resolved "https://registry.npmjs.org/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" From e3071a0d496b60498c7caa9ab09fddc786f3a1b2 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Fri, 4 Dec 2020 09:12:29 -0700 Subject: [PATCH 14/86] Add support for multiple types of entity cost breakdown --- .../cost-insights-quick-lizards-smash.md | 7 + plugins/cost-insights/package.json | 2 + .../cost-insights/src/api/CostInsightsApi.ts | 1 + .../ProductInsights/ProductInsights.test.tsx | 12 +- .../ProductEntityDialog.test.tsx | 134 +++ .../ProductEntityDialog.tsx | 178 +--- .../ProductEntityTable.tsx | 174 ++++ .../ProductInsightsCard.test.tsx | 14 +- .../ProductInsightsCard.tsx | 22 +- .../ProductInsightsChart.tsx | 45 +- .../ProjectGrowthAlertCard.tsx | 4 +- .../ProjectGrowthInstructionsPage.tsx | 42 +- .../UnlabeledDataflowAlertCard.tsx | 6 +- plugins/cost-insights/src/types/Entity.ts | 112 ++- plugins/cost-insights/src/utils/assert.ts | 6 + plugins/cost-insights/src/utils/formatters.ts | 4 +- plugins/cost-insights/src/utils/grammar.ts | 14 - plugins/cost-insights/src/utils/mockData.ts | 773 ++++++++++-------- yarn.lock | 10 + 19 files changed, 933 insertions(+), 627 deletions(-) create mode 100644 .changeset/cost-insights-quick-lizards-smash.md create mode 100644 plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.test.tsx create mode 100644 plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx diff --git a/.changeset/cost-insights-quick-lizards-smash.md b/.changeset/cost-insights-quick-lizards-smash.md new file mode 100644 index 0000000000..eae60fc744 --- /dev/null +++ b/.changeset/cost-insights-quick-lizards-smash.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-cost-insights': minor +--- + +Add support for multiple types of entity cost breakdown. + +This change is backwards-incompatible with plugin-cost-insights 0.3.x; the `entities` field on Entity returned in product cost queries changed from `Entity[]` to `Record; + /** * Get current cost alerts for a given group. These show up as Action Items for the group on the * Cost Insights page. Alerts may include cost-saving recommendations, such as infrastructure diff --git a/plugins/cost-insights/src/components/ProductInsights/ProductInsights.test.tsx b/plugins/cost-insights/src/components/ProductInsights/ProductInsights.test.tsx index 85f39f9388..45d0946f86 100644 --- a/plugins/cost-insights/src/components/ProductInsights/ProductInsights.test.tsx +++ b/plugins/cost-insights/src/components/ProductInsights/ProductInsights.test.tsx @@ -40,7 +40,7 @@ const MockComputeEngine: Product = { const MockComputeEngineInsights: Entity = { id: 'compute-engine', - entities: [], + entities: {}, aggregation: [0, 0], change: { ratio: 0, @@ -55,7 +55,7 @@ const MockCloudDataflow: Product = { const MockCloudDataflowInsights: Entity = { id: MockCloudDataflow.kind, - entities: [], + entities: {}, aggregation: [1_000, 2_000], change: { ratio: 1, @@ -70,7 +70,7 @@ const MockCloudStorage: Product = { const MockCloudStorageInsights: Entity = { id: MockCloudStorage.kind, - entities: [], + entities: {}, aggregation: [2_000, 4_000], change: { ratio: 1, @@ -85,7 +85,7 @@ const MockBigQuery: Product = { const MockBigQueryInsights: Entity = { id: MockBigQuery.kind, - entities: [], + entities: {}, aggregation: [8_000, 16_000], change: { ratio: 1, @@ -100,7 +100,7 @@ const MockBigTable: Product = { const MockBigTableInsights: Entity = { id: MockBigTable.kind, - entities: [], + entities: {}, aggregation: [16_000, 32_000], change: { ratio: 1, @@ -115,7 +115,7 @@ const MockCloudPubSub: Product = { const MockCloudPubSubInsights: Entity = { id: MockCloudPubSub.kind, - entities: [], + entities: {}, aggregation: [32_000, 64_000], change: { ratio: 1, diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.test.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.test.tsx new file mode 100644 index 0000000000..076edf60a3 --- /dev/null +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.test.tsx @@ -0,0 +1,134 @@ +/* + * 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 { wrapInTestApp } from '@backstage/test-utils'; +import { ProductEntityDialog } from './ProductEntityDialog'; +import { render } from '@testing-library/react'; +import { Entity } from '../../types'; + +const atomicEntity: Entity = { + id: null, + aggregation: [0, 0], + change: { ratio: 0, amount: 0 }, + entities: {}, +}; + +const singleBreakdownEntity = { + ...atomicEntity, + entities: { + SKU: [ + { + id: 'sku-1', + aggregation: [0, 0], + change: { ratio: 0, amount: 0 }, + entities: {}, + }, + { + id: 'sku-2', + aggregation: [0, 0], + change: { ratio: 0, amount: 0 }, + entities: {}, + }, + ] as Entity[], + }, +}; + +const multiBreakdownEntity = { + ...singleBreakdownEntity, + entities: { + ...singleBreakdownEntity.entities, + deployment: [ + { + id: 'd-1', + aggregation: [0, 0], + change: { ratio: 0, amount: 0 }, + entities: {}, + }, + { + id: 'd-2', + aggregation: [0, 0], + change: { ratio: 0, amount: 0 }, + entities: {}, + }, + ] as Entity[], + }, +}; + +describe('', () => { + it('Should error if no sub-entities exist', () => { + expect(() => + render( + wrapInTestApp( + , + ), + ), + ).toThrow(); + }); + + it('Should not show tabs for a single sub-entity type', () => { + const { queryByText } = render( + wrapInTestApp( + , + ), + ); + expect(queryByText('Breakdown by SKU')).not.toBeInTheDocument(); + }); + + it('Should show tabs when multiple sub-entity types exist', () => { + const { getByText } = render( + wrapInTestApp( + , + ), + ); + expect(getByText('Breakdown by SKU')).toBeInTheDocument(); + expect(getByText('Breakdown by deployment')).toBeInTheDocument(); + expect(getByText('sku-1')).toBeInTheDocument(); + }); + + it('Shows the pre-selected tab, if provided', () => { + const { getByText } = render( + wrapInTestApp( + , + ), + ); + expect(getByText('d-1')).toBeInTheDocument(); + }); +}); diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.tsx index d617c0df35..a98e5dc87a 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.tsx @@ -14,179 +14,55 @@ * limitations under the License. */ -import React from 'react'; -import classnames from 'classnames'; -import { Table, TableColumn } from '@backstage/core'; -import { Dialog, IconButton, Typography } from '@material-ui/core'; +import React, { useState } from 'react'; +import { HeaderTabs } from '@backstage/core'; +import { Dialog, IconButton } from '@material-ui/core'; import { default as CloseButton } from '@material-ui/icons/Close'; -import { CostGrowthIndicator } from '../CostGrowth'; -import { costFormatter, formatPercent } from '../../utils/formatters'; import { useEntityDialogStyles as useStyles } from '../../utils/styles'; -import { BarChartOptions, Entity } from '../../types'; - -function createRenderer(col: keyof RowData, classes: Record) { - return function render(rowData: {}): JSX.Element { - const row = rowData as RowData; - const rowStyles = classnames(classes.row, { - [classes.rowTotal]: row.id === 'total', - [classes.colFirst]: col === 'label', - [classes.colLast]: col === 'ratio', - }); - - switch (col) { - case 'previous': - case 'current': - return ( - - {costFormatter.format(row[col])} - - ); - case 'ratio': - return ( - formatPercent(Math.abs(amount))} - /> - ); - default: - return {row.label}; - } - }; -} - -// material-table does not support fixed rows. Override the sorting algorithm -// to force Total row to bottom by default or when a user sort toggles a column. -function createSorter(field?: keyof Omit) { - return function rowSort(data1: {}, data2: {}): number { - const a = data1 as RowData; - const b = data2 as RowData; - if (a.id === 'total') return 1; - if (b.id === 'total') return 1; - if (field === 'label') return a.label.localeCompare(b.label); - - return field - ? a[field] - b[field] - : b.previous + b.current - (a.previous - a.current); - }; -} - -const defaultEntity: Entity = { - id: null, - aggregation: [0, 0], - change: { ratio: 0, amount: 0 }, - entities: [], -}; - -type RowData = { - id: string; - label: string; - previous: number; - current: number; - ratio: number; -}; - -type ProductEntityDialogOptions = Partial< - Pick ->; +import { Entity } from '../../types'; +import { + ProductEntityTable, + ProductEntityTableOptions, +} from './ProductEntityTable'; +import { findAlways } from '../../utils/assert'; type ProductEntityDialogProps = { open: boolean; - entity?: Entity; - entitiesLabel: string; - options?: ProductEntityDialogOptions; + entity: Entity; + options?: ProductEntityTableOptions; onClose: () => void; }; export const ProductEntityDialog = ({ open, - entity = defaultEntity, - entitiesLabel, + entity, options = {}, onClose, }: ProductEntityDialogProps) => { const classes = useStyles(); - - const data = Object.assign( - { - previousName: 'Previous', - currentName: 'Current', - }, - options, + const labels = Object.keys(entity.entities); + const [selectedLabel, setSelectedLabel] = useState( + findAlways(labels, _ => true), ); - const firstColClasses = classnames(classes.column, classes.colFirst); - const lastColClasses = classnames(classes.column, classes.colLast); - - const columns: TableColumn[] = [ - { - field: 'label', - title: ( - {entitiesLabel} - ), - render: createRenderer('label', classes), - customSort: createSorter('label'), - width: '33.33%', - }, - { - field: 'previous', - title: ( - {data.previousName} - ), - align: 'right', - render: createRenderer('previous', classes), - customSort: createSorter('previous'), - }, - { - field: 'current', - title: ( - {data.currentName} - ), - align: 'right', - render: createRenderer('current', classes), - customSort: createSorter('current'), - }, - { - field: 'ratio', - title: M/M, - align: 'right', - render: createRenderer('ratio', classes), - customSort: createSorter('ratio'), - }, - ]; - - const rowData: RowData[] = entity.entities - .map(e => ({ - id: e.id || 'Unknown', - label: e.id || 'Unknown', - previous: e.aggregation[0], - current: e.aggregation[1], - ratio: e.change.ratio, - })) - .concat({ - id: 'total', - label: 'Total', - previous: entity.aggregation[0], - current: entity.aggregation[1], - ratio: entity.change.ratio, - }) - .sort(createSorter()); + const tabs = labels.map((label, index) => ({ + id: index.toString(), + label: `Breakdown by ${label}`, + })); return ( - setSelectedLabel(labels[index])} + /> + ); diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx new file mode 100644 index 0000000000..7c239ba30d --- /dev/null +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx @@ -0,0 +1,174 @@ +/* + * 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 classnames from 'classnames'; +import { Table, TableColumn } from '@backstage/core'; +import { Typography } from '@material-ui/core'; +import { costFormatter, formatPercent } from '../../utils/formatters'; +import { useEntityDialogStyles as useStyles } from '../../utils/styles'; +import { CostGrowthIndicator } from '../CostGrowth'; +import { BarChartOptions, Entity } from '../../types'; + +export type ProductEntityTableOptions = Partial< + Pick +>; + +type RowData = { + id: string; + label: string; + previous: number; + current: number; + ratio: number; +}; + +function createRenderer(col: keyof RowData, classes: Record) { + return function render(rowData: {}): JSX.Element { + const row = rowData as RowData; + const rowStyles = classnames(classes.row, { + [classes.rowTotal]: row.id === 'total', + [classes.colFirst]: col === 'label', + [classes.colLast]: col === 'ratio', + }); + + switch (col) { + case 'previous': + case 'current': + return ( + + {costFormatter.format(row[col])} + + ); + case 'ratio': + return ( + formatPercent(Math.abs(amount))} + /> + ); + default: + return {row.label}; + } + }; +} + +// material-table does not support fixed rows. Override the sorting algorithm +// to force Total row to bottom by default or when a user sort toggles a column. +function createSorter(field?: keyof Omit) { + return function rowSort(data1: {}, data2: {}): number { + const a = data1 as RowData; + const b = data2 as RowData; + if (a.id === 'total') return 1; + if (b.id === 'total') return 1; + if (field === 'label') return a.label.localeCompare(b.label); + + return field + ? a[field] - b[field] + : b.previous + b.current - (a.previous - a.current); + }; +} + +type ProductEntityTableProps = { + entityLabel: string; + entity: Entity; + options: ProductEntityTableOptions; +}; + +export const ProductEntityTable = ({ + entityLabel, + entity, + options, +}: ProductEntityTableProps) => { + const classes = useStyles(); + const entities = entity.entities[entityLabel]; + + const data = Object.assign( + { + previousName: 'Previous', + currentName: 'Current', + }, + options, + ); + + const firstColClasses = classnames(classes.column, classes.colFirst); + const lastColClasses = classnames(classes.column, classes.colLast); + + const columns: TableColumn[] = [ + { + field: 'label', + title: {entityLabel}, + render: createRenderer('label', classes), + customSort: createSorter('label'), + width: '33.33%', + }, + { + field: 'previous', + title: ( + {data.previousName} + ), + align: 'right', + render: createRenderer('previous', classes), + customSort: createSorter('previous'), + }, + { + field: 'current', + title: ( + {data.currentName} + ), + align: 'right', + render: createRenderer('current', classes), + customSort: createSorter('current'), + }, + { + field: 'ratio', + title: M/M, + align: 'right', + render: createRenderer('ratio', classes), + customSort: createSorter('ratio'), + }, + ]; + + const rowData: RowData[] = entities + .map(e => ({ + id: e.id || 'Unknown', + label: e.id || 'Unknown', + previous: e.aggregation[0], + current: e.aggregation[1], + ratio: e.change.ratio, + })) + .concat({ + id: 'total', + label: 'Total', + previous: entity.aggregation[0], + current: entity.aggregation[1], + ratio: entity.change.ratio, + }) + .sort(createSorter()); + + return ( +
+ ); +}; diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx index 385ff82f9b..5600ed832f 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx @@ -42,7 +42,7 @@ const costInsightsApi = (entity: Entity): Partial => ({ const mockProductCost = createMockEntity(() => ({ id: 'test-id', - entities: [], + entities: {}, aggregation: [3000, 4000], change: { ratio: 0.23, @@ -91,21 +91,21 @@ describe('', () => { it('Should render the right subheader for products with cost data', async () => { const entity = { ...mockProductCost, - entities: [...Array(1000)].map(createMockEntity), + entities: { entity: [...Array(1000)].map(createMockEntity) }, }; const rendered = await renderProductInsightsCardInTestApp( entity, MockComputeEngine, ); - const subheader = 'entities, sorted by cost'; - const subheaderRgx = new RegExp(`${entity.entities.length} ${subheader}`); - expect(rendered.getByText(subheaderRgx)).toBeInTheDocument(); + expect( + rendered.getByText(/1000 entities, sorted by cost/), + ).toBeInTheDocument(); }); it('Should render the right subheader if there is no cost data or change data', async () => { const entity: Entity = { id: 'test-id', - entities: [], + entities: {}, aggregation: [0, 0], change: { ratio: 0, amount: 0 }, }; @@ -135,7 +135,7 @@ describe('', () => { it(`Should display the correct relative time for ${duration}`, async () => { const entity = { ...mockProductCost, - entities: [...Array(3)].map(createMockEntity), + entities: { entity: [...Array(3)].map(createMockEntity) }, }; const rendered = await renderProductInsightsCardInTestApp( entity, diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx index e633d6653d..e4cb475ab5 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx @@ -21,6 +21,7 @@ import React, { useRef, useState, } from 'react'; +import pluralize from 'pluralize'; import { InfoCard } from '@backstage/core'; import { Typography } from '@material-ui/core'; import { default as Alert } from '@material-ui/lab/Alert'; @@ -30,12 +31,12 @@ import { useProductInsightsCardStyles as useStyles } from '../../utils/styles'; import { DefaultLoadingAction } from '../../utils/loading'; import { Duration, Entity, Maybe, Product } from '../../types'; import { - useLastCompleteBillingDate, - useScroll, - useLoading, MapLoadingToProps, + useLastCompleteBillingDate, + useLoading, + useScroll, } from '../../hooks'; -import { pluralOf } from '../../utils/grammar'; +import { findFirstKey } from '../../utils/assert'; type LoadingProps = (isLoading: boolean) => void; @@ -91,13 +92,12 @@ export const ProductInsightsCard = ({ } }, [product, duration, onSelectAsync, dispatchLoadingProduct]); - const entities = entity?.entities ?? []; - const subheader = entities.length - ? `${entities.length} ${pluralOf( - entities.length, - 'entity', - 'entities', - )}, sorted by cost` + // Only a single entities Record for the root product entity is supported + const entityKey = findFirstKey(entity?.entities); + const entities = entityKey ? entity!.entities[entityKey] : []; + + const subheader = entityKey + ? `${pluralize(entityKey, entities.length, true)}, sorted by cost` : null; const headerProps = { classes: classes, diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx index 2c26f62574..d7c796adf3 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx @@ -20,6 +20,7 @@ import { TooltipProps as RechartsTooltipProps, RechartsFunction, } from 'recharts'; +import pluralize from 'pluralize'; import { Box, Typography } from '@material-ui/core'; import { default as FullScreenIcon } from '@material-ui/icons/Fullscreen'; import { LegendItem } from '../LegendItem'; @@ -32,8 +33,13 @@ import { BarChartTooltipItem, BarChartLegendOptions, } from '../BarChart'; -import { pluralOf } from '../../utils/grammar'; -import { findAlways, notEmpty, isUndefined } from '../../utils/assert'; +import { + findAlways, + notEmpty, + isUndefined, + findFirstKey, + assertAlways, +} from '../../utils/assert'; import { formatPeriod, formatPercent } from '../../utils/formatters'; import { titleOf, @@ -62,19 +68,26 @@ export const ProductInsightsChart = ({ }: ProductInsightsChartProps) => { const classes = useStyles(); const layoutClasses = useLayoutStyles(); + + // Only a single entities Record for the root product entity is supported + const entityLabel = assertAlways(findFirstKey(entity.entities)); + const entities = entity.entities[entityLabel] ?? []; + const [activeLabel, setActive] = useState>(); const [selectLabel, setSelected] = useState>(); const isSelected = useMemo(() => !isUndefined(selectLabel), [selectLabel]); + const isClickable = useMemo(() => { - const breakdownEntities = - entity.entities.find(e => e.id === activeLabel)?.entities ?? []; - return breakdownEntities.length > 0; - }, [entity, activeLabel]); + const breakdowns = Object.keys( + entities.find(e => e.id === activeLabel)?.entities ?? {}, + ); + return breakdowns.length > 0; + }, [entities, activeLabel]); const legendTitle = `Cost ${entity.change.ratio <= 0 ? 'Savings' : 'Growth'}`; const costStart = entity.aggregation[0]; const costEnd = entity.aggregation[1]; - const resources = entity.entities.map(resourceOf); + const resources = entities.map(resourceOf); const options: Partial = { previousName: formatPeriod(duration, billingDate, false), @@ -120,15 +133,14 @@ export const ProductInsightsChart = ({ const title = titleOf(label); const items = payload.map(tooltipItemOf).filter(notEmpty); - const activeEntity = findAlways(entity.entities, e => e.id === id); + const activeEntity = findAlways(entities, e => e.id === id); const ratio = activeEntity.change.ratio; - const breakdownEntities = activeEntity.entities; - const subtitle = `${breakdownEntities.length} ${pluralOf( - breakdownEntities.length, - entity.entitiesLabel || 'SKU', - )}`; + const breakdowns = Object.keys(activeEntity.entities); - if (breakdownEntities.length) { + if (breakdowns.length) { + const subtitle = breakdowns + .map(b => pluralize(b, activeEntity.entities[b].length, true)) + .join(', '); return ( - {isSelected && entity.entities.length && ( + {isSelected && entities.length && ( setSelected(undefined)} - entity={entity.entities.find(e => e.id === selectLabel)} + entity={findAlways(entities, e => e.id === selectLabel)} options={options} - entitiesLabel={entity.entitiesLabel || 'SKU'} /> )} diff --git a/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.tsx b/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.tsx index bcda21e0af..a3df76088c 100644 --- a/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.tsx +++ b/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.tsx @@ -15,10 +15,10 @@ */ import React from 'react'; +import pluralize from 'pluralize'; import { InfoCard } from '@backstage/core'; import { ProjectGrowthAlertChart } from './ProjectGrowthAlertChart'; import { ProjectGrowthData } from '../../types'; -import { pluralOf } from '../../utils/grammar'; type ProjectGrowthAlertProps = { alert: ProjectGrowthData; @@ -26,7 +26,7 @@ type ProjectGrowthAlertProps = { export const ProjectGrowthAlertCard = ({ alert }: ProjectGrowthAlertProps) => { const subheader = ` - ${alert.products.length} ${pluralOf(alert.products.length, 'product')}${ + ${pluralize('product', alert.products.length, true)}${ alert.products.length > 1 ? ', sorted by cost' : '' }`; diff --git a/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx b/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx index 41b4ca90f6..4fcc34c87f 100644 --- a/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx +++ b/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx @@ -72,26 +72,28 @@ export const ProjectGrowthInstructionsPage = () => { ratio: 3, amount: 40_000, }, - entities: [ - { - id: 'service-one', - aggregation: [18_200, 58_500], - entities: [], - change: { ratio: 2.21, amount: 40_300 }, - }, - { - id: 'service-two', - aggregation: [1200, 1300], - entities: [], - change: { ratio: 0.083, amount: 100 }, - }, - { - id: 'service-three', - aggregation: [600, 200], - entities: [], - change: { ratio: -0.666, amount: -400 }, - }, - ], + entities: { + service: [ + { + id: 'service-one', + aggregation: [18_200, 58_500], + entities: {}, + change: { ratio: 2.21, amount: 40_300 }, + }, + { + id: 'service-two', + aggregation: [1200, 1300], + entities: {}, + change: { ratio: 0.083, amount: 100 }, + }, + { + id: 'service-three', + aggregation: [600, 200], + entities: {}, + change: { ratio: -0.666, amount: -400 }, + }, + ], + }, }; return ( diff --git a/plugins/cost-insights/src/components/UnlabeledDataflowAlertCard/UnlabeledDataflowAlertCard.tsx b/plugins/cost-insights/src/components/UnlabeledDataflowAlertCard/UnlabeledDataflowAlertCard.tsx index e56b3b044e..dc020d6ce1 100644 --- a/plugins/cost-insights/src/components/UnlabeledDataflowAlertCard/UnlabeledDataflowAlertCard.tsx +++ b/plugins/cost-insights/src/components/UnlabeledDataflowAlertCard/UnlabeledDataflowAlertCard.tsx @@ -15,11 +15,11 @@ */ import React from 'react'; +import pluralize from 'pluralize'; import { InfoCard } from '@backstage/core'; import { Box } from '@material-ui/core'; import { BarChart, BarChartLegend } from '../BarChart'; import { UnlabeledDataflowData, ResourceData } from '../../types'; -import { pluralOf } from '../../utils/grammar'; import { useBarChartLayoutStyles as useStyles } from '../../utils/styles'; type UnlabeledDataflowAlertProps = { @@ -30,9 +30,9 @@ export const UnlabeledDataflowAlertCard = ({ alert, }: UnlabeledDataflowAlertProps) => { const classes = useStyles(); - const projects = pluralOf(alert.projects.length, 'project'); + const projects = pluralize('project', alert.projects.length, true); const subheader = ` - Showing costs from ${alert.projects.length} ${projects} with unlabeled Dataflow jobs in the last 30 days. + Showing costs from ${projects} with unlabeled Dataflow jobs in the last 30 days. `; const options = { previousName: 'Unlabeled Cost', diff --git a/plugins/cost-insights/src/types/Entity.ts b/plugins/cost-insights/src/types/Entity.ts index 52f6271bb4..e3e1618b4a 100644 --- a/plugins/cost-insights/src/types/Entity.ts +++ b/plugins/cost-insights/src/types/Entity.ts @@ -20,9 +20,8 @@ import { Maybe } from './Maybe'; export interface Entity { id: Maybe; aggregation: [number, number]; - entities: Entity[]; + entities: Record; change: ChangeStatistic; - entitiesLabel?: string; } /* @@ -31,8 +30,15 @@ export interface Entity { An entity could be atomic or composite. An atomic entity is indivisible and cannot be broken into sub-entities. - A composite entity can be broken down recursively into sub-entities - that generate cost **over the same time period**. All costs must sum to the root cost. + A composite entity is divided into sub-entities that account for portions + of the total cost **over the same time period**. The root entity is + expected to only have _one_ Record consisting of the sub-entities to display + in the product panel (keyed by the entity type, such as "service" for + compute entities. + + The root sub-entities may have multiple breakdowns - for example, a + breakdown of an entity cost by SKU vs deployment environment. The sum + aggregated cost of each keyed breakdown should equal the sub-entity's cost. Entities with null ids are considered "unlabeled" - costs without attribution. If an entity is a composite, it may only have one (1) null child but may have any number of @@ -45,44 +51,68 @@ export interface Entity { ratio: 2000, amount: 200 }, - entities: [ - { - id: 'service-a', - aggregation: [0, 100], - change: { - ratio: 100, - amount: 100 - }, - entities: [] - }, - { - id: 'service-b', - aggregation: [0, 100], - change: { - ratio: 100, - amount: 100 - }, - entities: [ - { - id: 'service-b-sku-a', - aggregation: [0, 25], - change: { - ratio: 25, - amount: 25 - }, - entities: [] + entities: { + service: [ + { + id: 'service-a', + aggregation: [0, 100], + change: { + ratio: 100, + amount: 100 }, - { - id: null, // Unlabeled cost for service-b - aggregation: [0, 75], - change: { - ratio: 75, - amount: 75 - }, - entities: [] + entities: {} + }, + { + id: 'service-b', + aggregation: [0, 100], + change: { + ratio: 100, + amount: 100 }, - ] - }, - ] + entities: { + SKU: [ + { + id: 'service-b-sku-a', + aggregation: [0, 25], + change: { + ratio: 25, + amount: 25 + }, + entities: {} + }, + { + id: null, // Unlabeled cost for service-b + aggregation: [0, 75], + change: { + ratio: 75, + amount: 75 + }, + entities: {} + }, + ], + deployment: [ + { + id: 'service-b-env-a', + aggregation: [0, 50], + change: { + ratio: 50, + amount: 50 + }, + entities: {} + }, + { + id: 'service-b-env-b', + aggregation: [0, 50], + change: { + ratio: 50, + amount: 50 + }, + entities: {} + }, + ] + } + }, + ] + } } */ diff --git a/plugins/cost-insights/src/utils/assert.ts b/plugins/cost-insights/src/utils/assert.ts index 19ddabe574..39b710ac3d 100644 --- a/plugins/cost-insights/src/utils/assert.ts +++ b/plugins/cost-insights/src/utils/assert.ts @@ -50,3 +50,9 @@ export function findAlways( ): T { return assertAlways(collection.find(callback)); } + +export function findFirstKey( + record: Record | undefined, +): string | undefined { + return Object.keys(record ?? {}).find(_ => true); +} diff --git a/plugins/cost-insights/src/utils/formatters.ts b/plugins/cost-insights/src/utils/formatters.ts index 9c3b2d643e..01a19c17fa 100644 --- a/plugins/cost-insights/src/utils/formatters.ts +++ b/plugins/cost-insights/src/utils/formatters.ts @@ -15,9 +15,9 @@ */ import moment from 'moment'; +import pluralize from 'pluralize'; import { Duration, DEFAULT_DATE_FORMAT } from '../types'; import { inclusiveEndDateOf, inclusiveStartDateOf } from '../utils/duration'; -import { pluralOf } from '../utils/grammar'; export type Period = { periodStart: string; @@ -75,7 +75,7 @@ export function formatCurrency(amount: number, currency?: string): string { const n = Math.round(amount); const numString = numberFormatter.format(n); - return currency ? `${numString} ${pluralOf(n, currency)}` : numString; + return currency ? `${numString} ${pluralize(currency, n)}` : numString; } export function formatPercent(n: number): string { diff --git a/plugins/cost-insights/src/utils/grammar.ts b/plugins/cost-insights/src/utils/grammar.ts index 653c567d0a..b82520dd53 100644 --- a/plugins/cost-insights/src/utils/grammar.ts +++ b/plugins/cost-insights/src/utils/grammar.ts @@ -22,20 +22,6 @@ const vowels = { u: 'U', }; -export const pluralOf = ( - n: number, - string: string, - plural?: string, -): string => { - if (n !== 1) { - if (plural) { - return plural; - } - return string.concat('s'); - } - return string; -}; - export const indefiniteArticleOf = ( articles: [string, string], word: string, diff --git a/plugins/cost-insights/src/utils/mockData.ts b/plugins/cost-insights/src/utils/mockData.ts index 76bdf5f08f..2961f67cd9 100644 --- a/plugins/cost-insights/src/utils/mockData.ts +++ b/plugins/cost-insights/src/utils/mockData.ts @@ -52,7 +52,7 @@ export const createMockEntity = ( const defaultEntity: Entity = { id: 'test-entity', aggregation: [100, 200], - entities: [], + entities: {}, change: { ratio: 0, amount: 0, @@ -517,35 +517,37 @@ export const SampleBigQueryInsights: Entity = { ratio: 3, amount: 20_000, }, - entities: [ - { - id: 'entity-a', - aggregation: [5_000, 10_000], - change: { - ratio: 1, - amount: 5_000, + entities: { + dataset: [ + { + id: 'entity-a', + aggregation: [5_000, 10_000], + change: { + ratio: 1, + amount: 5_000, + }, + entities: {}, }, - entities: [], - }, - { - id: 'entity-b', - aggregation: [5_000, 10_000], - change: { - ratio: 1, - amount: 5_000, + { + id: 'entity-b', + aggregation: [5_000, 10_000], + change: { + ratio: 1, + amount: 5_000, + }, + entities: {}, }, - entities: [], - }, - { - id: 'entity-c', - aggregation: [0, 10_000], - change: { - ratio: 10_000, - amount: 10_000, + { + id: 'entity-c', + aggregation: [0, 10_000], + change: { + ratio: 10_000, + amount: 10_000, + }, + entities: {}, }, - entities: [], - }, - ], + ], + }, }; export const SampleCloudDataflowInsights: Entity = { @@ -555,110 +557,118 @@ export const SampleCloudDataflowInsights: Entity = { ratio: 0.58, amount: 58_000, }, - entities: [ - { - id: null, - aggregation: [10_000, 12_000], - change: { - ratio: 0.2, - amount: 2_000, + entities: { + pipeline: [ + { + id: null, + aggregation: [10_000, 12_000], + change: { + ratio: 0.2, + amount: 2_000, + }, + entities: { + SKU: [ + { + id: 'Sample SKU A', + aggregation: [3_000, 4_000], + change: { + ratio: 0.333333, + amount: 1_000, + }, + entities: {}, + }, + { + id: 'Sample SKU B', + aggregation: [7_000, 8_000], + change: { + ratio: 0.14285714, + amount: 1_000, + }, + entities: {}, + }, + ], + }, }, - entities: [ - { - id: 'Sample SKU A', - aggregation: [3_000, 4_000], - change: { - ratio: 0.333333, - amount: 1_000, - }, - entities: [], + { + id: 'entity-a', + aggregation: [60_000, 70_000], + change: { + ratio: 0.16666666666666666, + amount: 10_000, }, - { - id: 'Sample SKU B', - aggregation: [7_000, 8_000], - change: { - ratio: 0.14285714, - amount: 1_000, - }, - entities: [], + entities: { + SKU: [ + { + id: 'Sample SKU A', + aggregation: [20_000, 15_000], + change: { + ratio: -0.25, + amount: -5_000, + }, + entities: {}, + }, + { + id: 'Sample SKU B', + aggregation: [30_000, 35_000], + change: { + ratio: -0.16666666666666666, + amount: -5_000, + }, + entities: {}, + }, + { + id: 'Sample SKU C', + aggregation: [10_000, 20_000], + change: { + ratio: 1, + amount: 10_000, + }, + entities: {}, + }, + ], }, - ], - }, - { - id: 'entity-a', - aggregation: [60_000, 70_000], - change: { - ratio: 0.16666666666666666, - amount: 10_000, }, - entities: [ - { - id: 'Sample SKU A', - aggregation: [20_000, 15_000], - change: { - ratio: -0.25, - amount: -5_000, - }, - entities: [], + { + id: 'entity-b', + aggregation: [12_000, 8_000], + change: { + ratio: -0.33333, + amount: -4_000, }, - { - id: 'Sample SKU B', - aggregation: [30_000, 35_000], - change: { - ratio: -0.16666666666666666, - amount: -5_000, - }, - entities: [], + entities: { + SKU: [ + { + id: 'Sample SKU A', + aggregation: [4_000, 4_000], + change: { + ratio: 0, + amount: 0, + }, + entities: {}, + }, + { + id: 'Sample SKU B', + aggregation: [8_000, 4_000], + change: { + ratio: -0.5, + amount: -4_000, + }, + entities: {}, + }, + ], }, - { - id: 'Sample SKU C', - aggregation: [10_000, 20_000], - change: { - ratio: 1, - amount: 10_000, - }, - entities: [], - }, - ], - }, - { - id: 'entity-b', - aggregation: [12_000, 8_000], - change: { - ratio: -0.33333, - amount: -4_000, }, - entities: [ - { - id: 'Sample SKU A', - aggregation: [4_000, 4_000], - change: { - ratio: 0, - amount: 0, - }, - entities: [], + { + id: 'entity-c', + aggregation: [0, 10_000], + change: { + ratio: 10_000, + amount: 10_000, }, - { - id: 'Sample SKU B', - aggregation: [8_000, 4_000], - change: { - ratio: -0.5, - amount: -4_000, - }, - entities: [], - }, - ], - }, - { - id: 'entity-c', - aggregation: [0, 10_000], - change: { - ratio: 10_000, - amount: 10_000, + entities: {}, }, - entities: [], - }, - ], + ], + }, }; export const SampleCloudStorageInsights: Entity = { @@ -668,91 +678,97 @@ export const SampleCloudStorageInsights: Entity = { ratio: 0, amount: 0, }, - entities: [ - { - id: 'entity-a', - aggregation: [15_000, 20_000], - change: { - ratio: 0.333, - amount: 5_000, + entities: { + bucket: [ + { + id: 'entity-a', + aggregation: [15_000, 20_000], + change: { + ratio: 0.333, + amount: 5_000, + }, + entities: { + SKU: [ + { + id: 'Sample SKU A', + aggregation: [10_000, 11_000], + change: { + ratio: 0.1, + amount: 1_000, + }, + entities: {}, + }, + { + id: 'Sample SKU B', + aggregation: [2_000, 5_000], + change: { + ratio: 1.5, + amount: 3_000, + }, + entities: {}, + }, + { + id: 'Sample SKU C', + aggregation: [3_000, 4_000], + change: { + ratio: 0.3333, + amount: 1_000, + }, + entities: {}, + }, + ], + }, }, - entities: [ - { - id: 'Sample SKU A', - aggregation: [10_000, 11_000], - change: { - ratio: 0.1, - amount: 1_000, - }, - entities: [], + { + id: 'entity-b', + aggregation: [30_000, 25_000], + change: { + ratio: -0.16666, + amount: -5_000, }, - { - id: 'Sample SKU B', - aggregation: [2_000, 5_000], - change: { - ratio: 1.5, - amount: 3_000, - }, - entities: [], + entities: { + SKU: [ + { + id: 'Sample SKU A', + aggregation: [12_000, 13_000], + change: { + ratio: 0.08333333333333333, + amount: 1_000, + }, + entities: {}, + }, + { + id: 'Sample SKU B', + aggregation: [16_000, 12_000], + change: { + ratio: -0.25, + amount: -4_000, + }, + entities: {}, + }, + { + id: 'Sample SKU C', + aggregation: [2_000, 0], + change: { + ratio: -1, + amount: -2000, + }, + entities: {}, + }, + ], }, - { - id: 'Sample SKU C', - aggregation: [3_000, 4_000], - change: { - ratio: 0.3333, - amount: 1_000, - }, - entities: [], - }, - ], - }, - { - id: 'entity-b', - aggregation: [30_000, 25_000], - change: { - ratio: -0.16666, - amount: -5_000, }, - entities: [ - { - id: 'Sample SKU A', - aggregation: [12_000, 13_000], - change: { - ratio: 0.08333333333333333, - amount: 1_000, - }, - entities: [], + { + id: 'entity-c', + aggregation: [0, 0], + change: { + ratio: 0, + amount: 0, }, - { - id: 'Sample SKU B', - aggregation: [16_000, 12_000], - change: { - ratio: -0.25, - amount: -4_000, - }, - entities: [], - }, - { - id: 'Sample SKU C', - aggregation: [2_000, 0], - change: { - ratio: -1, - amount: -2000, - }, - entities: [], - }, - ], - }, - { - id: 'entity-c', - aggregation: [0, 0], - change: { - ratio: 0, - amount: 0, + entities: {}, }, - entities: [], - }, - ], + ], + }, }; export const SampleComputeEngineInsights: Entity = { @@ -762,91 +778,137 @@ export const SampleComputeEngineInsights: Entity = { ratio: 0.125, amount: 10_000, }, - entities: [ - { - id: 'entity-a', - aggregation: [20_000, 10_000], - change: { - ratio: -0.5, - amount: -10_000, + entities: { + service: [ + { + id: 'entity-a', + aggregation: [20_000, 10_000], + change: { + ratio: -0.5, + amount: -10_000, + }, + entities: { + SKU: [ + { + id: 'Sample SKU A', + aggregation: [4_000, 2_000], + change: { + ratio: -0.5, + amount: -2_000, + }, + entities: {}, + }, + { + id: 'Sample SKU B', + aggregation: [7_000, 6_000], + change: { + ratio: -0.14285714285714285, + amount: -1_000, + }, + entities: {}, + }, + { + id: 'Sample SKU C', + aggregation: [9_000, 2_000], + change: { + ratio: -0.7777777777777778, + amount: -7000, + }, + entities: {}, + }, + ], + deployment: [ + { + id: 'Compute Engine', + aggregation: [7_000, 6_000], + change: { + ratio: -0.5, + amount: -2_000, + }, + entities: {}, + }, + { + id: 'Kubernetes', + aggregation: [4_000, 2_000], + change: { + ratio: -0.14285714285714285, + amount: -1_000, + }, + entities: {}, + }, + ], + }, }, - entities: [ - { - id: 'Sample SKU A', - aggregation: [4_000, 2_000], - change: { - ratio: -0.5, - amount: -2_000, - }, - entities: [], + { + id: 'entity-b', + aggregation: [10_000, 20_000], + change: { + ratio: 1, + amount: 10_000, }, - { - id: 'Sample SKU B', - aggregation: [7_000, 6_000], - change: { - ratio: -0.14285714285714285, - amount: -1_000, - }, - entities: [], + entities: { + SKU: [ + { + id: 'Sample SKU A', + aggregation: [1_000, 2_000], + change: { + ratio: 1, + amount: 1_000, + }, + entities: {}, + }, + { + id: 'Sample SKU B', + aggregation: [4_000, 8_000], + change: { + ratio: 1, + amount: 4_000, + }, + entities: {}, + }, + { + id: 'Sample SKU C', + aggregation: [5_000, 10_000], + change: { + ratio: 1, + amount: 5_000, + }, + entities: {}, + }, + ], + deployment: [ + { + id: 'Compute Engine', + aggregation: [7_000, 6_000], + change: { + ratio: -0.5, + amount: -2_000, + }, + entities: {}, + }, + { + id: 'Kubernetes', + aggregation: [4_000, 2_000], + change: { + ratio: -0.14285714285714285, + amount: -1_000, + }, + entities: {}, + }, + ], }, - { - id: 'Sample SKU C', - aggregation: [9_000, 2_000], - change: { - ratio: -0.7777777777777778, - amount: -7000, - }, - entities: [], - }, - ], - }, - { - id: 'entity-b', - aggregation: [10_000, 20_000], - change: { - ratio: 1, - amount: 10_000, }, - entities: [ - { - id: 'Sample SKU A', - aggregation: [1_000, 2_000], - change: { - ratio: 1, - amount: 1_000, - }, - entities: [], + { + id: 'entity-c', + aggregation: [0, 10_000], + change: { + ratio: 10_000, + amount: 10_000, }, - { - id: 'Sample SKU B', - aggregation: [4_000, 8_000], - change: { - ratio: 1, - amount: 4_000, - }, - entities: [], - }, - { - id: 'Sample SKU C', - aggregation: [5_000, 10_000], - change: { - ratio: 1, - amount: 5_000, - }, - entities: [], - }, - ], - }, - { - id: 'entity-c', - aggregation: [0, 10_000], - change: { - ratio: 10_000, - amount: 10_000, + entities: {}, }, - entities: [], - }, - ], + ], + }, }; export const SampleEventsInsights: Entity = { @@ -856,83 +918,88 @@ export const SampleEventsInsights: Entity = { ratio: -0.5, amount: -10_000, }, - entitiesLabel: 'Product', - entities: [ - { - id: 'entity-a', - aggregation: [15_000, 7_000], - change: { - ratio: -0.53333333333, - amount: -8_000, + entities: { + event: [ + { + id: 'entity-a', + aggregation: [15_000, 7_000], + change: { + ratio: -0.53333333333, + amount: -8_000, + }, + entities: { + product: [ + { + id: 'Sample Product A', + aggregation: [5_000, 2_000], + change: { + ratio: -0.6, + amount: -3_000, + }, + entities: {}, + }, + { + id: 'Sample Product B', + aggregation: [7_000, 2_500], + change: { + ratio: -0.64285714285, + amount: -4_500, + }, + entities: {}, + }, + { + id: 'Sample Product C', + aggregation: [3_000, 2_500], + change: { + ratio: -0.16666666666, + amount: -500, + }, + entities: {}, + }, + ], + }, }, - entities: [ - { - id: 'Sample Product A', - aggregation: [5_000, 2_000], - change: { - ratio: -0.6, - amount: -3_000, - }, - entities: [], + { + id: 'entity-b', + aggregation: [5_000, 3_000], + change: { + ratio: -0.4, + amount: -2_000, }, - { - id: 'Sample Product B', - aggregation: [7_000, 2_500], - change: { - ratio: -0.64285714285, - amount: -4_500, - }, - entities: [], + entities: { + product: [ + { + id: 'Sample Product A', + aggregation: [2_000, 1_000], + change: { + ratio: -0.5, + amount: -1_000, + }, + entities: {}, + }, + { + id: 'Sample Product B', + aggregation: [1_000, 1_500], + change: { + ratio: 0.5, + amount: 500, + }, + entities: {}, + }, + { + id: 'Sample Product C', + aggregation: [2_000, 500], + change: { + ratio: -0.75, + amount: -1_500, + }, + entities: {}, + }, + ], }, - { - id: 'Sample Product C', - aggregation: [3_000, 2_500], - change: { - ratio: -0.16666666666, - amount: -500, - }, - entities: [], - }, - ], - }, - { - id: 'entity-b', - aggregation: [5_000, 3_000], - change: { - ratio: -0.4, - amount: -2_000, }, - entities: [ - { - id: 'Sample Product A', - aggregation: [2_000, 1_000], - change: { - ratio: -0.5, - amount: -1_000, - }, - entities: [], - }, - { - id: 'Sample Product B', - aggregation: [1_000, 1_500], - change: { - ratio: 0.5, - amount: 500, - }, - entities: [], - }, - { - id: 'Sample Product C', - aggregation: [2_000, 500], - change: { - ratio: -0.75, - amount: -1_500, - }, - entities: [], - }, - ], - }, - ], + ], + }, }; export function entityOf(product: string): Entity { diff --git a/yarn.lock b/yarn.lock index 44024b5952..f33bf5a34c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5736,6 +5736,11 @@ dependencies: "@types/express" "*" +"@types/pluralize@^0.0.29": + version "0.0.29" + resolved "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.29.tgz#6ffa33ed1fc8813c469b859681d09707eb40d03c" + integrity sha512-BYOID+l2Aco2nBik+iYS4SZX0Lf20KPILP5RGmM1IgzdwNdTs0eebiFriOPcej1sX9mLnSoiNte5zcFxssgpGA== + "@types/prettier@^2.0.0": version "2.0.0" resolved "https://registry.npmjs.org/@types/prettier/-/prettier-2.0.0.tgz#dc85454b953178cc6043df5208b9e949b54a3bc4" @@ -18925,6 +18930,11 @@ please-upgrade-node@^3.2.0: dependencies: semver-compare "^1.0.0" +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + pn@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" From b14cbe28d3093deba54cf8178687c0eafae55331 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Fri, 4 Dec 2020 09:27:32 -0700 Subject: [PATCH 15/86] Fix tests --- .../ProductEntityDialog.test.tsx | 27 +++---------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.test.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.test.tsx index 076edf60a3..99b6c8ae26 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.test.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.test.tsx @@ -77,27 +77,23 @@ describe('', () => { open entity={atomicEntity} onClose={jest.fn()} - selectedLabel={null} - setSelectedLabel={jest.fn()} />, ), ), ).toThrow(); }); - it('Should not show tabs for a single sub-entity type', () => { - const { queryByText } = render( + it('Should show a tab for a single sub-entity type', () => { + const { getByText } = render( wrapInTestApp( , ), ); - expect(queryByText('Breakdown by SKU')).not.toBeInTheDocument(); + expect(getByText('Breakdown by SKU')).toBeInTheDocument(); }); it('Should show tabs when multiple sub-entity types exist', () => { @@ -107,8 +103,6 @@ describe('', () => { open entity={multiBreakdownEntity} onClose={jest.fn()} - selectedLabel={null} - setSelectedLabel={jest.fn()} />, ), ); @@ -116,19 +110,4 @@ describe('', () => { expect(getByText('Breakdown by deployment')).toBeInTheDocument(); expect(getByText('sku-1')).toBeInTheDocument(); }); - - it('Shows the pre-selected tab, if provided', () => { - const { getByText } = render( - wrapInTestApp( - , - ), - ); - expect(getByText('d-1')).toBeInTheDocument(); - }); }); From cadb4b531126bbc05e3dd3bc014109a35092189c Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Fri, 4 Dec 2020 15:24:40 -0700 Subject: [PATCH 16/86] findFirst -> findAny --- .../components/ProductInsightsCard/ProductInsightsCard.tsx | 4 ++-- .../components/ProductInsightsCard/ProductInsightsChart.tsx | 4 ++-- plugins/cost-insights/src/utils/assert.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx index e4cb475ab5..534ef490c3 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx @@ -36,7 +36,7 @@ import { useLoading, useScroll, } from '../../hooks'; -import { findFirstKey } from '../../utils/assert'; +import { findAnyKey } from '../../utils/assert'; type LoadingProps = (isLoading: boolean) => void; @@ -93,7 +93,7 @@ export const ProductInsightsCard = ({ }, [product, duration, onSelectAsync, dispatchLoadingProduct]); // Only a single entities Record for the root product entity is supported - const entityKey = findFirstKey(entity?.entities); + const entityKey = findAnyKey(entity?.entities); const entities = entityKey ? entity!.entities[entityKey] : []; const subheader = entityKey diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx index d7c796adf3..08a4c5efa2 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx @@ -37,7 +37,7 @@ import { findAlways, notEmpty, isUndefined, - findFirstKey, + findAnyKey, assertAlways, } from '../../utils/assert'; import { formatPeriod, formatPercent } from '../../utils/formatters'; @@ -70,7 +70,7 @@ export const ProductInsightsChart = ({ const layoutClasses = useLayoutStyles(); // Only a single entities Record for the root product entity is supported - const entityLabel = assertAlways(findFirstKey(entity.entities)); + const entityLabel = assertAlways(findAnyKey(entity.entities)); const entities = entity.entities[entityLabel] ?? []; const [activeLabel, setActive] = useState>(); diff --git a/plugins/cost-insights/src/utils/assert.ts b/plugins/cost-insights/src/utils/assert.ts index 39b710ac3d..05ce65197b 100644 --- a/plugins/cost-insights/src/utils/assert.ts +++ b/plugins/cost-insights/src/utils/assert.ts @@ -51,7 +51,7 @@ export function findAlways( return assertAlways(collection.find(callback)); } -export function findFirstKey( +export function findAnyKey( record: Record | undefined, ): string | undefined { return Object.keys(record ?? {}).find(_ => true); From a8b4dbcb6ba63f9e114ac7b9d5a939082f0336a3 Mon Sep 17 00:00:00 2001 From: AlleyWei <64219471+AlleyWei@users.noreply.github.com> Date: Fri, 4 Dec 2020 17:28:38 -0500 Subject: [PATCH 17/86] Update Footer.js Please make the <3 an emoji --- microsite/core/Footer.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/microsite/core/Footer.js b/microsite/core/Footer.js index 6ce3c3dd84..585a1c2091 100644 --- a/microsite/core/Footer.js +++ b/microsite/core/Footer.js @@ -1,4 +1,6 @@ /* +* Made with <3 at Spotify + * Copyright 2020 Backstage Project Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); From 4ac602d6e84a728b1fceceba3d34d581e489d2d1 Mon Sep 17 00:00:00 2001 From: AlleyWei <64219471+AlleyWei@users.noreply.github.com> Date: Fri, 4 Dec 2020 17:34:40 -0500 Subject: [PATCH 18/86] Update Footer.js --- microsite/core/Footer.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/microsite/core/Footer.js b/microsite/core/Footer.js index 585a1c2091..ef7fdb7c3a 100644 --- a/microsite/core/Footer.js +++ b/microsite/core/Footer.js @@ -56,6 +56,13 @@ class Footer extends React.Component { Open Source @ {this.props.config.organizationName} + + Spotify Engineering Blog + + + Spotify for Developers + + GitHub Date: Fri, 4 Dec 2020 15:38:36 -0700 Subject: [PATCH 19/86] M/M -> Change --- .../src/components/ProductInsightsCard/ProductEntityTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx index 7c239ba30d..5588154ad0 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx @@ -135,7 +135,7 @@ export const ProductEntityTable = ({ }, { field: 'ratio', - title: M/M, + title: Change, align: 'right', render: createRenderer('ratio', classes), customSort: createSorter('ratio'), From 8cc65fec5f903138b5e3e956dfe0a915379772e1 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Fri, 4 Dec 2020 20:29:11 -0700 Subject: [PATCH 20/86] The embarrassment A developer without A final paren --- plugins/cost-insights/src/types/Entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cost-insights/src/types/Entity.ts b/plugins/cost-insights/src/types/Entity.ts index e3e1618b4a..b49bb596ae 100644 --- a/plugins/cost-insights/src/types/Entity.ts +++ b/plugins/cost-insights/src/types/Entity.ts @@ -34,7 +34,7 @@ export interface Entity { of the total cost **over the same time period**. The root entity is expected to only have _one_ Record consisting of the sub-entities to display in the product panel (keyed by the entity type, such as "service" for - compute entities. + compute entities). The root sub-entities may have multiple breakdowns - for example, a breakdown of an entity cost by SKU vs deployment environment. The sum From 49435447993b0b5f11257b37c11b6f79cabfa10f Mon Sep 17 00:00:00 2001 From: AlleyWei <64219471+AlleyWei@users.noreply.github.com> Date: Sat, 5 Dec 2020 11:55:45 -0500 Subject: [PATCH 21/86] Update Footer.js --- microsite/core/Footer.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/microsite/core/Footer.js b/microsite/core/Footer.js index ef7fdb7c3a..fe4fbfaa80 100644 --- a/microsite/core/Footer.js +++ b/microsite/core/Footer.js @@ -57,11 +57,9 @@ class Footer extends React.Component { Open Source @ {this.props.config.organizationName} - Spotify Engineering Blog - - - Spotify for Developers - + Spotify Engineering Blog + Spotify for Developers + GitHub Date: Sat, 5 Dec 2020 11:58:13 -0500 Subject: [PATCH 22/86] Update Footer.js --- microsite/core/Footer.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/microsite/core/Footer.js b/microsite/core/Footer.js index fe4fbfaa80..4a5bca6071 100644 --- a/microsite/core/Footer.js +++ b/microsite/core/Footer.js @@ -57,8 +57,11 @@ class Footer extends React.Component { Open Source @ {this.props.config.organizationName} - Spotify Engineering Blog - Spotify for Developers + + Spotify Engineering Blog + + + Spotify for Developers GitHub From d298d506390e84c93051f293acd994c6b620bcf1 Mon Sep 17 00:00:00 2001 From: David Tuite Date: Sat, 5 Dec 2020 21:49:03 +0000 Subject: [PATCH 23/86] Add pagerduty config to helm chart --- contrib/chart/backstage/templates/backend-secret.yaml | 1 + contrib/chart/backstage/values.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/contrib/chart/backstage/templates/backend-secret.yaml b/contrib/chart/backstage/templates/backend-secret.yaml index b340f39d7c..299d893ec4 100644 --- a/contrib/chart/backstage/templates/backend-secret.yaml +++ b/contrib/chart/backstage/templates/backend-secret.yaml @@ -20,4 +20,5 @@ stringData: AZURE_TOKEN: {{ .Values.auth.azure.api.token }} NEW_RELIC_REST_API_KEY: {{ .Values.auth.newRelicRestApiKey }} TRAVISCI_AUTH_TOKEN: {{ .Values.auth.travisciAuthToken }} + PAGERDUTY_TOKEN: {{ .Values.auth.pagerdutyToken }} {{- end }} diff --git a/contrib/chart/backstage/values.yaml b/contrib/chart/backstage/values.yaml index a4a0fadcc2..261f352f93 100644 --- a/contrib/chart/backstage/values.yaml +++ b/contrib/chart/backstage/values.yaml @@ -250,3 +250,4 @@ auth: gitlabToken: g newRelicRestApiKey: r travisciAuthToken: fake-travis-ci-auth-token + pagerdutyToken: h From 1e097d11ca80d96c19c2a80ca29cc957983858f6 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Sat, 5 Dec 2020 22:00:03 -0500 Subject: [PATCH 24/86] Align optional NPM fields --- packages/backend/package.json | 11 ++++++++++- packages/catalog-client/package.json | 9 +++++++++ packages/catalog-model/package.json | 9 +++++++++ packages/integration/package.json | 9 +++++++++ plugins/api-docs/package.json | 9 +++++++++ 5 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 5937d6a6f3..a282033169 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -3,11 +3,20 @@ "version": "0.2.5", "main": "dist/index.cjs.js", "types": "src/index.ts", - "private": true, "license": "Apache-2.0", + "private": true, "engines": { "node": "12 || 14" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/backend" + }, + "keywords": [ + "backstage" + ], "scripts": { "build": "backstage-cli backend:build", "build-image": "backstage-cli backend:build-image --build --tag example-backend", diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index a343cae716..97e2c47422 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -11,6 +11,15 @@ "module": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/catalog-client" + }, + "keywords": [ + "backstage" + ], "scripts": { "build": "backstage-cli build", "lint": "backstage-cli lint", diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 25406c8eef..1d06be8054 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -11,6 +11,15 @@ "module": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/catalog-model" + }, + "keywords": [ + "backstage" + ], "scripts": { "build": "backstage-cli build", "lint": "backstage-cli lint", diff --git a/packages/integration/package.json b/packages/integration/package.json index 342476c38f..80e8118979 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -11,6 +11,15 @@ "module": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/integration" + }, + "keywords": [ + "backstage" + ], "scripts": { "build": "backstage-cli build", "lint": "backstage-cli lint", diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 659ded8061..e6deb39b48 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -9,6 +9,15 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/api-docs" + }, + "keywords": [ + "backstage" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", From cb5fc4b29afedf1adc2e89fba0a258082f32c7be Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 7 Dec 2020 15:20:47 +0100 Subject: [PATCH 25/86] Improve changelog for the api-docs plugin --- .changeset/seven-tips-more.md | 133 ++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 .changeset/seven-tips-more.md diff --git a/.changeset/seven-tips-more.md b/.changeset/seven-tips-more.md new file mode 100644 index 0000000000..fcb51e30aa --- /dev/null +++ b/.changeset/seven-tips-more.md @@ -0,0 +1,133 @@ +--- +'@backstage/create-app': patch +--- + +Adjust template to the latest changes in the `api-docs` plugin. + +## Template Changes + +While updating to the latest `api-docs` plugin, the following changes are necessary for the `create-app` template in your `app/src/components/catalog/EntityPage.tsx`. This adds: + +- A custom entity page for API entities +- Changes the API tab to include the new `ConsumedApisCard` and `ProvidedApisCard` that link to the API entity. + +```diff + import { ++ ApiDefinitionCard, +- Router as ApiDocsRouter, ++ ConsumedApisCard, ++ ProvidedApisCard, ++ ConsumedApisCard, ++ ConsumingComponentsCard, ++ ProvidedApisCard, ++ ProvidingComponentsCard + } from '@backstage/plugin-api-docs'; + +... + ++const ComponentApisContent = ({ entity }: { entity: Entity }) => ( ++ ++ ++ ++ ++ ++ ++ ++ ++); + + const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( + + } + /> + } + /> + } ++ element={} + /> +... + +-export const EntityPage = () => { +- const { entity } = useEntity(); +- switch (entity?.spec?.type) { +- case 'service': +- return ; +- case 'website': +- return ; +- default: +- return ; +- } +-}; + ++export const ComponentEntityPage = ({ entity }: { entity: Entity }) => { ++ switch (entity?.spec?.type) { ++ case 'service': ++ return ; ++ case 'website': ++ return ; ++ default: ++ return ; ++ } ++}; ++ ++const ApiOverviewContent = ({ entity }: { entity: Entity }) => ( ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++); ++ ++const ApiDefinitionContent = ({ entity }: { entity: ApiEntity }) => ( ++ ++ ++ ++ ++ ++); ++ ++const ApiEntityPage = ({ entity }: { entity: Entity }) => ( ++ ++ } ++ /> ++ } ++ /> ++ ++); ++ ++export const EntityPage = () => { ++ const { entity } = useEntity(); ++ ++ switch (entity?.kind?.toLowerCase()) { ++ case 'component': ++ return ; ++ case 'api': ++ return ; ++ default: ++ return ; ++ } ++}; +``` From 221156b2f5c47a3f8e798a54791f32f9871a6774 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 7 Dec 2020 15:21:32 +0100 Subject: [PATCH 26/86] Add custom API entity page to create-app --- .changeset/seven-tips-begin.md | 13 +++- .changeset/seven-tips-more.md | 7 +- .../app/src/components/catalog/EntityPage.tsx | 65 +++++++++++++++++-- 3 files changed, 76 insertions(+), 9 deletions(-) diff --git a/.changeset/seven-tips-begin.md b/.changeset/seven-tips-begin.md index b9ebc4f159..5609315144 100644 --- a/.changeset/seven-tips-begin.md +++ b/.changeset/seven-tips-begin.md @@ -1,6 +1,13 @@ --- -'@backstage/create-app': patch -'@backstage/plugin-api-docs': patch +'@backstage/plugin-api-docs': minor --- -Add tables with consumes and provides relationships to the API and component entity pages. +Stop exposing a custom router from the `api-docs` plugin. Instead, use the +widgets exported by the plugin to compose your custom entity pages. + +Instead of displaying the API definitions directly in the API tab of the +component, it now contains tables linking to the API entities. This also adds +new widgets to display relationships (bot provides & consumes relationships) +between components and APIs. + +See the changelog of `create-app` for a migration guide. diff --git a/.changeset/seven-tips-more.md b/.changeset/seven-tips-more.md index fcb51e30aa..9a6c4855c3 100644 --- a/.changeset/seven-tips-more.md +++ b/.changeset/seven-tips-more.md @@ -6,10 +6,13 @@ Adjust template to the latest changes in the `api-docs` plugin. ## Template Changes -While updating to the latest `api-docs` plugin, the following changes are necessary for the `create-app` template in your `app/src/components/catalog/EntityPage.tsx`. This adds: +While updating to the latest `api-docs` plugin, the following changes are +necessary for the `create-app` template in your +`app/src/components/catalog/EntityPage.tsx`. This adds: - A custom entity page for API entities -- Changes the API tab to include the new `ConsumedApisCard` and `ProvidedApisCard` that link to the API entity. +- Changes the API tab to include the new `ConsumedApisCard` and + `ProvidedApisCard` that link to the API entity. ```diff import { diff --git a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx index 608a4f5f76..b5e384f7a9 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx @@ -13,9 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; +import { ApiEntity, Entity } from '@backstage/catalog-model'; import { WarningPanel } from '@backstage/core'; -import { ConsumedApisCard, ProvidedApisCard } from '@backstage/plugin-api-docs'; +import { + ApiDefinitionCard, + ConsumedApisCard, + ConsumingComponentsCard, + ProvidedApisCard, + ProvidingComponentsCard +} from '@backstage/plugin-api-docs'; import { AboutCard, EntityPageLayout, useEntity @@ -128,8 +134,7 @@ const DefaultEntityPage = ({ entity }: { entity: Entity }) => ( ); -export const EntityPage = () => { - const { entity } = useEntity(); +export const ComponentEntityPage = ({ entity }: { entity: Entity }) => { switch (entity?.spec?.type) { case 'service': return ; @@ -139,3 +144,55 @@ export const EntityPage = () => { return ; } }; + +const ApiOverviewContent = ({ entity }: { entity: Entity }) => ( + + + + + + + + + + + + + +); + +const ApiDefinitionContent = ({ entity }: { entity: ApiEntity }) => ( + + + + + +); + +const ApiEntityPage = ({ entity }: { entity: Entity }) => ( + + } + /> + } + /> + +); + +export const EntityPage = () => { + const { entity } = useEntity(); + + switch (entity?.kind?.toLowerCase()) { + case 'component': + return ; + case 'api': + return ; + default: + return ; + } +}; From 040f453c36418f1ba3aacd193f9465b946c3eae8 Mon Sep 17 00:00:00 2001 From: Mateusz Lewtak <36006588+lewtakm@users.noreply.github.com> Date: Mon, 7 Dec 2020 16:46:15 +0100 Subject: [PATCH 27/86] Feat: Org Plugin (#3448) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: Create Groups plugin * Feat: update code after CR * Feat: change routing, replace filters to use relations, modify EntityPageLayout to support users * Feat: update CLI version * Feat: add some tests * Update daily cost data to return groupedCosts * Add aggregation sum util * Add top panel breakdown view * Add top panel tabs * Add mock data for grouped Costs * Add comments on groupedCosts * Update wording to product in cost by product component * Move cost overview chart legend to separate component * Move mock data utils * Add data viz colors for both themes * Move mock data utils * Update data viz dark theme colors * Update bar chart legend type usage * catalog-backend: gracefully handle missing codeowners * Derive the owned entities in the catalog from group memberships * Filter the response headers in the proxy backend * Make backend-commons tests work on Windows * backend: remove yarn cache from built docker image * Refactor the hooks and also support users owning components * backend,yarnrc: bring yarn network-timeout down to 300s * Fix another Windows test issue This one is a bit tricky. Instead of testing at the root folder of the drive, this test is now relative to the current directory. This resolves the difference between "/pkgs/a" and "d:\pkgs\a" * Drop fill opacity for lighter colors * Update dark theme colors * Fix review comments * techdocs-backend: update Gitlab clone auth * Use case-insensitive filters * Add Kubernetes plugin (#3505) * backend-common: allow port in config to be both a number or a string * chore: set the port as number if it comes through as a string * Feat: Bump GitHub Insights plugin version (#3509) * Feat: groups and components card * Feat: update Org plugin according to CR * Feat: update TS stuff * Feat: change tile titles * Feat: bump packages * Code review fixes Co-authored-by: Brenda Sukh Co-authored-by: Fredrik Adelöw Co-authored-by: Dominik Henneke Co-authored-by: Oliver Sand Co-authored-by: Patrik Oldsberg Co-authored-by: Chongyang Adrian, Ke Co-authored-by: Adam Harvey Co-authored-by: blam Co-authored-by: Marek Calus --- packages/app/package.json | 1 + .../app/src/components/catalog/EntityPage.tsx | 62 +++++- packages/app/src/plugins.ts | 1 + .../EntityPageLayout/EntityPageLayout.tsx | 9 +- plugins/org/.eslintrc.js | 3 + plugins/org/README.md | 6 + plugins/org/dev/index.tsx | 19 ++ plugins/org/package.json | 50 +++++ plugins/org/src/components/Avatar/Avatar.tsx | 73 +++++++ plugins/org/src/components/Avatar/index.ts | 16 ++ .../Group/GroupProfile/GroupProfileCard.tsx | 141 +++++++++++++ .../Cards/Group/GroupProfile/index.ts | 16 ++ .../MembersList/MembersListCard.test.tsx | 100 +++++++++ .../Group/MembersList/MembersListCard.tsx | 155 ++++++++++++++ .../Cards/Group/MembersList/index.ts | 16 ++ .../org/src/components/Cards/Group/index.ts | 17 ++ .../Cards/OwnershipCard/OwnershipCard.tsx | 196 ++++++++++++++++++ .../components/Cards/OwnershipCard/index.ts | 16 ++ .../UserProfileCard/UserProfileCard.test.tsx | 64 ++++++ .../User/UserProfileCard/UserProfileCard.tsx | 134 ++++++++++++ .../Cards/User/UserProfileCard/index.ts | 16 ++ .../org/src/components/Cards/User/index.ts | 16 ++ plugins/org/src/components/Cards/index.ts | 18 ++ plugins/org/src/components/index.ts | 17 ++ plugins/org/src/index.ts | 17 ++ plugins/org/src/plugin.test.ts | 22 ++ plugins/org/src/plugin.ts | 20 ++ plugins/org/src/setupTests.ts | 17 ++ 28 files changed, 1233 insertions(+), 5 deletions(-) create mode 100644 plugins/org/.eslintrc.js create mode 100644 plugins/org/README.md create mode 100644 plugins/org/dev/index.tsx create mode 100644 plugins/org/package.json create mode 100644 plugins/org/src/components/Avatar/Avatar.tsx create mode 100644 plugins/org/src/components/Avatar/index.ts create mode 100644 plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx create mode 100644 plugins/org/src/components/Cards/Group/GroupProfile/index.ts create mode 100644 plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx create mode 100644 plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx create mode 100644 plugins/org/src/components/Cards/Group/MembersList/index.ts create mode 100644 plugins/org/src/components/Cards/Group/index.ts create mode 100644 plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx create mode 100644 plugins/org/src/components/Cards/OwnershipCard/index.ts create mode 100644 plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx create mode 100644 plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx create mode 100644 plugins/org/src/components/Cards/User/UserProfileCard/index.ts create mode 100644 plugins/org/src/components/Cards/User/index.ts create mode 100644 plugins/org/src/components/Cards/index.ts create mode 100644 plugins/org/src/components/index.ts create mode 100644 plugins/org/src/index.ts create mode 100644 plugins/org/src/plugin.test.ts create mode 100644 plugins/org/src/plugin.ts create mode 100644 plugins/org/src/setupTests.ts diff --git a/packages/app/package.json b/packages/app/package.json index e671e23b79..d46ce8b439 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -18,6 +18,7 @@ "@backstage/plugin-github-actions": "^0.2.3", "@backstage/plugin-gitops-profiles": "^0.2.1", "@backstage/plugin-graphiql": "^0.2.1", + "@backstage/plugin-org": "^0.3.0", "@backstage/plugin-jenkins": "^0.3.2", "@backstage/plugin-kubernetes": "^0.3.1", "@backstage/plugin-lighthouse": "^0.2.4", diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 13cbe324c6..2378afbc5e 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -13,7 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ApiEntity, Entity } from '@backstage/catalog-model'; +import { + ApiEntity, + Entity, + GroupEntity, + UserEntity, +} from '@backstage/catalog-model'; import { EmptyState } from '@backstage/core'; import { ApiDefinitionCard, @@ -48,6 +53,12 @@ import { isPluginApplicableToEntity as isLighthouseAvailable, LastLighthouseAuditCard, } from '@backstage/plugin-lighthouse'; +import { + OwnershipCard, + MembersListCard, + GroupProfileCard, + UserProfileCard, +} from '@backstage/plugin-org'; import { Router as SentryRouter } from '@backstage/plugin-sentry'; import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs'; import { Button, Grid } from '@material-ui/core'; @@ -323,6 +334,51 @@ const ApiEntityPage = ({ entity }: { entity: Entity }) => ( ); +const UserOverviewContent = ({ entity }: { entity: UserEntity }) => ( + + + + + + + + +); + +const UserEntityPage = ({ entity }: { entity: Entity }) => ( + + } + /> + +); + +const GroupOverviewContent = ({ entity }: { entity: GroupEntity }) => ( + + + + + + + + + + + +); + +const GroupEntityPage = ({ entity }: { entity: Entity }) => ( + + } + /> + +); + export const EntityPage = () => { const { entity } = useEntity(); @@ -331,6 +387,10 @@ export const EntityPage = () => { return ; case 'api': return ; + case 'group': + return ; + case 'user': + return ; default: return ; } diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index c6a2a0e7e0..6a4924cb0e 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -42,3 +42,4 @@ export { plugin as UserSettings } from '@backstage/plugin-user-settings'; export { plugin as PagerDuty } from '@backstage/plugin-pagerduty'; export { plugin as Buildkite } from '@roadiehq/backstage-plugin-buildkite'; export { plugin as Search } from '@backstage/plugin-search'; +export { plugin as Org } from '@backstage/plugin-org'; diff --git a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx index 8c48d69413..eb75593ffc 100644 --- a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx +++ b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx @@ -39,12 +39,12 @@ const EntityPageTitle = ({ ); -function headerProps( +const headerProps = ( kind: string, namespace: string | undefined, name: string, entity: Entity | undefined, -): { headerTitle: string; headerType: string } { +): { headerTitle: string; headerType: string } => { return { headerTitle: `${name}${ namespace && namespace !== ENTITY_DEFAULT_NAMESPACE @@ -60,7 +60,7 @@ function headerProps( return t; })(), }; -} +}; export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => { const { kind, namespace, name } = useEntityCompoundName(); @@ -88,7 +88,8 @@ export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => { pageTitleOverride={headerTitle} type={headerType} > - {entity && ( + {/* TODO: fix after catalog page customization is added */} + {entity && kind !== 'user' && ( <> + createStyles({ + avatar: { + width: '4rem', + height: '4rem', + color: '#fff', + fontWeight: theme.typography.fontWeightBold, + letterSpacing: '1px', + textTransform: 'uppercase', + }, + }), +); + +const stringToColour = (str: string) => { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + let colour = '#'; + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 0xff; + colour += `00${value.toString(16)}`.substr(-2); + } + return colour; +}; + +export const Avatar = ({ + displayName, + picture, + customStyles, +}: { + displayName: string | undefined; + picture: string | undefined; + customStyles?: CSSProperties; +}) => { + const classes = useStyles(); + return ( + + {displayName && displayName.match(/\b\w/g)!.join('').substring(0, 2)} + + ); +}; diff --git a/plugins/org/src/components/Avatar/index.ts b/plugins/org/src/components/Avatar/index.ts new file mode 100644 index 0000000000..962414634e --- /dev/null +++ b/plugins/org/src/components/Avatar/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 { Avatar } from './Avatar'; diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx new file mode 100644 index 0000000000..ce8829439c --- /dev/null +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -0,0 +1,141 @@ +/* + * 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. + */ + +/* + * 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 { Box, Grid, Link, Tooltip, Typography } from '@material-ui/core'; +import Alert from '@material-ui/lab/Alert'; +import { InfoCard } from '@backstage/core'; +import { entityRouteParams } from '@backstage/plugin-catalog'; +import { + Entity, + GroupEntity, + RELATION_CHILD_OF, + RELATION_PARENT_OF, +} from '@backstage/catalog-model'; +import AccountTreeIcon from '@material-ui/icons/AccountTree'; +import GroupIcon from '@material-ui/icons/Group'; +import { Link as RouterLink, generatePath } from 'react-router-dom'; + +const GroupLink = ({ + groupName, + index = 0, + entity, +}: { + groupName: string; + index?: number; + entity: Entity; +}) => ( + <> + {index >= 1 ? ', ' : ''} + + [{groupName}] + + +); + +const CardTitle = ({ title }: { title: string }) => ( + + + {title} + +); + +export const GroupProfileCard = ({ + entity: group, + variant, +}: { + entity: GroupEntity; + variant: string; +}) => { + const { + metadata: { name, description }, + } = group; + const parent = group?.relations + ?.filter(r => r.type === RELATION_CHILD_OF) + ?.map(group => group.target.name) + .toString(); + + const childrens = group?.relations + ?.filter(r => r.type === RELATION_PARENT_OF) + ?.map(group => group.target.name); + + if (!group) return User not found; + + return ( + } + subheader={description} + variant={variant} + > + + + {parent ? ( + + + + + + + + + + + ) : null} + {childrens?.length ? ( + + + + + + + {childrens.map((children, index) => ( + + ))} + + + + ) : null} + + + + ); +}; diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/index.ts b/plugins/org/src/components/Cards/Group/GroupProfile/index.ts new file mode 100644 index 0000000000..44efe25a50 --- /dev/null +++ b/plugins/org/src/components/Cards/Group/GroupProfile/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 * from './GroupProfileCard'; diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx new file mode 100644 index 0000000000..0c188afc57 --- /dev/null +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.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 { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog'; +import { Entity } from '@backstage/catalog-model'; +import { MembersListCard } from './MembersListCard'; + +describe('MemberTab Test', () => { + const groupEntity = { + apiVersion: 'v1', + kind: 'Group', + metadata: { + name: 'team-d', + description: 'The evil-corp organization', + namespace: 'default', + }, + spec: { + type: 'team', + parent: 'boxoffice', + ancestors: ['boxoffice', 'acme-corp'], + children: [], + descendants: [], + }, + }; + + const catalogApi: Partial = { + getEntities: () => + Promise.resolve({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'tara.macgovern', + namespace: 'default', + uid: 'a5gerth56', + }, + relations: [ + { + type: 'memberOf', + target: { + kind: 'group', + name: 'team-d', + namespace: 'default', + }, + }, + ], + spec: { + profile: { + displayName: 'Tara MacGovern', + email: 'tara-macgovern@example.com', + picture: 'https://example.com/staff/tara.jpeg', + }, + memberOf: ['team-d'], + }, + }, + ] as Entity[], + }), + }; + + const apis = ApiRegistry.from([[catalogApiRef, catalogApi]]); + + it('Display Profile Card', async () => { + const rendered = await renderWithEffects( + wrapInTestApp( + + + , + ), + ); + + expect(rendered.getByAltText('Tara MacGovern')).toHaveAttribute( + 'src', + 'https://example.com/staff/tara.jpeg', + ); + expect( + rendered.getByText('tara-macgovern@example.com'), + ).toBeInTheDocument(); + expect(rendered.getByText('Tara MacGovern')).toHaveAttribute( + 'href', + '/catalog/default/user/tara.macgovern', + ); + }); +}); diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx new file mode 100644 index 0000000000..cd39241ca8 --- /dev/null +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx @@ -0,0 +1,155 @@ +/* + * 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 Alert from '@material-ui/lab/Alert'; +import { + Box, + createStyles, + Grid, + Link, + makeStyles, + Theme, + Typography, +} from '@material-ui/core'; +import { InfoCard, Progress, useApi } from '@backstage/core'; +import { + UserEntity, + RELATION_MEMBER_OF, + Entity, +} from '@backstage/catalog-model'; +import { Link as RouterLink, generatePath } from 'react-router-dom'; +import { catalogApiRef, entityRouteParams } from '@backstage/plugin-catalog'; +import { useAsync } from 'react-use'; +import { Avatar } from '../../../Avatar'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + card: { + border: `1px solid ${theme.palette.divider}`, + boxShadow: theme.shadows[2], + borderRadius: '4px', + overflow: 'visible', + position: 'relative', + margin: theme.spacing(3, 0, 0), + }, + }), +); + +const MemberComponent = ({ + member, + groupEntity, +}: { + member: UserEntity; + groupEntity: Entity; +}) => { + const classes = useStyles(); + const { name: metaName } = member.metadata; + const { profile } = member.spec; + return ( + + + + + + + + {profile?.displayName} + + + {profile?.email} + + + + + ); +}; + +export const MembersListCard = ({ + entity: groupEntity, +}: { + entity: Entity; +}) => { + const { + metadata: { name: groupName }, + } = groupEntity; + const catalogApi = useApi(catalogApiRef); + + const { loading, error, value: members } = useAsync(async () => { + const membersList = await catalogApi.getEntities({ + filter: { + kind: 'User', + }, + }); + const groupMembersList = ((membersList.items as unknown) as Array< + UserEntity + >).filter(member => + member?.relations?.some( + r => r.type === RELATION_MEMBER_OF && r.target.name === groupName, + ), + ); + return groupMembersList; + }, [catalogApi]); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + return ( + + + + {members && members.length ? ( + members.map(member => ( + + )) + ) : ( + + This group has no members. + + )} + + + + ); +}; diff --git a/plugins/org/src/components/Cards/Group/MembersList/index.ts b/plugins/org/src/components/Cards/Group/MembersList/index.ts new file mode 100644 index 0000000000..c3f4ea9178 --- /dev/null +++ b/plugins/org/src/components/Cards/Group/MembersList/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 * from './MembersListCard'; diff --git a/plugins/org/src/components/Cards/Group/index.ts b/plugins/org/src/components/Cards/Group/index.ts new file mode 100644 index 0000000000..a011891f62 --- /dev/null +++ b/plugins/org/src/components/Cards/Group/index.ts @@ -0,0 +1,17 @@ +/* + * 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 * from './MembersList'; +export * from './GroupProfile'; diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx new file mode 100644 index 0000000000..7c4ad7a56a --- /dev/null +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx @@ -0,0 +1,196 @@ +/* + * 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 { InfoCard, useApi, Progress } from '@backstage/core'; +import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { catalogApiRef } from '@backstage/plugin-catalog'; +import { useAsync } from 'react-use'; +import Alert from '@material-ui/lab/Alert'; +import { + Box, + createStyles, + Grid, + makeStyles, + Theme, + Typography, +} from '@material-ui/core'; +import { pageTheme } from '@backstage/theme'; + +type EntitiesKinds = 'Component' | 'API'; +type EntitiesTypes = + | 'service' + | 'website' + | 'library' + | 'documentation' + | 'api' + | 'tool'; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + card: { + border: `1px solid ${theme.palette.divider}`, + boxShadow: theme.shadows[2], + borderRadius: '4px', + padding: theme.spacing(2), + color: '#fff', + transition: `${theme.transitions.duration.standard}ms`, + '&:hover': { + boxShadow: theme.shadows[4], + }, + }, + bold: { + fontWeight: theme.typography.fontWeightBold, + }, + service: { + background: `${pageTheme.home.shape}, linear-gradient(90deg, ${pageTheme.service.colors})`, + }, + website: { + background: `${pageTheme.home.shape}, linear-gradient(90deg, ${pageTheme.website.colors})`, + }, + library: { + background: `${pageTheme.home.shape}, linear-gradient(90deg, ${pageTheme.library.colors})`, + }, + documentation: { + background: `${pageTheme.home.shape}, linear-gradient(90deg, ${pageTheme.documentation.colors})`, + }, + api: { + background: `${pageTheme.home.shape}, linear-gradient(90deg, #005B4B, #005B4B)`, + }, + tool: { + background: `${pageTheme.home.shape}, linear-gradient(90deg, ${pageTheme.tool.colors})`, + }, + }), +); + +const countEntitiesBy = ( + entities: Array, + kind: EntitiesKinds, + type?: EntitiesTypes, +) => + entities.filter( + e => e.kind === kind && (type ? e?.spec?.type === type : true), + ).length; + +const EntityCountTile = ({ + counter, + className, + name, +}: { + counter: number; + className: EntitiesTypes; + name: string; +}) => { + const classes = useStyles(); + return ( + + + {counter} + + + {name} + + + ); +}; + +export const OwnershipCard = ({ + entity, + variant, +}: { + entity: Entity; + variant: string; +}) => { + const { + metadata: { name: groupName }, + } = entity; + const catalogApi = useApi(catalogApiRef); + const { + loading, + error, + value: componentsWithCounters, + } = useAsync(async () => { + const entitiesList = await catalogApi.getEntities(); + const ownedEntitiesList = entitiesList.items.filter(component => + component?.relations?.some( + r => r.type === RELATION_OWNED_BY && r.target.name === groupName, + ), + ) as Array; + + return [ + { + counter: countEntitiesBy(ownedEntitiesList, 'Component', 'service'), + className: 'service', + name: 'Services', + }, + { + counter: countEntitiesBy( + ownedEntitiesList, + 'Component', + 'documentation', + ), + className: 'documentation', + name: 'Documentation', + }, + { + counter: countEntitiesBy(ownedEntitiesList, 'API'), + className: 'api', + name: 'APIs', + }, + { + counter: countEntitiesBy(ownedEntitiesList, 'Component', 'library'), + className: 'library', + name: 'Libraries', + }, + { + counter: countEntitiesBy(ownedEntitiesList, 'Component', 'website'), + className: 'website', + name: 'Websites', + }, + { + counter: countEntitiesBy(ownedEntitiesList, 'Component', 'tool'), + className: 'tool', + name: 'Tools', + }, + ] as Array<{ counter: number; className: EntitiesTypes; name: string }>; + }, [catalogApi]); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + return ( + + + {componentsWithCounters?.map(c => ( + + + + ))} + + + ); +}; diff --git a/plugins/org/src/components/Cards/OwnershipCard/index.ts b/plugins/org/src/components/Cards/OwnershipCard/index.ts new file mode 100644 index 0000000000..1fa1bb4044 --- /dev/null +++ b/plugins/org/src/components/Cards/OwnershipCard/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 * from './OwnershipCard'; diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx new file mode 100644 index 0000000000..77708e6f27 --- /dev/null +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx @@ -0,0 +1,64 @@ +/* + * 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 { UserEntity } from '@backstage/catalog-model'; +import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { UserProfileCard } from './UserProfileCard'; + +describe('UserSummary Test', () => { + const userEntity: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'calum.leavy', + }, + spec: { + profile: { + displayName: 'Calum Leavy', + email: 'calum-leavy@example.com', + picture: 'https://example.com/staff/calum.jpeg', + }, + memberOf: ['ExampleGroup'], + }, + relations: [ + { + type: 'memberOf', + target: { + kind: 'group', + name: 'ExampleGroup', + namespace: 'default', + }, + }, + ], + }; + + it('Display Profile Card', async () => { + const rendered = await renderWithEffects( + wrapInTestApp(), + ); + + expect(rendered.getByText('calum-leavy@example.com')).toBeInTheDocument(); + expect(rendered.getByAltText('Calum Leavy')).toHaveAttribute( + 'src', + 'https://example.com/staff/calum.jpeg', + ); + expect(rendered.getByText('[ExampleGroup]')).toHaveAttribute( + 'href', + '/catalog/default/group/ExampleGroup', + ); + }); +}); diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx new file mode 100644 index 0000000000..0f32835d70 --- /dev/null +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -0,0 +1,134 @@ +/* + * 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 { Box, Grid, Link, Tooltip, Typography } from '@material-ui/core'; +import Alert from '@material-ui/lab/Alert'; +import { InfoCard } from '@backstage/core'; +import { entityRouteParams } from '@backstage/plugin-catalog'; +import { + Entity, + RELATION_MEMBER_OF, + UserEntity, +} from '@backstage/catalog-model'; +import EmailIcon from '@material-ui/icons/Email'; +import GroupIcon from '@material-ui/icons/Group'; +import PersonIcon from '@material-ui/icons/Person'; +import { Link as RouterLink, generatePath } from 'react-router-dom'; +import { Avatar } from '../../../Avatar'; + +const GroupLink = ({ + groupName, + index, + entity, +}: { + groupName: string; + index: number; + entity: Entity; +}) => ( + <> + {index >= 1 ? ', ' : ''} + + [{groupName}] + + +); + +const CardTitle = ({ title }: { title?: string }) => + title ? ( + + + {title} + + ) : null; + +export const UserProfileCard = ({ + entity: user, + variant, +}: { + entity: UserEntity; + variant: string; +}) => { + const { + spec: { profile }, + } = user; + const groupNames = + user?.relations + ?.filter(r => r.type === RELATION_MEMBER_OF) + ?.map(group => group.target.name) || []; + + if (!user) return User not found; + + return ( + } + variant={variant} + > + + + + + + + + + + + + + {profile?.email && ( + + {profile.email} + + )} + + + + + + + + + {groupNames.map((groupName, index) => ( + + ))} + + + + + + + ); +}; diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/index.ts b/plugins/org/src/components/Cards/User/UserProfileCard/index.ts new file mode 100644 index 0000000000..dc5e2902b7 --- /dev/null +++ b/plugins/org/src/components/Cards/User/UserProfileCard/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 * from './UserProfileCard'; diff --git a/plugins/org/src/components/Cards/User/index.ts b/plugins/org/src/components/Cards/User/index.ts new file mode 100644 index 0000000000..dc5e2902b7 --- /dev/null +++ b/plugins/org/src/components/Cards/User/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 * from './UserProfileCard'; diff --git a/plugins/org/src/components/Cards/index.ts b/plugins/org/src/components/Cards/index.ts new file mode 100644 index 0000000000..62f63da3d4 --- /dev/null +++ b/plugins/org/src/components/Cards/index.ts @@ -0,0 +1,18 @@ +/* + * 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 * from './Group'; +export * from './User'; +export * from './OwnershipCard'; diff --git a/plugins/org/src/components/index.ts b/plugins/org/src/components/index.ts new file mode 100644 index 0000000000..975f66bd25 --- /dev/null +++ b/plugins/org/src/components/index.ts @@ -0,0 +1,17 @@ +/* + * 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 * from './Cards'; diff --git a/plugins/org/src/index.ts b/plugins/org/src/index.ts new file mode 100644 index 0000000000..77ad7f9266 --- /dev/null +++ b/plugins/org/src/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { plugin } from './plugin'; +export * from './components'; diff --git a/plugins/org/src/plugin.test.ts b/plugins/org/src/plugin.test.ts new file mode 100644 index 0000000000..d77cfd7ae8 --- /dev/null +++ b/plugins/org/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { plugin } from './plugin'; + +describe('groups', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/plugins/org/src/plugin.ts b/plugins/org/src/plugin.ts new file mode 100644 index 0000000000..39c3502fb5 --- /dev/null +++ b/plugins/org/src/plugin.ts @@ -0,0 +1,20 @@ +/* + * 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 { createPlugin } from '@backstage/core'; + +export const plugin = createPlugin({ + id: 'org', +}); diff --git a/plugins/org/src/setupTests.ts b/plugins/org/src/setupTests.ts new file mode 100644 index 0000000000..43b8421558 --- /dev/null +++ b/plugins/org/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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 '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; From b8ecf6f4822a8beacf93eac15d7cc6416978ef2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sat, 28 Nov 2020 23:32:21 +0100 Subject: [PATCH 28/86] integration: add the basics of cross-integration concerns --- .changeset/nine-forks-sleep.md | 5 ++ .../integration/src/ScmIntegrationsImpl.ts | 51 ++++++++++++++++ .../src/azure/AzureIntegration.test.ts | 48 +++++++++++++++ .../integration/src/azure/AzureIntegration.ts | 40 +++++++++++++ .../bitbucket/BitbucketIntegration.test.ts | 51 ++++++++++++++++ .../src/bitbucket/BitbucketIntegration.ts | 43 ++++++++++++++ .../src/github/GitHubIntegration.test.ts | 50 ++++++++++++++++ .../src/github/GitHubIntegration.ts | 43 ++++++++++++++ .../src/gitlab/GitLabIntegration.test.ts | 48 +++++++++++++++ .../src/gitlab/GitLabIntegration.ts | 43 ++++++++++++++ packages/integration/src/index.ts | 1 + packages/integration/src/types.ts | 58 +++++++++++++++++++ 12 files changed, 481 insertions(+) create mode 100644 .changeset/nine-forks-sleep.md create mode 100644 packages/integration/src/ScmIntegrationsImpl.ts create mode 100644 packages/integration/src/azure/AzureIntegration.test.ts create mode 100644 packages/integration/src/azure/AzureIntegration.ts create mode 100644 packages/integration/src/bitbucket/BitbucketIntegration.test.ts create mode 100644 packages/integration/src/bitbucket/BitbucketIntegration.ts create mode 100644 packages/integration/src/github/GitHubIntegration.test.ts create mode 100644 packages/integration/src/github/GitHubIntegration.ts create mode 100644 packages/integration/src/gitlab/GitLabIntegration.test.ts create mode 100644 packages/integration/src/gitlab/GitLabIntegration.ts create mode 100644 packages/integration/src/types.ts diff --git a/.changeset/nine-forks-sleep.md b/.changeset/nine-forks-sleep.md new file mode 100644 index 0000000000..7edbca2169 --- /dev/null +++ b/.changeset/nine-forks-sleep.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': minor +--- + +Add the basics of cross-integration concerns diff --git a/packages/integration/src/ScmIntegrationsImpl.ts b/packages/integration/src/ScmIntegrationsImpl.ts new file mode 100644 index 0000000000..ef6a6835b9 --- /dev/null +++ b/packages/integration/src/ScmIntegrationsImpl.ts @@ -0,0 +1,51 @@ +/* + * 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 { Config } from '@backstage/config'; +import { AzureIntegration } from './azure/AzureIntegration'; +import { BitbucketIntegration } from './bitbucket/BitbucketIntegration'; +import { GitHubIntegration } from './github/GitHubIntegration'; +import { GitLabIntegration } from './gitlab/GitLabIntegration'; +import { + ScmIntegration, + ScmIntegrationPredicateTuple, + ScmIntegrations, +} from './types'; + +export class ScmIntegrationsImpl implements ScmIntegrations { + static fromConfig(config: Config): ScmIntegrationsImpl { + return new ScmIntegrationsImpl([ + ...AzureIntegration.factory({ config }), + ...BitbucketIntegration.factory({ config }), + ...GitHubIntegration.factory({ config }), + ...GitLabIntegration.factory({ config }), + ]); + } + + constructor(private readonly integrations: ScmIntegrationPredicateTuple[]) {} + + list(): ScmIntegration[] { + return this.integrations.map(i => i.integration); + } + + byName(name: string): ScmIntegration | undefined { + return this.integrations.map(i => i.integration).find(i => i.name === name); + } + + byUrl(url: string): ScmIntegration | undefined { + return this.integrations.find(i => i.predicate(new URL(url)))?.integration; + } +} diff --git a/packages/integration/src/azure/AzureIntegration.test.ts b/packages/integration/src/azure/AzureIntegration.test.ts new file mode 100644 index 0000000000..d0d3cf65ad --- /dev/null +++ b/packages/integration/src/azure/AzureIntegration.test.ts @@ -0,0 +1,48 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { AzureIntegration } from './AzureIntegration'; + +describe('AzureIntegration', () => { + it('has a working factory', () => { + const integrations = AzureIntegration.factory({ + config: ConfigReader.fromConfigs([ + { + context: '', + data: { + integrations: { + azure: [ + { + host: 'h.com', + token: 'token', + }, + ], + }, + }, + }, + ]), + }); + expect(integrations.length).toBe(2); // including default + expect(integrations[0].predicate(new URL('https://h.com/a'))).toBe(true); + }); + + it('returns the basics', () => { + const integration = new AzureIntegration({ host: 'h.com' } as any); + expect(integration.type).toBe('azure'); + expect(integration.name).toBe('h.com'); + }); +}); diff --git a/packages/integration/src/azure/AzureIntegration.ts b/packages/integration/src/azure/AzureIntegration.ts new file mode 100644 index 0000000000..2e1e4d9795 --- /dev/null +++ b/packages/integration/src/azure/AzureIntegration.ts @@ -0,0 +1,40 @@ +/* + * 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 { ScmIntegration, ScmIntegrationFactory } from '../types'; +import { AzureIntegrationConfig, readAzureIntegrationConfigs } from './config'; + +export class AzureIntegration implements ScmIntegration { + static factory: ScmIntegrationFactory = ({ config }) => { + const configs = readAzureIntegrationConfigs( + config.getOptionalConfigArray('integrations.azure') ?? [], + ); + return configs.map(integration => ({ + predicate: (url: URL) => url.host === integration.host, + integration: new AzureIntegration(integration), + })); + }; + + constructor(private readonly config: AzureIntegrationConfig) {} + + get type(): string { + return 'azure'; + } + + get name(): string { + return this.config.host; + } +} diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.test.ts b/packages/integration/src/bitbucket/BitbucketIntegration.test.ts new file mode 100644 index 0000000000..9678b35014 --- /dev/null +++ b/packages/integration/src/bitbucket/BitbucketIntegration.test.ts @@ -0,0 +1,51 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { BitbucketIntegration } from './BitbucketIntegration'; + +describe('BitbucketIntegration', () => { + it('has a working factory', () => { + const integrations = BitbucketIntegration.factory({ + config: ConfigReader.fromConfigs([ + { + context: '', + data: { + integrations: { + bitbucket: [ + { + host: 'h.com', + apiBaseUrl: 'a', + token: 't', + username: 'u', + appPassword: 'p', + }, + ], + }, + }, + }, + ]), + }); + expect(integrations.length).toBe(2); // including default + expect(integrations[0].predicate(new URL('https://h.com/a'))).toBe(true); + }); + + it('returns the basics', () => { + const integration = new BitbucketIntegration({ host: 'h.com' } as any); + expect(integration.type).toBe('bitbucket'); + expect(integration.name).toBe('h.com'); + }); +}); diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.ts b/packages/integration/src/bitbucket/BitbucketIntegration.ts new file mode 100644 index 0000000000..e06b539611 --- /dev/null +++ b/packages/integration/src/bitbucket/BitbucketIntegration.ts @@ -0,0 +1,43 @@ +/* + * 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 { ScmIntegration, ScmIntegrationFactory } from '../types'; +import { + BitbucketIntegrationConfig, + readBitbucketIntegrationConfigs, +} from './config'; + +export class BitbucketIntegration implements ScmIntegration { + static factory: ScmIntegrationFactory = ({ config }) => { + const configs = readBitbucketIntegrationConfigs( + config.getOptionalConfigArray('integrations.bitbucket') ?? [], + ); + return configs.map(integration => ({ + predicate: (url: URL) => url.host === integration.host, + integration: new BitbucketIntegration(integration), + })); + }; + + constructor(private readonly config: BitbucketIntegrationConfig) {} + + get type(): string { + return 'bitbucket'; + } + + get name(): string { + return this.config.host; + } +} diff --git a/packages/integration/src/github/GitHubIntegration.test.ts b/packages/integration/src/github/GitHubIntegration.test.ts new file mode 100644 index 0000000000..5a8800200f --- /dev/null +++ b/packages/integration/src/github/GitHubIntegration.test.ts @@ -0,0 +1,50 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { GitHubIntegration } from './GitHubIntegration'; + +describe('GitHubIntegration', () => { + it('has a working factory', () => { + const integrations = GitHubIntegration.factory({ + config: ConfigReader.fromConfigs([ + { + context: '', + data: { + integrations: { + github: [ + { + host: 'h.com', + apiBaseUrl: 'a', + rawBaseUrl: 'r', + token: 't', + }, + ], + }, + }, + }, + ]), + }); + expect(integrations.length).toBe(2); // including default + expect(integrations[0].predicate(new URL('https://h.com/a'))).toBe(true); + }); + + it('returns the basics', () => { + const integration = new GitHubIntegration({ host: 'h.com' } as any); + expect(integration.type).toBe('github'); + expect(integration.name).toBe('h.com'); + }); +}); diff --git a/packages/integration/src/github/GitHubIntegration.ts b/packages/integration/src/github/GitHubIntegration.ts new file mode 100644 index 0000000000..f70684975d --- /dev/null +++ b/packages/integration/src/github/GitHubIntegration.ts @@ -0,0 +1,43 @@ +/* + * 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 { ScmIntegration, ScmIntegrationFactory } from '../types'; +import { + GitHubIntegrationConfig, + readGitHubIntegrationConfigs, +} from './config'; + +export class GitHubIntegration implements ScmIntegration { + static factory: ScmIntegrationFactory = ({ config }) => { + const configs = readGitHubIntegrationConfigs( + config.getOptionalConfigArray('integrations.github') ?? [], + ); + return configs.map(integration => ({ + predicate: (url: URL) => url.host === integration.host, + integration: new GitHubIntegration(integration), + })); + }; + + constructor(private readonly config: GitHubIntegrationConfig) {} + + get type(): string { + return 'github'; + } + + get name(): string { + return this.config.host; + } +} diff --git a/packages/integration/src/gitlab/GitLabIntegration.test.ts b/packages/integration/src/gitlab/GitLabIntegration.test.ts new file mode 100644 index 0000000000..5c46610c53 --- /dev/null +++ b/packages/integration/src/gitlab/GitLabIntegration.test.ts @@ -0,0 +1,48 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { GitLabIntegration } from './GitLabIntegration'; + +describe('GitLabIntegration', () => { + it('has a working factory', () => { + const integrations = GitLabIntegration.factory({ + config: ConfigReader.fromConfigs([ + { + context: '', + data: { + integrations: { + gitlab: [ + { + host: 'h.com', + token: 't', + }, + ], + }, + }, + }, + ]), + }); + expect(integrations.length).toBe(2); // including default + expect(integrations[0].predicate(new URL('https://h.com/a'))).toBe(true); + }); + + it('returns the basics', () => { + const integration = new GitLabIntegration({ host: 'h.com' } as any); + expect(integration.type).toBe('gitlab'); + expect(integration.name).toBe('h.com'); + }); +}); diff --git a/packages/integration/src/gitlab/GitLabIntegration.ts b/packages/integration/src/gitlab/GitLabIntegration.ts new file mode 100644 index 0000000000..6e94e084cc --- /dev/null +++ b/packages/integration/src/gitlab/GitLabIntegration.ts @@ -0,0 +1,43 @@ +/* + * 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 { ScmIntegration, ScmIntegrationFactory } from '../types'; +import { + GitLabIntegrationConfig, + readGitLabIntegrationConfigs, +} from './config'; + +export class GitLabIntegration implements ScmIntegration { + static factory: ScmIntegrationFactory = ({ config }) => { + const configs = readGitLabIntegrationConfigs( + config.getOptionalConfigArray('integrations.gitlab') ?? [], + ); + return configs.map(integration => ({ + predicate: (url: URL) => url.host === integration.host, + integration: new GitLabIntegration(integration), + })); + }; + + constructor(private readonly config: GitLabIntegrationConfig) {} + + get type(): string { + return 'gitlab'; + } + + get name(): string { + return this.config.host; + } +} diff --git a/packages/integration/src/index.ts b/packages/integration/src/index.ts index bfed81824f..9d42914fdb 100644 --- a/packages/integration/src/index.ts +++ b/packages/integration/src/index.ts @@ -18,3 +18,4 @@ export * from './azure'; export * from './bitbucket'; export * from './github'; export * from './gitlab'; +export type { ScmIntegration, ScmIntegrations } from './types'; diff --git a/packages/integration/src/types.ts b/packages/integration/src/types.ts new file mode 100644 index 0000000000..6f9290a948 --- /dev/null +++ b/packages/integration/src/types.ts @@ -0,0 +1,58 @@ +/* + * 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 { Config } from '@backstage/config'; + +/** + * Encapsulates a single SCM integration. + */ +export type ScmIntegration = { + type: string; + name: string; +}; + +/** + * Holds all registered SCM integrations. + */ +export type ScmIntegrations = { + /** + * Lists all registered integrations. + */ + list(): ScmIntegration[]; + + /** + * Fetches a named integration + * + * @param name The name of a registered integration + */ + byName(name: string): ScmIntegration | undefined; + + /** + * Fetches an integration by URL + * + * @param name A URL that matches a registered integration + */ + byUrl(url: string): ScmIntegration | undefined; +}; + +export type ScmIntegrationPredicateTuple = { + predicate: (url: URL) => boolean; + integration: ScmIntegration; +}; + +export type ScmIntegrationFactory = (options: { + config: Config; +}) => ScmIntegrationPredicateTuple[]; From 87a33d2fe8060407d832e51ce2c595aa8d6f7dad Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Mon, 7 Dec 2020 16:49:24 +0100 Subject: [PATCH 29/86] [TechDocs] Bug fixes and cleanups (#3599) * Fixed issue with some internal doc links reloading the page and removed modifyCss transformer * Create silly-kiwis-rest.md * Formatting in changeset --- .changeset/silly-kiwis-rest.md | 6 ++ .../techdocs/src/reader/components/Reader.tsx | 15 ++--- .../transformers/addLinkClickListener.ts | 6 +- .../techdocs/src/reader/transformers/index.ts | 1 - .../reader/transformers/modifyCss.test.tsx | 58 ------------------- .../src/reader/transformers/modifyCss.ts | 43 -------------- 6 files changed, 14 insertions(+), 115 deletions(-) create mode 100644 .changeset/silly-kiwis-rest.md delete mode 100644 plugins/techdocs/src/reader/transformers/modifyCss.test.tsx delete mode 100644 plugins/techdocs/src/reader/transformers/modifyCss.ts diff --git a/.changeset/silly-kiwis-rest.md b/.changeset/silly-kiwis-rest.md new file mode 100644 index 0000000000..acb0d79bb2 --- /dev/null +++ b/.changeset/silly-kiwis-rest.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-techdocs': minor +--- + +Removed modifyCss transformer and moved the css to injectCss transformer +Fixed issue where some internal doc links would cause a reload of the page diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index bdf91936ba..fb49b90096 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -30,7 +30,6 @@ import transformer, { addLinkClickListener, removeMkdocsHeader, simplifyMkdocsFooter, - modifyCss, onCssReady, sanitizeDOM, injectCss, @@ -71,15 +70,6 @@ export const Reader = ({ entityId, onReady }: Props) => { path, }), rewriteDocLinks(), - modifyCss({ - cssTransforms: { - '.md-main__inner': [{ 'margin-top': '0' }], - '.md-sidebar': [{ top: '0' }, { width: '20rem' }], - '.md-typeset': [{ 'font-size': '1rem' }], - '.md-nav': [{ 'font-size': '1rem' }], - '.md-grid': [{ 'max-width': '80vw' }], - }, - }), removeMkdocsHeader(), simplifyMkdocsFooter(), injectCss({ @@ -92,6 +82,11 @@ export const Reader = ({ entityId, onReady }: Props) => { --md-code-fg-color: ${theme.palette.text.primary}; --md-code-bg-color: ${theme.palette.background.paper}; } + .md-main__inner { margin-top: 0; } + .md-sidebar { top: 0; width: 20rem; } + .md-typeset { font-size: 1rem; } + .md-nav { font-size: 1rem; } + .md-grid { max-width: 80vw; } `, }), ]); diff --git a/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts b/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts index b6a6509550..7c5fd4d9f4 100644 --- a/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts +++ b/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts @@ -28,13 +28,13 @@ export const addLinkClickListener = ({ return dom => { Array.from(dom.getElementsByTagName('a')).forEach(elem => { elem.addEventListener('click', (e: MouseEvent) => { - const target = e.target as HTMLAnchorElement; - const href = target?.getAttribute('href'); + const target = elem as HTMLAnchorElement; + const href = target.getAttribute('href'); if (!href) return; if (href.startsWith(baseUrl)) { e.preventDefault(); - onClick(e, target.getAttribute('href')!); + onClick(e, href); } }); }); diff --git a/plugins/techdocs/src/reader/transformers/index.ts b/plugins/techdocs/src/reader/transformers/index.ts index 0bab085abe..24f3976d75 100644 --- a/plugins/techdocs/src/reader/transformers/index.ts +++ b/plugins/techdocs/src/reader/transformers/index.ts @@ -19,7 +19,6 @@ export * from './rewriteDocLinks'; export * from './addLinkClickListener'; export * from './removeMkdocsHeader'; export * from './simplifyMkdocsFooter'; -export * from './modifyCss'; export * from './onCssReady'; export * from './sanitizeDOM'; export * from './injectCss'; diff --git a/plugins/techdocs/src/reader/transformers/modifyCss.test.tsx b/plugins/techdocs/src/reader/transformers/modifyCss.test.tsx deleted file mode 100644 index fff3869c96..0000000000 --- a/plugins/techdocs/src/reader/transformers/modifyCss.test.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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 { createTestShadowDom } from '../../test-utils'; -import { modifyCss } from '../transformers'; - -describe('modifyCss', () => { - it('does not modify css', () => { - const shadowDom = createTestShadowDom( - `
`, - { - preTransformers: [], - postTransformers: [], - }, - ); - - const { fontSize } = getComputedStyle( - shadowDom.querySelector('.md-typeset')!, - ); - - expect(fontSize).toBe('0.8em'); - }); - - it('does modify css', () => { - const shadowDom = createTestShadowDom( - `
`, - { - preTransformers: [ - modifyCss({ - cssTransforms: { - '.md-typeset': [{ 'font-size': '1em' }], - }, - }), - ], - postTransformers: [], - }, - ); - - const { fontSize } = getComputedStyle( - shadowDom.querySelector('.md-typeset')!, - ); - - expect(fontSize).toBe('1em'); - }); -}); diff --git a/plugins/techdocs/src/reader/transformers/modifyCss.ts b/plugins/techdocs/src/reader/transformers/modifyCss.ts deleted file mode 100644 index 5116baac45..0000000000 --- a/plugins/techdocs/src/reader/transformers/modifyCss.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 type { Transformer } from './index'; - -type ModifyCssOptions = { - // Example: { '.md-container': { 'marginTop': '10px' }} - cssTransforms: { [key: string]: { [key: string]: string }[] }; -}; - -export const modifyCss = ({ cssTransforms }: ModifyCssOptions): Transformer => { - return dom => { - Object.entries(cssTransforms).forEach(([cssSelector, cssChanges]) => { - const elementsToChange = Array.from( - dom.querySelectorAll(cssSelector), - ); - if (elementsToChange.length < 1) return; - - cssChanges.forEach(changes => { - elementsToChange.forEach((element: HTMLElement) => { - Object.entries(changes).forEach(([cssProperty, cssValue]) => { - element.style.setProperty(cssProperty, cssValue); - }); - }); - }); - }); - - return dom; - }; -}; From 38e24db009a50428984103562e21ac3848f39c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sun, 29 Nov 2020 14:28:27 +0100 Subject: [PATCH 30/86] integration: move the core url and auth logic to integration for the four major providers --- .changeset/lazy-beans-search.md | 6 + .../src/reading/AzureUrlReader.test.ts | 23 +-- .../src/reading/AzureUrlReader.ts | 108 +----------- .../src/reading/BitbucketUrlReader.test.ts | 87 +--------- .../src/reading/BitbucketUrlReader.ts | 64 +------ .../src/reading/GithubUrlReader.test.ts | 146 +--------------- .../src/reading/GithubUrlReader.ts | 100 +---------- .../src/reading/GitlabUrlReader.ts | 118 +------------ packages/integration/package.json | 4 +- packages/integration/src/azure/core.test.ts | 93 +++++++++++ packages/integration/src/azure/core.ts | 131 +++++++++++++++ packages/integration/src/azure/index.ts | 5 + .../integration/src/bitbucket/core.test.ts | 100 +++++++++++ packages/integration/src/bitbucket/core.ts | 84 ++++++++++ packages/integration/src/bitbucket/index.ts | 1 + packages/integration/src/github/core.test.ts | 115 +++++++++++++ packages/integration/src/github/core.ts | 82 +++++++++ packages/integration/src/github/index.ts | 1 + packages/integration/src/gitlab/core.test.ts | 80 +++++++++ packages/integration/src/gitlab/core.ts | 157 ++++++++++++++++++ packages/integration/src/gitlab/index.ts | 1 + 21 files changed, 884 insertions(+), 622 deletions(-) create mode 100644 .changeset/lazy-beans-search.md create mode 100644 packages/integration/src/azure/core.test.ts create mode 100644 packages/integration/src/azure/core.ts create mode 100644 packages/integration/src/bitbucket/core.test.ts create mode 100644 packages/integration/src/bitbucket/core.ts create mode 100644 packages/integration/src/github/core.test.ts create mode 100644 packages/integration/src/github/core.ts create mode 100644 packages/integration/src/gitlab/core.test.ts create mode 100644 packages/integration/src/gitlab/core.ts diff --git a/.changeset/lazy-beans-search.md b/.changeset/lazy-beans-search.md new file mode 100644 index 0000000000..1ea3c4bb80 --- /dev/null +++ b/.changeset/lazy-beans-search.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-common': patch +'@backstage/integration': patch +--- + +Move the core url and auth logic to integration for the four major providers diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index ab97d1b073..2c8549f917 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -20,7 +20,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '../logging'; -import { AzureUrlReader, getDownloadUrl } from './AzureUrlReader'; +import { AzureUrlReader } from './AzureUrlReader'; import { msw } from '@backstage/test-utils'; import { ReadTreeResponseFactory } from './tree'; @@ -111,13 +111,13 @@ describe('AzureUrlReader', () => { url: 'https://api.com/a/b/blob/master/path/to/c.yaml', config: createConfig(), error: - 'Incorrect url: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Azure Devops URL or Invalid file path', + 'Incorrect URL: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Azure Devops URL or Invalid file path', }, { url: 'com/a/b/blob/master/path/to/c.yaml', config: createConfig(), error: - 'Incorrect url: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml', + 'Incorrect URL: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml', }, { url: '', @@ -178,21 +178,4 @@ describe('AzureUrlReader', () => { expect(indexMarkdownFile.toString()).toBe('# Test\n'); }); }); - - describe('getDownloadUrl', () => { - it('do not add scopePath if no path is specified', async () => { - const result = getDownloadUrl( - 'https://dev.azure.com/organization/project/_git/repository', - ); - - expect(result.searchParams.get('scopePath')).toBeNull(); - }); - - it('add scopePath if a path is specified', async () => { - const result = getDownloadUrl( - 'https://dev.azure.com/organization/project/_git/repository?path=%2Fdocs', - ); - expect(result.searchParams.get('scopePath')).toEqual('docs'); - }); - }); }); diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts index ad990d1d5d..ca934ee42c 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.ts @@ -17,10 +17,12 @@ import { AzureIntegrationConfig, readAzureIntegrationConfigs, + getAzureFileFetchUrl, + getAzureDownloadUrl, + getAzureRequestOptions, } from '@backstage/integration'; import fetch from 'cross-fetch'; import { Readable } from 'stream'; -import parseGitUri from 'git-url-parse'; import { NotFoundError } from '../errors'; import { ReaderFactory, @@ -30,28 +32,6 @@ import { } from './types'; import { ReadTreeResponseFactory } from './tree'; -export function getDownloadUrl(url: string): URL { - const { - name: repoName, - owner: project, - organization, - protocol, - resource, - filepath, - } = parseGitUri(url); - - // scopePath will limit the downloaded content - // /docs will only download the docs folder and everything below it - // /docs/index.md will only download index.md but put it in the root of the archive - const scopePath = filepath - ? `&scopePath=${encodeURIComponent(filepath)}` - : ''; - - return new URL( - `${protocol}://${resource}/${organization}/${project}/_apis/git/repositories/${repoName}/items?recursionLevel=full&download=true&api-version=6.0${scopePath}`, - ); -} - export class AzureUrlReader implements UrlReader { static factory: ReaderFactory = ({ config, treeResponseFactory }) => { const configs = readAzureIntegrationConfigs( @@ -76,11 +56,11 @@ export class AzureUrlReader implements UrlReader { } async read(url: string): Promise { - const builtUrl = this.buildRawUrl(url); + const builtUrl = getAzureFileFetchUrl(url); let response: Response; try { - response = await fetch(builtUrl.toString(), this.getRequestOptions()); + response = await fetch(builtUrl, getAzureRequestOptions(this.options)); } catch (e) { throw new Error(`Unable to read ${url}, ${e}`); } @@ -102,8 +82,8 @@ export class AzureUrlReader implements UrlReader { options?: ReadTreeOptions, ): Promise { const response = await fetch( - getDownloadUrl(url).toString(), - this.getRequestOptions({ Accept: 'application/zip' }), + getAzureDownloadUrl(url), + getAzureRequestOptions(this.options, { Accept: 'application/zip' }), ); if (!response.ok) { const message = `Failed to read tree from ${url}, ${response.status} ${response.statusText}`; @@ -119,80 +99,6 @@ export class AzureUrlReader implements UrlReader { }); } - // Converts - // from: https://dev.azure.com/{organization}/{project}/_git/reponame?path={path}&version=GB{commitOrBranch}&_a=contents - // to: https://dev.azure.com/{organization}/{project}/_apis/git/repositories/reponame/items?path={path}&version={commitOrBranch} - private buildRawUrl(target: string): URL { - try { - const url = new URL(target); - - const [ - empty, - userOrOrg, - project, - srcKeyword, - repoName, - ] = url.pathname.split('/'); - - const path = url.searchParams.get('path') || ''; - const ref = url.searchParams.get('version')?.substr(2); - - if ( - url.hostname !== 'dev.azure.com' || - empty !== '' || - userOrOrg === '' || - project === '' || - srcKeyword !== '_git' || - repoName === '' || - path === '' || - ref === '' - ) { - throw new Error('Wrong Azure Devops URL or Invalid file path'); - } - - // transform to api - url.pathname = [ - empty, - userOrOrg, - project, - '_apis', - 'git', - 'repositories', - repoName, - 'items', - ].join('/'); - - const queryParams = [`path=${path}`]; - - if (ref) { - queryParams.push(`version=${ref}`); - } - - url.search = queryParams.join('&'); - - url.protocol = 'https'; - - return url; - } catch (e) { - throw new Error(`Incorrect url: ${target}, ${e}`); - } - } - - private getRequestOptions(additionalHeaders?: { - [key: string]: string; - }): RequestInit { - const headers: HeadersInit = additionalHeaders ?? {}; - - if (this.options.token) { - headers.Authorization = `Basic ${Buffer.from( - `:${this.options.token}`, - 'utf8', - ).toString('base64')}`; - } - - return { headers }; - } - toString() { const { host, token } = this.options; return `azure{host=${host},authed=${Boolean(token)}}`; diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index 01744db28a..1c0bd39372 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -14,94 +14,9 @@ * limitations under the License. */ -import { BitbucketIntegrationConfig } from '@backstage/integration'; -import { - BitbucketUrlReader, - getApiRequestOptions, - getApiUrl, -} from './BitbucketUrlReader'; +import { BitbucketUrlReader } from './BitbucketUrlReader'; describe('BitbucketUrlReader', () => { - describe('getApiRequestOptions', () => { - it('inserts a token when needed', () => { - const withToken: BitbucketIntegrationConfig = { - host: '', - apiBaseUrl: '', - token: 'A', - }; - const withoutToken: BitbucketIntegrationConfig = { - host: '', - apiBaseUrl: '', - }; - expect( - (getApiRequestOptions(withToken).headers as any).Authorization, - ).toEqual('Bearer A'); - expect( - (getApiRequestOptions(withoutToken).headers as any).Authorization, - ).toBeUndefined(); - }); - - it('insert basic auth when needed', () => { - const withUsernameAndPassword: BitbucketIntegrationConfig = { - host: '', - apiBaseUrl: '', - username: 'some-user', - appPassword: 'my-secret', - }; - const withoutUsernameAndPassword: BitbucketIntegrationConfig = { - host: '', - apiBaseUrl: '', - }; - expect( - (getApiRequestOptions(withUsernameAndPassword).headers as any) - .Authorization, - ).toEqual('Basic c29tZS11c2VyOm15LXNlY3JldA=='); - expect( - (getApiRequestOptions(withoutUsernameAndPassword).headers as any) - .Authorization, - ).toBeUndefined(); - }); - }); - - describe('getApiUrl', () => { - it('rejects targets that do not look like URLs', () => { - const config: BitbucketIntegrationConfig = { host: '', apiBaseUrl: '' }; - expect(() => getApiUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/); - }); - it('happy path for Bitbucket Cloud', () => { - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.org', - apiBaseUrl: 'https://api.bitbucket.org/2.0', - }; - expect( - getApiUrl( - 'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml', - config, - ), - ).toEqual( - new URL( - 'https://api.bitbucket.org/2.0/repositories/org-name/repo-name/src/master/templates/my-template.yaml', - ), - ); - }); - it('happy path for Bitbucket Server', () => { - const config: BitbucketIntegrationConfig = { - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://bitbucket.mycompany.net/rest/api/1.0', - }; - expect( - getApiUrl( - 'https://bitbucket.mycompany.net/projects/a/repos/b/browse/path/to/c.yaml', - config, - ), - ).toEqual( - new URL( - 'https://bitbucket.mycompany.net/rest/api/1.0/projects/a/repos/b/raw/path/to/c.yaml', - ), - ); - }); - }); - describe('implementation', () => { it('rejects unknown targets', async () => { const processor = new BitbucketUrlReader({ diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index 9694c1d987..8c97bf2ee2 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -16,69 +16,14 @@ import { BitbucketIntegrationConfig, + getBitbucketFileFetchUrl, + getBitbucketRequestOptions, readBitbucketIntegrationConfigs, } from '@backstage/integration'; import fetch from 'cross-fetch'; -import parseGitUri from 'git-url-parse'; import { NotFoundError } from '../errors'; import { ReaderFactory, ReadTreeResponse, UrlReader } from './types'; -export function getApiRequestOptions( - provider: BitbucketIntegrationConfig, -): RequestInit { - const headers: HeadersInit = {}; - - if (provider.token) { - headers.Authorization = `Bearer ${provider.token}`; - } else if (provider.username && provider.appPassword) { - headers.Authorization = `Basic ${Buffer.from( - `${provider.username}:${provider.appPassword}`, - 'utf8', - ).toString('base64')}`; - } - - return { - headers, - }; -} - -// Converts for example -// from: https://bitbucket.org/orgname/reponame/src/master/file.yaml -// to: https://api.bitbucket.org/2.0/repositories/orgname/reponame/src/master/file.yaml -export function getApiUrl( - target: string, - provider: BitbucketIntegrationConfig, -): URL { - try { - const { owner, name, ref, filepathtype, filepath } = parseGitUri(target); - if ( - !owner || - !name || - (filepathtype !== 'browse' && - filepathtype !== 'raw' && - filepathtype !== 'src') - ) { - throw new Error('Invalid Bitbucket URL or file path'); - } - - const pathWithoutSlash = filepath.replace(/^\//, ''); - - if (provider.host === 'bitbucket.org') { - if (!ref) { - throw new Error('Invalid Bitbucket URL or file path'); - } - return new URL( - `${provider.apiBaseUrl}/repositories/${owner}/${name}/src/${ref}/${pathWithoutSlash}`, - ); - } - return new URL( - `${provider.apiBaseUrl}/projects/${owner}/repos/${name}/raw/${pathWithoutSlash}?at=${ref}`, - ); - } catch (e) { - throw new Error(`Incorrect URL: ${target}, ${e}`); - } -} - /** * A processor that adds the ability to read files from Bitbucket v1 and v2 APIs, such as * the one exposed by Bitbucket Cloud itself. @@ -116,9 +61,8 @@ export class BitbucketUrlReader implements UrlReader { } async read(url: string): Promise { - const bitbucketUrl = getApiUrl(url, this.config); - - const options = getApiRequestOptions(this.config); + const bitbucketUrl = getBitbucketFileFetchUrl(url, this.config); + const options = getBitbucketRequestOptions(this.config); let response: Response; try { diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index abe8b4f640..f842adcf90 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -15,19 +15,12 @@ */ import { ConfigReader } from '@backstage/config'; -import { GitHubIntegrationConfig } from '@backstage/integration'; import { msw } from '@backstage/test-utils'; import fs from 'fs'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import path from 'path'; -import { - getApiRequestOptions, - getApiUrl, - getRawRequestOptions, - getRawUrl, - GithubUrlReader, -} from './GithubUrlReader'; +import { GithubUrlReader } from './GithubUrlReader'; import { ReadTreeResponseFactory } from './tree'; const treeResponseFactory = ReadTreeResponseFactory.create({ @@ -35,143 +28,6 @@ const treeResponseFactory = ReadTreeResponseFactory.create({ }); describe('GithubUrlReader', () => { - describe('getApiRequestOptions', () => { - it('sets the correct API version', () => { - const config: GitHubIntegrationConfig = { host: '', apiBaseUrl: '' }; - expect((getApiRequestOptions(config).headers as any).Accept).toEqual( - 'application/vnd.github.v3.raw', - ); - }); - - it('inserts a token when needed', () => { - const withToken: GitHubIntegrationConfig = { - host: '', - apiBaseUrl: '', - token: 'A', - }; - const withoutToken: GitHubIntegrationConfig = { - host: '', - apiBaseUrl: '', - }; - expect( - (getApiRequestOptions(withToken).headers as any).Authorization, - ).toEqual('token A'); - expect( - (getApiRequestOptions(withoutToken).headers as any).Authorization, - ).toBeUndefined(); - }); - }); - - describe('getRawRequestOptions', () => { - it('inserts a token when needed', () => { - const withToken: GitHubIntegrationConfig = { - host: '', - rawBaseUrl: '', - token: 'A', - }; - const withoutToken: GitHubIntegrationConfig = { - host: '', - rawBaseUrl: '', - }; - expect( - (getRawRequestOptions(withToken).headers as any).Authorization, - ).toEqual('token A'); - expect( - (getRawRequestOptions(withoutToken).headers as any).Authorization, - ).toBeUndefined(); - }); - }); - - describe('getApiUrl', () => { - it('rejects targets that do not look like URLs', () => { - const config: GitHubIntegrationConfig = { host: '', apiBaseUrl: '' }; - expect(() => getApiUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/); - }); - - it('happy path for github', () => { - const config: GitHubIntegrationConfig = { - host: 'github.com', - apiBaseUrl: 'https://api.github.com', - }; - expect( - getApiUrl( - 'https://github.com/a/b/blob/branchname/path/to/c.yaml', - config, - ), - ).toEqual( - new URL( - 'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname', - ), - ); - expect( - getApiUrl( - 'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml', - config, - ), - ).toEqual( - new URL( - 'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname', - ), - ); - }); - - it('happy path for ghe', () => { - const config: GitHubIntegrationConfig = { - host: 'ghe.mycompany.net', - apiBaseUrl: 'https://ghe.mycompany.net/api/v3', - }; - expect( - getApiUrl( - 'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml', - config, - ), - ).toEqual( - new URL( - 'https://ghe.mycompany.net/api/v3/repos/a/b/contents/path/to/c.yaml?ref=branchname', - ), - ); - }); - }); - - describe('getRawUrl', () => { - it('rejects targets that do not look like URLs', () => { - const config: GitHubIntegrationConfig = { host: '', apiBaseUrl: '' }; - expect(() => getRawUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/); - }); - - it('happy path for github', () => { - const config: GitHubIntegrationConfig = { - host: 'github.com', - rawBaseUrl: 'https://raw.githubusercontent.com', - }; - expect( - getRawUrl( - 'https://github.com/a/b/blob/branchname/path/to/c.yaml', - config, - ), - ).toEqual( - new URL( - 'https://raw.githubusercontent.com/a/b/branchname/path/to/c.yaml', - ), - ); - }); - - it('happy path for ghe', () => { - const config: GitHubIntegrationConfig = { - host: 'ghe.mycompany.net', - rawBaseUrl: 'https://ghe.mycompany.net/raw', - }; - expect( - getRawUrl( - 'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml', - config, - ), - ).toEqual( - new URL('https://ghe.mycompany.net/raw/a/b/branchname/path/to/c.yaml'), - ); - }); - }); - describe('implementation', () => { it('rejects unknown targets', async () => { const processor = new GithubUrlReader( diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index de798a7067..5ca2a99692 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -17,6 +17,8 @@ import { GitHubIntegrationConfig, readGitHubIntegrationConfigs, + getGitHubFileFetchUrl, + getGitHubRequestOptions, } from '@backstage/integration'; import fetch from 'cross-fetch'; import parseGitUri from 'git-url-parse'; @@ -30,92 +32,6 @@ import { UrlReader, } from './types'; -export function getApiRequestOptions( - provider: GitHubIntegrationConfig, -): RequestInit { - const headers: HeadersInit = { - Accept: 'application/vnd.github.v3.raw', - }; - - if (provider.token) { - headers.Authorization = `token ${provider.token}`; - } - - return { - headers, - }; -} - -export function getRawRequestOptions( - provider: GitHubIntegrationConfig, -): RequestInit { - const headers: HeadersInit = {}; - - if (provider.token) { - headers.Authorization = `token ${provider.token}`; - } - - return { - headers, - }; -} - -// Converts for example -// from: https://github.com/a/b/blob/branchname/path/to/c.yaml -// to: https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname -export function getApiUrl( - target: string, - provider: GitHubIntegrationConfig, -): URL { - try { - const { owner, name, ref, filepathtype, filepath } = parseGitUri(target); - - if ( - !owner || - !name || - !ref || - (filepathtype !== 'blob' && filepathtype !== 'raw') - ) { - throw new Error('Invalid GitHub URL or file path'); - } - - const pathWithoutSlash = filepath.replace(/^\//, ''); - return new URL( - `${provider.apiBaseUrl}/repos/${owner}/${name}/contents/${pathWithoutSlash}?ref=${ref}`, - ); - } catch (e) { - throw new Error(`Incorrect URL: ${target}, ${e}`); - } -} - -// Converts for example -// from: https://github.com/a/b/blob/branchname/c.yaml -// to: https://raw.githubusercontent.com/a/b/branchname/c.yaml -export function getRawUrl( - target: string, - provider: GitHubIntegrationConfig, -): URL { - try { - const { owner, name, ref, filepathtype, filepath } = parseGitUri(target); - - if ( - !owner || - !name || - !ref || - (filepathtype !== 'blob' && filepathtype !== 'raw') - ) { - throw new Error('Invalid GitHub URL or file path'); - } - - const pathWithoutSlash = filepath.replace(/^\//, ''); - return new URL( - `${provider.rawBaseUrl}/${owner}/${name}/${ref}/${pathWithoutSlash}`, - ); - } catch (e) { - throw new Error(`Incorrect URL: ${target}, ${e}`); - } -} - /** * A processor that adds the ability to read files from GitHub v3 APIs, such as * the one exposed by GitHub itself. @@ -144,14 +60,8 @@ export class GithubUrlReader implements UrlReader { } async read(url: string): Promise { - const useApi = - this.config.apiBaseUrl && (this.config.token || !this.config.rawBaseUrl); - const ghUrl = useApi - ? getApiUrl(url, this.config) - : getRawUrl(url, this.config); - const options = useApi - ? getApiRequestOptions(this.config) - : getRawRequestOptions(this.config); + const ghUrl = getGitHubFileFetchUrl(url, this.config); + const options = getGitHubRequestOptions(this.config); let response: Response; try { @@ -196,7 +106,7 @@ export class GithubUrlReader implements UrlReader { new URL( `${protocol}://${resource}/${full_name}/archive/${ref}.tar.gz`, ).toString(), - getRawRequestOptions(this.config), + getGitHubRequestOptions(this.config), ); if (!response.ok) { const message = `Failed to read tree from ${url}, ${response.status} ${response.statusText}`; diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts index e2d3edfea2..d6d1da5cbb 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.ts @@ -15,6 +15,8 @@ */ import { + getGitLabFileFetchUrl, + getGitLabRequestOptions, GitLabIntegrationConfig, readGitLabIntegrationConfigs, } from '@backstage/integration'; @@ -37,20 +39,11 @@ export class GitlabUrlReader implements UrlReader { constructor(private readonly options: GitLabIntegrationConfig) {} async read(url: string): Promise { - // TODO(Rugvip): merged the old GitlabReaderProcessor in here and used - // the existence of /~/blob/ to switch the logic. Don't know if this - // makes sense and it might require some more work. - let builtUrl: URL; - if (url.includes('/-/blob/')) { - const projectID = await this.getProjectID(url); - builtUrl = this.buildProjectUrl(url, projectID); - } else { - builtUrl = this.buildRawUrl(url); - } + const builtUrl = await getGitLabFileFetchUrl(url, this.options); let response: Response; try { - response = await fetch(builtUrl.toString(), this.getRequestOptions()); + response = await fetch(builtUrl, getGitLabRequestOptions(this.options)); } catch (e) { throw new Error(`Unable to read ${url}, ${e}`); } @@ -70,109 +63,6 @@ export class GitlabUrlReader implements UrlReader { throw new Error('GitlabUrlReader does not implement readTree'); } - // Converts - // from: https://gitlab.example.com/a/b/blob/master/c.yaml - // to: https://gitlab.example.com/a/b/raw/master/c.yaml - private buildRawUrl(target: string): URL { - try { - const url = new URL(target); - - const [ - empty, - userOrOrg, - repoName, - blobKeyword, - ...restOfPath - ] = url.pathname.split('/'); - - if ( - empty !== '' || - userOrOrg === '' || - repoName === '' || - blobKeyword !== 'blob' || - !restOfPath.join('/').match(/\.yaml$/) - ) { - throw new Error('Wrong GitLab URL'); - } - - // Replace 'blob' with 'raw' - url.pathname = [empty, userOrOrg, repoName, 'raw', ...restOfPath].join( - '/', - ); - - return url; - } catch (e) { - throw new Error(`Incorrect url: ${target}, ${e}`); - } - } - - // convert https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath - // to https://gitlab.com/api/v4/projects//repository/files/filepath?ref=branch - private buildProjectUrl(target: string, projectID: Number): URL { - try { - const url = new URL(target); - - const branchAndFilePath = url.pathname.split('/-/blob/')[1]; - - const [branch, ...filePath] = branchAndFilePath.split('/'); - - url.pathname = [ - '/api/v4/projects', - projectID, - 'repository/files', - encodeURIComponent(filePath.join('/')), - 'raw', - ].join('/'); - url.search = `?ref=${branch}`; - - return url; - } catch (e) { - throw new Error(`Incorrect url: ${target}, ${e}`); - } - } - - private async getProjectID(target: string): Promise { - const url = new URL(target); - - if ( - // absPaths to gitlab files should contain /-/blob - // ex: https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath - !url.pathname.match(/\/\-\/blob\//) - ) { - throw new Error('Please provide full path to yaml file from Gitlab'); - } - try { - const repo = url.pathname.split('/-/blob/')[0]; - - // Find ProjectID from url - // convert 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath' - // to 'https://gitlab.com/api/v4/projects/groupA%2Fteams%2FsubgroupA%2FteamA%2Frepo' - const repoIDLookup = new URL( - `${url.protocol + url.hostname}/api/v4/projects/${encodeURIComponent( - repo.replace(/^\//, ''), - )}`, - ); - const response = await fetch( - repoIDLookup.toString(), - this.getRequestOptions(), - ); - const projectIDJson = await response.json(); - const projectID: Number = projectIDJson.id; - - return projectID; - } catch (e) { - throw new Error(`Could not get GitLab ProjectID for: ${target}, ${e}`); - } - } - - private getRequestOptions(): RequestInit { - return { - headers: { - ['PRIVATE-TOKEN']: this.options.token ?? '', - }, - }; - } - toString() { const { host, token } = this.options; return `gitlab{host=${host},authed=${Boolean(token)}}`; diff --git a/packages/integration/package.json b/packages/integration/package.json index 342476c38f..8af3cb60b8 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -21,11 +21,13 @@ }, "dependencies": { "@backstage/config": "^0.1.1", + "cross-fetch": "^3.0.6", "git-url-parse": "^11.4.0" }, "devDependencies": { "@backstage/cli": "^0.4.0", - "@types/jest": "^26.0.7" + "@types/jest": "^26.0.7", + "msw": "^0.21.2" }, "files": [ "dist", diff --git a/packages/integration/src/azure/core.test.ts b/packages/integration/src/azure/core.test.ts new file mode 100644 index 0000000000..17041a9eb2 --- /dev/null +++ b/packages/integration/src/azure/core.test.ts @@ -0,0 +1,93 @@ +/* + * 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 { + getAzureFileFetchUrl, + getAzureDownloadUrl, + getAzureRequestOptions, +} from './core'; + +describe('azure core', () => { + describe('getAzureRequestOptions', () => { + it('fills in the token if necessary', () => { + expect(getAzureRequestOptions({ host: '', token: '0123456789' })).toEqual( + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'Basic OjAxMjM0NTY3ODk=', + }), + }), + ); + expect(getAzureRequestOptions({ host: '' })).toEqual( + expect.objectContaining({ + headers: expect.not.objectContaining({ + Authorization: expect.anything(), + }), + }), + ); + }); + }); + + describe('getAzureFileFetchUrl', () => { + it.each([ + { + url: + 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster', + result: + 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master', + }, + { + url: + 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml', + result: + 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml', + }, + ])('should handle happy path %#', async ({ url, result }) => { + expect(getAzureFileFetchUrl(url)).toBe(result); + }); + + it.each([ + { + url: 'https://api.com/a/b/blob/master/path/to/c.yaml', + error: + 'Incorrect URL: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Azure Devops URL or Invalid file path', + }, + { + url: 'com/a/b/blob/master/path/to/c.yaml', + error: + 'Incorrect URL: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml', + }, + ])('should handle error path %#', ({ url, error }) => { + expect(() => getAzureFileFetchUrl(url)).toThrow(error); + }); + }); + + describe('getAzureDownloadUrl', () => { + it('do not add scopePath if no path is specified', async () => { + const result = getAzureDownloadUrl( + 'https://dev.azure.com/organization/project/_git/repository', + ); + + expect(new URL(result).searchParams.get('scopePath')).toBeNull(); + }); + + it('add scopePath if a path is specified', async () => { + const result = getAzureDownloadUrl( + 'https://dev.azure.com/organization/project/_git/repository?path=%2Fdocs', + ); + expect(new URL(result).searchParams.get('scopePath')).toEqual('docs'); + }); + }); +}); diff --git a/packages/integration/src/azure/core.ts b/packages/integration/src/azure/core.ts new file mode 100644 index 0000000000..44591f2509 --- /dev/null +++ b/packages/integration/src/azure/core.ts @@ -0,0 +1,131 @@ +/* + * 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 parseGitUrl from 'git-url-parse'; +import { AzureIntegrationConfig } from './config'; + +/** + * Given a URL pointing to a file on a provider, returns a URL that is suitable + * for fetching the contents of the data. + * + * Converts + * from: https://dev.azure.com/{organization}/{project}/_git/reponame?path={path}&version=GB{commitOrBranch}&_a=contents + * to: https://dev.azure.com/{organization}/{project}/_apis/git/repositories/reponame/items?path={path}&version={commitOrBranch} + * + * @param url A URL pointing to a file + */ +export function getAzureFileFetchUrl(url: string): string { + try { + const parsedUrl = new URL(url); + + const [ + empty, + userOrOrg, + project, + srcKeyword, + repoName, + ] = parsedUrl.pathname.split('/'); + + const path = parsedUrl.searchParams.get('path') || ''; + const ref = parsedUrl.searchParams.get('version')?.substr(2); + + if ( + parsedUrl.hostname !== 'dev.azure.com' || + empty !== '' || + userOrOrg === '' || + project === '' || + srcKeyword !== '_git' || + repoName === '' || + path === '' || + ref === '' + ) { + throw new Error('Wrong Azure Devops URL or Invalid file path'); + } + + // transform to api + parsedUrl.pathname = [ + empty, + userOrOrg, + project, + '_apis', + 'git', + 'repositories', + repoName, + 'items', + ].join('/'); + + const queryParams = [`path=${path}`]; + + if (ref) { + queryParams.push(`version=${ref}`); + } + + parsedUrl.search = queryParams.join('&'); + + parsedUrl.protocol = 'https'; + + return parsedUrl.toString(); + } catch (e) { + throw new Error(`Incorrect URL: ${url}, ${e}`); + } +} + +/** + * Given a URL pointing to a path on a provider, returns a URL that is suitable + * for downloading the subtree. + * + * @param url A URL pointing to a path + */ +export function getAzureDownloadUrl(url: string): string { + const { + name: repoName, + owner: project, + organization, + protocol, + resource, + filepath, + } = parseGitUrl(url); + + // scopePath will limit the downloaded content + // /docs will only download the docs folder and everything below it + // /docs/index.md will only download index.md but put it in the root of the archive + const scopePath = filepath + ? `&scopePath=${encodeURIComponent(filepath)}` + : ''; + + return `${protocol}://${resource}/${organization}/${project}/_apis/git/repositories/${repoName}/items?recursionLevel=full&download=true&api-version=6.0${scopePath}`; +} + +/** + * Gets the request options necessary to make requests to a given provider. + * + * @param config The relevant provider config + */ +export function getAzureRequestOptions( + config: AzureIntegrationConfig, + additionalHeaders?: Record, +): RequestInit { + const headers: HeadersInit = additionalHeaders + ? { ...additionalHeaders } + : {}; + + if (config.token) { + const buffer = Buffer.from(`:${config.token}`, 'utf8'); + headers.Authorization = `Basic ${buffer.toString('base64')}`; + } + + return { headers }; +} diff --git a/packages/integration/src/azure/index.ts b/packages/integration/src/azure/index.ts index ede0c88a81..365e4cdcdc 100644 --- a/packages/integration/src/azure/index.ts +++ b/packages/integration/src/azure/index.ts @@ -19,3 +19,8 @@ export { readAzureIntegrationConfigs, } from './config'; export type { AzureIntegrationConfig } from './config'; +export { + getAzureDownloadUrl, + getAzureFileFetchUrl, + getAzureRequestOptions, +} from './core'; diff --git a/packages/integration/src/bitbucket/core.test.ts b/packages/integration/src/bitbucket/core.test.ts new file mode 100644 index 0000000000..8d9f956c6b --- /dev/null +++ b/packages/integration/src/bitbucket/core.test.ts @@ -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 { BitbucketIntegrationConfig } from './config'; +import { getBitbucketFileFetchUrl, getBitbucketRequestOptions } from './core'; + +describe('bitbucket core', () => { + describe('getBitbucketRequestOptions', () => { + it('inserts a token when needed', () => { + const withToken: BitbucketIntegrationConfig = { + host: '', + apiBaseUrl: '', + token: 'A', + }; + const withoutToken: BitbucketIntegrationConfig = { + host: '', + apiBaseUrl: '', + }; + expect( + (getBitbucketRequestOptions(withToken).headers as any).Authorization, + ).toEqual('Bearer A'); + expect( + (getBitbucketRequestOptions(withoutToken).headers as any).Authorization, + ).toBeUndefined(); + }); + + it('insert basic auth when needed', () => { + const withUsernameAndPassword: BitbucketIntegrationConfig = { + host: '', + apiBaseUrl: '', + username: 'some-user', + appPassword: 'my-secret', + }; + const withoutUsernameAndPassword: BitbucketIntegrationConfig = { + host: '', + apiBaseUrl: '', + }; + expect( + (getBitbucketRequestOptions(withUsernameAndPassword).headers as any) + .Authorization, + ).toEqual('Basic c29tZS11c2VyOm15LXNlY3JldA=='); + expect( + (getBitbucketRequestOptions(withoutUsernameAndPassword).headers as any) + .Authorization, + ).toBeUndefined(); + }); + }); + + describe('getBitbucketFileFetchUrl', () => { + it('rejects targets that do not look like URLs', () => { + const config: BitbucketIntegrationConfig = { host: '', apiBaseUrl: '' }; + expect(() => getBitbucketFileFetchUrl('a/b', config)).toThrow( + /Incorrect URL: a\/b/, + ); + }); + + it('happy path for Bitbucket Cloud', () => { + const config: BitbucketIntegrationConfig = { + host: 'bitbucket.org', + apiBaseUrl: 'https://api.bitbucket.org/2.0', + }; + expect( + getBitbucketFileFetchUrl( + 'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml', + config, + ), + ).toEqual( + 'https://api.bitbucket.org/2.0/repositories/org-name/repo-name/src/master/templates/my-template.yaml', + ); + }); + + it('happy path for Bitbucket Server', () => { + const config: BitbucketIntegrationConfig = { + host: 'bitbucket.mycompany.net', + apiBaseUrl: 'https://bitbucket.mycompany.net/rest/api/1.0', + }; + expect( + getBitbucketFileFetchUrl( + 'https://bitbucket.mycompany.net/projects/a/repos/b/browse/path/to/c.yaml', + config, + ), + ).toEqual( + 'https://bitbucket.mycompany.net/rest/api/1.0/projects/a/repos/b/raw/path/to/c.yaml?at=', + ); + }); + }); +}); diff --git a/packages/integration/src/bitbucket/core.ts b/packages/integration/src/bitbucket/core.ts new file mode 100644 index 0000000000..a522e7ce9f --- /dev/null +++ b/packages/integration/src/bitbucket/core.ts @@ -0,0 +1,84 @@ +/* + * 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 parseGitUrl from 'git-url-parse'; +import { BitbucketIntegrationConfig } from './config'; + +/** + * Given a URL pointing to a file on a provider, returns a URL that is suitable + * for fetching the contents of the data. + * + * Converts + * from: https://bitbucket.org/orgname/reponame/src/master/file.yaml + * to: https://api.bitbucket.org/2.0/repositories/orgname/reponame/src/master/file.yaml + * + * @param url A URL pointing to a file + * @param config The relevant provider config + */ +export function getBitbucketFileFetchUrl( + url: string, + config: BitbucketIntegrationConfig, +): string { + try { + const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url); + if ( + !owner || + !name || + (filepathtype !== 'browse' && + filepathtype !== 'raw' && + filepathtype !== 'src') + ) { + throw new Error('Invalid Bitbucket URL or file path'); + } + + const pathWithoutSlash = filepath.replace(/^\//, ''); + + if (config.host === 'bitbucket.org') { + if (!ref) { + throw new Error('Invalid Bitbucket URL or file path'); + } + return `${config.apiBaseUrl}/repositories/${owner}/${name}/src/${ref}/${pathWithoutSlash}`; + } + return `${config.apiBaseUrl}/projects/${owner}/repos/${name}/raw/${pathWithoutSlash}?at=${ref}`; + } catch (e) { + throw new Error(`Incorrect URL: ${url}, ${e}`); + } +} + +/** + * Gets the request options necessary to make requests to a given provider. + * + * @param config The relevant provider config + */ +export function getBitbucketRequestOptions( + config: BitbucketIntegrationConfig, +): RequestInit { + const headers: HeadersInit = {}; + + if (config.token) { + headers.Authorization = `Bearer ${config.token}`; + } else if (config.username && config.appPassword) { + const buffer = Buffer.from( + `${config.username}:${config.appPassword}`, + 'utf8', + ); + headers.Authorization = `Basic ${buffer.toString('base64')}`; + } + + return { + headers, + }; +} diff --git a/packages/integration/src/bitbucket/index.ts b/packages/integration/src/bitbucket/index.ts index 897c00d160..b8d37220db 100644 --- a/packages/integration/src/bitbucket/index.ts +++ b/packages/integration/src/bitbucket/index.ts @@ -19,3 +19,4 @@ export { readBitbucketIntegrationConfigs, } from './config'; export type { BitbucketIntegrationConfig } from './config'; +export { getBitbucketFileFetchUrl, getBitbucketRequestOptions } from './core'; diff --git a/packages/integration/src/github/core.test.ts b/packages/integration/src/github/core.test.ts new file mode 100644 index 0000000000..03235acfbb --- /dev/null +++ b/packages/integration/src/github/core.test.ts @@ -0,0 +1,115 @@ +/* + * 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 { GitHubIntegrationConfig } from './config'; +import { getGitHubFileFetchUrl, getGitHubRequestOptions } from './core'; + +describe('github core', () => { + describe('getGitHubRequestOptions', () => { + it('inserts a token when needed', () => { + const withToken: GitHubIntegrationConfig = { + host: '', + rawBaseUrl: '', + token: 'A', + }; + const withoutToken: GitHubIntegrationConfig = { + host: '', + rawBaseUrl: '', + }; + expect( + (getGitHubRequestOptions(withToken).headers as any).Authorization, + ).toEqual('token A'); + expect( + (getGitHubRequestOptions(withoutToken).headers as any).Authorization, + ).toBeUndefined(); + }); + }); + + describe('getGitHubFileFetchUrl', () => { + it('rejects targets that do not look like URLs', () => { + const config: GitHubIntegrationConfig = { host: '', apiBaseUrl: '' }; + expect(() => getGitHubFileFetchUrl('a/b', config)).toThrow( + /Incorrect URL: a\/b/, + ); + }); + + it('happy path for github api', () => { + const config: GitHubIntegrationConfig = { + host: 'github.com', + apiBaseUrl: 'https://api.github.com', + }; + expect( + getGitHubFileFetchUrl( + 'https://github.com/a/b/blob/branchname/path/to/c.yaml', + config, + ), + ).toEqual( + 'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname', + ); + expect( + getGitHubFileFetchUrl( + 'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml', + config, + ), + ).toEqual( + 'https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname', + ); + }); + + it('happy path for ghe api', () => { + const config: GitHubIntegrationConfig = { + host: 'ghe.mycompany.net', + apiBaseUrl: 'https://ghe.mycompany.net/api/v3', + }; + expect( + getGitHubFileFetchUrl( + 'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml', + config, + ), + ).toEqual( + 'https://ghe.mycompany.net/api/v3/repos/a/b/contents/path/to/c.yaml?ref=branchname', + ); + }); + + it('happy path for github raw', () => { + const config: GitHubIntegrationConfig = { + host: 'github.com', + rawBaseUrl: 'https://raw.githubusercontent.com', + }; + expect( + getGitHubFileFetchUrl( + 'https://github.com/a/b/blob/branchname/path/to/c.yaml', + config, + ), + ).toEqual( + 'https://raw.githubusercontent.com/a/b/branchname/path/to/c.yaml', + ); + }); + + it('happy path for ghe raw', () => { + const config: GitHubIntegrationConfig = { + host: 'ghe.mycompany.net', + rawBaseUrl: 'https://ghe.mycompany.net/raw', + }; + expect( + getGitHubFileFetchUrl( + 'https://ghe.mycompany.net/a/b/blob/branchname/path/to/c.yaml', + config, + ), + ).toEqual('https://ghe.mycompany.net/raw/a/b/branchname/path/to/c.yaml'); + }); + }); +}); diff --git a/packages/integration/src/github/core.ts b/packages/integration/src/github/core.ts new file mode 100644 index 0000000000..239692e962 --- /dev/null +++ b/packages/integration/src/github/core.ts @@ -0,0 +1,82 @@ +/* + * 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 parseGitUrl from 'git-url-parse'; +import { GitHubIntegrationConfig } from './config'; + +/** + * Given a URL pointing to a file on a provider, returns a URL that is suitable + * for fetching the contents of the data. + * + * Converts + * from: https://github.com/a/b/blob/branchname/path/to/c.yaml + * to: https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname + * or: https://raw.githubusercontent.com/a/b/branchname/c.yaml + * + * @param url A URL pointing to a file + * @param config The relevant provider config + */ +export function getGitHubFileFetchUrl( + url: string, + config: GitHubIntegrationConfig, +): string { + try { + const { owner, name, ref, filepathtype, filepath } = parseGitUrl(url); + if ( + !owner || + !name || + !ref || + (filepathtype !== 'blob' && filepathtype !== 'raw') + ) { + throw new Error('Invalid GitHub URL or file path'); + } + + const pathWithoutSlash = filepath.replace(/^\//, ''); + if (chooseEndpoint(config) === 'api') { + return `${config.apiBaseUrl}/repos/${owner}/${name}/contents/${pathWithoutSlash}?ref=${ref}`; + } + return `${config.rawBaseUrl}/${owner}/${name}/${ref}/${pathWithoutSlash}`; + } catch (e) { + throw new Error(`Incorrect URL: ${url}, ${e}`); + } +} + +/** + * Gets the request options necessary to make requests to a given provider. + * + * @param config The relevant provider config + */ +export function getGitHubRequestOptions( + config: GitHubIntegrationConfig, +): RequestInit { + const headers: HeadersInit = {}; + + if (chooseEndpoint(config) === 'api') { + headers.Accept = 'application/vnd.github.v3.raw'; + } + if (config.token) { + headers.Authorization = `token ${config.token}`; + } + + return { headers }; +} + +export function chooseEndpoint(config: GitHubIntegrationConfig): 'api' | 'raw' { + if (config.apiBaseUrl && (config.token || !config.rawBaseUrl)) { + return 'api'; + } + return 'raw'; +} diff --git a/packages/integration/src/github/index.ts b/packages/integration/src/github/index.ts index 2099dd42e3..5f97f6980a 100644 --- a/packages/integration/src/github/index.ts +++ b/packages/integration/src/github/index.ts @@ -19,3 +19,4 @@ export { readGitHubIntegrationConfigs, } from './config'; export type { GitHubIntegrationConfig } from './config'; +export { getGitHubFileFetchUrl, getGitHubRequestOptions } from './core'; diff --git a/packages/integration/src/gitlab/core.test.ts b/packages/integration/src/gitlab/core.test.ts new file mode 100644 index 0000000000..43fea72e0b --- /dev/null +++ b/packages/integration/src/gitlab/core.test.ts @@ -0,0 +1,80 @@ +/* + * 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 { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { GitLabIntegrationConfig } from './config'; +import { getGitLabFileFetchUrl } from './core'; + +const worker = setupServer(); + +describe('gitlab core', () => { + beforeAll(() => worker.listen({ onUnhandledRequest: 'error' })); + afterAll(() => worker.close()); + afterEach(() => worker.resetHandlers()); + + beforeEach(() => { + worker.use( + rest.get('*/api/v4/projects/:name', (_, res, ctx) => + res(ctx.status(200), ctx.json({ id: 12345 })), + ), + ); + }); + + const configWithToken: GitLabIntegrationConfig = { + host: 'g.com', + token: '0123456789', + }; + + const configWithNoToken: GitLabIntegrationConfig = { + host: 'g.com', + }; + + describe('getGitLabFileFetchUrl', () => { + it.each([ + // Project URLs + { + config: configWithNoToken, + url: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', + result: + 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + }, + { + config: configWithToken, + url: + 'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', + result: + 'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + }, + { + config: configWithNoToken, + url: + 'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/to/file.yaml', // Repo not in subgroup + result: + 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + }, + // Raw URLs + { + config: configWithNoToken, + url: 'https://gitlab.example.com/a/b/blob/master/c.yaml', + result: 'https://gitlab.example.com/a/b/raw/master/c.yaml', + }, + ])('should handle happy path %#', async ({ config, url, result }) => { + await expect(getGitLabFileFetchUrl(url, config)).resolves.toBe(result); + }); + }); +}); diff --git a/packages/integration/src/gitlab/core.ts b/packages/integration/src/gitlab/core.ts new file mode 100644 index 0000000000..63dc3fdb3b --- /dev/null +++ b/packages/integration/src/gitlab/core.ts @@ -0,0 +1,157 @@ +/* + * 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 { GitLabIntegrationConfig } from './config'; +import fetch from 'cross-fetch'; + +/** + * Given a URL pointing to a file on a provider, returns a URL that is suitable + * for fetching the contents of the data. + * + * Converts + * from: https://gitlab.example.com/a/b/blob/master/c.yaml + * to: https://gitlab.example.com/a/b/raw/master/c.yaml + * -or- + * from: https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath + * to: https://gitlab.com/api/v4/projects/projectId/repository/files/filepath?ref=branch + * + * @param url A URL pointing to a file + * @param config The relevant provider config + */ +export async function getGitLabFileFetchUrl( + url: string, + config: GitLabIntegrationConfig, +): Promise { + // TODO(Rugvip): From the old GitlabReaderProcessor; used + // the existence of /-/blob/ to switch the logic. Don't know if this + // makes sense and it might require some more work. + if (url.includes('/-/blob/')) { + const projectID = await getProjectId(url, config); + return buildProjectUrl(url, projectID).toString(); + } + return buildRawUrl(url).toString(); +} + +/** + * Gets the request options necessary to make requests to a given provider. + * + * @param config The relevant provider config + */ +export function getGitLabRequestOptions( + config: GitLabIntegrationConfig, +): RequestInit { + const { token = '' } = config; + return { + headers: { + 'PRIVATE-TOKEN': token, + }, + }; +} + +// Converts +// from: https://gitlab.example.com/a/b/blob/master/c.yaml +// to: https://gitlab.example.com/a/b/raw/master/c.yaml +export function buildRawUrl(target: string): URL { + try { + const url = new URL(target); + + const [ + empty, + userOrOrg, + repoName, + blobKeyword, + ...restOfPath + ] = url.pathname.split('/'); + + if ( + empty !== '' || + userOrOrg === '' || + repoName === '' || + blobKeyword !== 'blob' || + !restOfPath.join('/').match(/\.yaml$/) + ) { + throw new Error('Wrong GitLab URL'); + } + + // Replace 'blob' with 'raw' + url.pathname = [empty, userOrOrg, repoName, 'raw', ...restOfPath].join('/'); + + return url; + } catch (e) { + throw new Error(`Incorrect url: ${target}, ${e}`); + } +} + +// Converts +// from: https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath +// to: https://gitlab.com/api/v4/projects/projectId/repository/files/filepath?ref=branch +export function buildProjectUrl(target: string, projectID: Number): URL { + try { + const url = new URL(target); + + const branchAndFilePath = url.pathname.split('/-/blob/')[1]; + const [branch, ...filePath] = branchAndFilePath.split('/'); + + url.pathname = [ + '/api/v4/projects', + projectID, + 'repository/files', + encodeURIComponent(filePath.join('/')), + 'raw', + ].join('/'); + url.search = `?ref=${branch}`; + + return url; + } catch (e) { + throw new Error(`Incorrect url: ${target}, ${e}`); + } +} + +// Convert +// from: https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath +// to: The project ID that corresponds to the URL +export async function getProjectId( + target: string, + config: GitLabIntegrationConfig, +): Promise { + const url = new URL(target); + + if (!url.pathname.includes('/-/blob/')) { + throw new Error('Please provide full path to yaml file from Gitlab'); + } + + try { + const repo = url.pathname.split('/-/blob/')[0]; + + // Convert + // to: https://gitlab.com/api/v4/projects/groupA%2Fteams%2FsubgroupA%2FteamA%2Frepo + const repoIDLookup = new URL( + `${url.protocol + url.hostname}/api/v4/projects/${encodeURIComponent( + repo.replace(/^\//, ''), + )}`, + ); + const response = await fetch( + repoIDLookup.toString(), + getGitLabRequestOptions(config), + ); + const projectIDJson = await response.json(); + const projectID = Number(projectIDJson.id); + + return projectID; + } catch (e) { + throw new Error(`Could not get GitLab project ID for: ${target}, ${e}`); + } +} diff --git a/packages/integration/src/gitlab/index.ts b/packages/integration/src/gitlab/index.ts index 0801914fd4..8dc4e90764 100644 --- a/packages/integration/src/gitlab/index.ts +++ b/packages/integration/src/gitlab/index.ts @@ -19,3 +19,4 @@ export { readGitLabIntegrationConfigs, } from './config'; export type { GitLabIntegrationConfig } from './config'; +export { getGitLabFileFetchUrl, getGitLabRequestOptions } from './core'; From 075d3dc5aedc0cb610afa003b92d1e54da28ed51 Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Mon, 7 Dec 2020 17:02:54 +0100 Subject: [PATCH 31/86] Refactor the Sentry plugin to use the proxy backend instead of a custom backend --- .changeset/chatty-pens-bathe.md | 65 ++++++++++ app-config.yaml | 7 ++ packages/backend/package.json | 1 - packages/backend/src/index.ts | 7 +- plugins/sentry-backend/README.md | 4 +- plugins/sentry-backend/src/index.ts | 11 +- plugins/sentry-backend/src/service/router.ts | 42 ------- .../sentry-backend/src/service/sentry-api.ts | 47 ------- .../src/service/standaloneApplication.ts | 42 ------- .../src/service/standaloneServer.ts | 42 ------- plugins/sentry/README.md | 119 ++++++++++++++++-- plugins/sentry/docs/sentry-card.png | Bin 0 -> 146025 bytes plugins/sentry/package.json | 12 +- .../sentry/src/api/index.ts | 11 +- .../SentryPluginPage => api/mock}/index.ts | 2 +- .../sentry/src/{data => api/mock}/mock-api.ts | 5 +- .../{data => api/mock}/sentry-issue-mock.json | 0 .../src/{data => api}/production-api.ts | 44 +++---- plugins/sentry/src/api/sentry-api.ts | 27 ++++ .../sentry/src/{data => api}/sentry-issue.ts | 1 + .../components/ErrorCell/ErrorCell.test.tsx | 3 +- .../src/components/ErrorCell/ErrorCell.tsx | 3 +- .../src/components/ErrorGraph/ErrorGraph.tsx | 3 +- plugins/sentry/src/components/Router.tsx | 18 +-- .../SentryIssuesTable.test.tsx | 5 +- .../SentryIssuesTable/SentryIssuesTable.tsx | 2 +- .../SentryIssuesWidget/SentryIssuesWidget.tsx | 84 +++++++++++++ .../SentryIssuesWidget/index.ts} | 5 +- .../SentryPluginPage.test.tsx | 58 --------- .../SentryPluginPage/SentryPluginPage.tsx | 69 ---------- .../SentryPluginWidget/SentryPluginWidget.tsx | 60 --------- .../src/components/index.ts} | 11 +- .../sentry/src/components/useProjectSlug.ts | 23 ++++ plugins/sentry/src/data/api-factory.ts | 28 ----- plugins/sentry/src/index.ts | 3 +- plugins/sentry/src/plugin.ts | 24 +++- 36 files changed, 406 insertions(+), 482 deletions(-) create mode 100644 .changeset/chatty-pens-bathe.md delete mode 100644 plugins/sentry-backend/src/service/router.ts delete mode 100644 plugins/sentry-backend/src/service/sentry-api.ts delete mode 100644 plugins/sentry-backend/src/service/standaloneApplication.ts delete mode 100644 plugins/sentry-backend/src/service/standaloneServer.ts create mode 100644 plugins/sentry/docs/sentry-card.png rename packages/backend/src/plugins/sentry.ts => plugins/sentry/src/api/index.ts (71%) rename plugins/sentry/src/{components/SentryPluginPage => api/mock}/index.ts (92%) rename plugins/sentry/src/{data => api/mock}/mock-api.ts (93%) rename plugins/sentry/src/{data => api/mock}/sentry-issue-mock.json (100%) rename plugins/sentry/src/{data => api}/production-api.ts (53%) create mode 100644 plugins/sentry/src/api/sentry-api.ts rename plugins/sentry/src/{data => api}/sentry-issue.ts (99%) create mode 100644 plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx rename plugins/sentry/src/{data/sentry-api.ts => components/SentryIssuesWidget/index.ts} (79%) delete mode 100644 plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.test.tsx delete mode 100644 plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.tsx delete mode 100644 plugins/sentry/src/components/SentryPluginWidget/SentryPluginWidget.tsx rename plugins/{sentry-backend/src/service/sentry-api.test.ts => sentry/src/components/index.ts} (66%) create mode 100644 plugins/sentry/src/components/useProjectSlug.ts delete mode 100644 plugins/sentry/src/data/api-factory.ts diff --git a/.changeset/chatty-pens-bathe.md b/.changeset/chatty-pens-bathe.md new file mode 100644 index 0000000000..4ca3378b2e --- /dev/null +++ b/.changeset/chatty-pens-bathe.md @@ -0,0 +1,65 @@ +--- +'@backstage/plugin-sentry': minor +'@backstage/plugin-sentry-backend': minor +--- + +The plugin uses the `proxy-backend` instead of a custom `sentry-backend`. +It requires a proxy configuration: + +`app-config.yaml`: + +```yaml +proxy: + '/sentry/api': + target: https://sentry.io/api/ + allowedMethods: ['GET'] + headers: + Authorization: + $env: SENTRY_TOKEN # export SENTRY_TOKEN="Bearer " +``` + +The `MockApiBackend` is no longer configured by the `NODE_ENV` variable. +Instead, the mock backend can be used with an api-override: + +`packages/app/src/apis.ts`: + +```ts +import { createApiFactory } from '@backstage/core'; +import { MockSentryApi, sentryApiRef } from '@backstage/plugin-sentry'; + +export const apis = [ + // ... + + createApiFactory(sentryApiRef, new MockSentryApi()), +]; +``` + +If you already use the Sentry backend, you must remove it from the backend: + +Delete `packages/backend/src/plugins/sentry.ts`. + +```diff +# packages/backend/package.json + +... + "@backstage/plugin-scaffolder-backend": "^0.3.2", +- "@backstage/plugin-sentry-backend": "^0.1.3", + "@backstage/plugin-techdocs-backend": "^0.3.0", +... +``` + +```diff +// packages/backend/src/index.html + + const apiRouter = Router(); + apiRouter.use('/catalog', await catalog(catalogEnv)); + apiRouter.use('/rollbar', await rollbar(rollbarEnv)); + apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); +- apiRouter.use('/sentry', await sentry(sentryEnv)); + apiRouter.use('/auth', await auth(authEnv)); + apiRouter.use('/techdocs', await techdocs(techdocsEnv)); + apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv)); + apiRouter.use('/proxy', await proxy(proxyEnv)); + apiRouter.use('/graphql', await graphql(graphqlEnv)); + apiRouter.use(notFoundHandler()); +``` diff --git a/app-config.yaml b/app-config.yaml index 54d564de7d..7c26176943 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -52,6 +52,13 @@ proxy: Authorization: $env: BUILDKITE_TOKEN + '/sentry/api': + target: https://sentry.io/api/ + allowedMethods: ['GET'] + headers: + Authorization: + $env: SENTRY_TOKEN + organization: name: My Company diff --git a/packages/backend/package.json b/packages/backend/package.json index 5937d6a6f3..8f328db285 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -29,7 +29,6 @@ "@backstage/plugin-proxy-backend": "^0.2.2", "@backstage/plugin-rollbar-backend": "^0.1.4", "@backstage/plugin-scaffolder-backend": "^0.3.3", - "@backstage/plugin-sentry-backend": "^0.1.3", "@backstage/plugin-techdocs-backend": "^0.3.1", "@gitbeaker/node": "^25.2.0", "@octokit/rest": "^18.0.0", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index b74954ecc5..68cc170901 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -25,13 +25,13 @@ import Router from 'express-promise-router'; import { createServiceBuilder, - loadBackendConfig, getRootLogger, - useHotMemoize, + loadBackendConfig, notFoundHandler, SingleConnectionDatabaseManager, SingleHostDiscovery, UrlReaders, + useHotMemoize, } from '@backstage/backend-common'; import { Config } from '@backstage/config'; import healthcheck from './plugins/healthcheck'; @@ -40,7 +40,6 @@ import catalog from './plugins/catalog'; import kubernetes from './plugins/kubernetes'; import rollbar from './plugins/rollbar'; import scaffolder from './plugins/scaffolder'; -import sentry from './plugins/sentry'; import proxy from './plugins/proxy'; import techdocs from './plugins/techdocs'; import graphql from './plugins/graphql'; @@ -76,7 +75,6 @@ async function main() { const authEnv = useHotMemoize(module, () => createEnv('auth')); const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); const rollbarEnv = useHotMemoize(module, () => createEnv('rollbar')); - const sentryEnv = useHotMemoize(module, () => createEnv('sentry')); const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs')); const kubernetesEnv = useHotMemoize(module, () => createEnv('kubernetes')); const graphqlEnv = useHotMemoize(module, () => createEnv('graphql')); @@ -86,7 +84,6 @@ async function main() { apiRouter.use('/catalog', await catalog(catalogEnv)); apiRouter.use('/rollbar', await rollbar(rollbarEnv)); apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); - apiRouter.use('/sentry', await sentry(sentryEnv)); apiRouter.use('/auth', await auth(authEnv)); apiRouter.use('/techdocs', await techdocs(techdocsEnv)); apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv)); diff --git a/plugins/sentry-backend/README.md b/plugins/sentry-backend/README.md index efd4c1b472..7a559276be 100644 --- a/plugins/sentry-backend/README.md +++ b/plugins/sentry-backend/README.md @@ -1,3 +1,5 @@ # sentry-backend -Simple plugin forwarding requests to [Sentry](https://sentry.io) API. +> DEPRECATED + +Please use the [proxy-backend](../proxy-backend) instead. See [CHANGELOG.md](./CHANGELOG.md). diff --git a/plugins/sentry-backend/src/index.ts b/plugins/sentry-backend/src/index.ts index 7612c392a2..6e1d12359e 100644 --- a/plugins/sentry-backend/src/index.ts +++ b/plugins/sentry-backend/src/index.ts @@ -14,4 +14,13 @@ * limitations under the License. */ -export * from './service/router'; +import { Router } from 'express'; +import { Logger } from 'winston'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const createRouter = async (_: Logger): Promise => Router(); + +throw new Error( + 'The sentry-backend has been deprecated and replaced by the proxy-backend. See the ' + + 'changelog on how to migrate to the proxy backend: https://github.com/backstage/backstage/blob/master/plugins/sentry/CHANGELOG.md.', +); diff --git a/plugins/sentry-backend/src/service/router.ts b/plugins/sentry-backend/src/service/router.ts deleted file mode 100644 index 153a8325b5..0000000000 --- a/plugins/sentry-backend/src/service/router.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 { Logger } from 'winston'; -import Router from 'express-promise-router'; -import express from 'express'; -import { getSentryApiForwarder } from './sentry-api'; - -export async function createRouter(logger: Logger): Promise { - const router = Router(); - router.use(express.json()); - - const SENTRY_TOKEN = process.env.SENTRY_TOKEN; - if (!SENTRY_TOKEN) { - if (process.env.NODE_ENV !== 'development') { - throw new Error( - 'Sentry token must be provided in SENTRY_TOKEN environment variable to start the API.', - ); - } - logger.warn( - 'Failed to initialize Sentry backend, set SENTRY_TOKEN environment variable to start the API.', - ); - } else { - const sentryForwarder = getSentryApiForwarder(SENTRY_TOKEN, logger); - - router.use(sentryForwarder); - } - - return router; -} diff --git a/plugins/sentry-backend/src/service/sentry-api.ts b/plugins/sentry-backend/src/service/sentry-api.ts deleted file mode 100644 index 8d35038840..0000000000 --- a/plugins/sentry-backend/src/service/sentry-api.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 express from 'express'; -import axios from 'axios'; -import { Logger } from 'winston'; - -export function getRequestHeaders(token: string) { - return { - headers: { - Authorization: `Bearer ${token}`, - }, - }; -} - -export function getSentryApiForwarder(token: string, logger: Logger) { - return function forwardRequest( - request: express.Request, - response: express.Response, - ) { - const sentryUrl = request.path; - const effectiveUrl = `https://sentry.io/${sentryUrl}`; - logger.info(`Calling Sentry REST API, ${effectiveUrl}`); - axios - .get(effectiveUrl, getRequestHeaders(token)) - .then(res => { - response.send(res.data); - }) - .catch(err => { - return response.status(err.response.status).json({ - detail: err.response.statusText, - }); - }); - }; -} diff --git a/plugins/sentry-backend/src/service/standaloneApplication.ts b/plugins/sentry-backend/src/service/standaloneApplication.ts deleted file mode 100644 index fdaad8bb2e..0000000000 --- a/plugins/sentry-backend/src/service/standaloneApplication.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 { - errorHandler, - notFoundHandler, - requestLoggingHandler, -} from '@backstage/backend-common'; -import cors from 'cors'; -import express from 'express'; -import helmet from 'helmet'; -import { Logger } from 'winston'; -import { createRouter } from './router'; - -export async function createStandaloneApplication( - logger: Logger, -): Promise { - const app = express(); - - app.use(helmet()); - app.use(cors()); - app.use(express.json()); - app.use(requestLoggingHandler()); - app.use('/', await createRouter(logger)); - app.use(notFoundHandler()); - app.use(errorHandler()); - - return app; -} diff --git a/plugins/sentry-backend/src/service/standaloneServer.ts b/plugins/sentry-backend/src/service/standaloneServer.ts deleted file mode 100644 index 37b87c5c40..0000000000 --- a/plugins/sentry-backend/src/service/standaloneServer.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 { Server } from 'http'; -import { Logger } from 'winston'; -import { createStandaloneApplication } from './standaloneApplication'; - -export async function startStandaloneServer( - parentLogger: Logger, -): Promise { - const logger = parentLogger.child({ service: 'scaffolder-backend' }); - logger.debug('Creating application...'); - - const app = await createStandaloneApplication(logger); - - logger.debug('Starting application server...'); - const PORT = parseInt(process.env.PORT || '5001', 10); - return await new Promise((resolve, reject) => { - const server = app.listen(PORT, (err?: Error) => { - if (err) { - reject(err); - return; - } - - logger.info(`Listening on port ${PORT}`); - resolve(server); - }); - }); -} diff --git a/plugins/sentry/README.md b/plugins/sentry/README.md index 73cca29d34..5d280de249 100644 --- a/plugins/sentry/README.md +++ b/plugins/sentry/README.md @@ -1,17 +1,116 @@ -# sentry +# Sentry Plugin -Welcome to the sentry plugin! +The Sentry Plugin displays issues from [Sentry](https://sentry.io). -_This plugin was created through the Backstage CLI_ +![Sentry Card](./docs/sentry-card.png) -## Getting started +## Getting Started -Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/sentry](http://localhost:3000/sentry). +1. Install the Sentry Plugin: -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. +```bash +# packages/app -Needs SENTRY_TOKEN set in the environment for the backend to startup +yarn add @backstage/plugin-sentry +``` -export SENTRY_TOKEN= +2. Add plugin to the app: + +```js +// packages/app/src/plugins.ts + +export { plugin as Sentry } from '@backstage/plugin-sentry'; +``` + +3. Add the `SentryIssuesWidget` to the EntityPage: + +```jsx +// packages/app/src/components/catalog/EntityPage.tsx + +import { SentryIssuesWidget } from '@backstage/plugin-sentry'; + +const OverviewContent = ({ entity }: { entity: Entity }) => ( + + // ... + + + + // ... + +); +``` + +> You can also import a `Router` if you want to have a dedicated sentry page: +> +> ```tsx +> // packages/app/src/components/catalog/EntityPage.tsx +> +> import { Router as SentryRouter } from '@backstage/plugin-sentry'; +> +> const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( +> +> // ... +> path="/sentry" +> title="Sentry" +> element={} +> /> +> // ... +> +> ); +> ``` + +4. Add the proxy config: + +```yaml +# app-config.yaml + +proxy: + '/sentry/api': + target: https://sentry.io/api/ + allowedMethods: ['GET'] + headers: + Authorization: + # Content: 'Bearer ' + $env: SENTRY_TOKEN + +sentry: + organization: +``` + +5. Create a new internal integration with the permissions `Issues & Events: Read` (https://docs.sentry.io/product/integrations/integration-platform/) and provide it as `SENTRY_TOKEN` as env variable. + +6. Add the `sentry.io/project-slug` annotation to your catalog-info.yaml file: + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage + description: | + Backstage is an open-source developer portal that puts the developer experience first. + annotations: + sentry.io/project-slug: YOUR_PROJECT_SLUG +spec: + type: library + owner: CNCF + lifecycle: experimental +``` + +### Demo Mode + +The plugin provides a MockAPI that always returns dummy data instead of talking to the sentry backend. +You can add it by overriding the `sentryApiRef`: + +```ts +// packages/app/src/apis.ts + +import { createApiFactory } from '@backstage/core'; +import { MockSentryApi, sentryApiRef } from '@backstage/plugin-sentry'; + +export const apis = [ + // ... + + createApiFactory(sentryApiRef, new MockSentryApi()), +]; +``` diff --git a/plugins/sentry/docs/sentry-card.png b/plugins/sentry/docs/sentry-card.png new file mode 100644 index 0000000000000000000000000000000000000000..30aad2a56ecfbb63ca02f58ac0532d3717e5e992 GIT binary patch literal 146025 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 literal 0 HcmV?d00001 diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index b952a81d51..c9708ee875 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -27,7 +27,6 @@ "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", - "@types/react": "^16.9", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", @@ -44,6 +43,7 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", + "@types/react": "^16.9", "cross-fetch": "^3.0.6", "msw": "^0.21.2" }, @@ -61,9 +61,15 @@ "organization": { "type": "string", "visibility": "frontend" - } + }, + "required": [ + "organization" + ] } } - } + }, + "required": [ + "sentry" + ] } } diff --git a/packages/backend/src/plugins/sentry.ts b/plugins/sentry/src/api/index.ts similarity index 71% rename from packages/backend/src/plugins/sentry.ts rename to plugins/sentry/src/api/index.ts index 5cd0e55761..4cccfdb7a1 100644 --- a/packages/backend/src/plugins/sentry.ts +++ b/plugins/sentry/src/api/index.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { createRouter } from '@backstage/plugin-sentry-backend'; -import type { PluginEnvironment } from '../types'; - -export default async function createPlugin({ logger }: PluginEnvironment) { - return await createRouter(logger); -} +export * from './mock'; +export type { SentryApi } from './sentry-api'; +export { sentryApiRef } from './sentry-api'; +export type { SentryIssue } from './sentry-issue'; +export { ProductionSentryApi } from './production-api'; diff --git a/plugins/sentry/src/components/SentryPluginPage/index.ts b/plugins/sentry/src/api/mock/index.ts similarity index 92% rename from plugins/sentry/src/components/SentryPluginPage/index.ts rename to plugins/sentry/src/api/mock/index.ts index 67b34db517..b65fb7a919 100644 --- a/plugins/sentry/src/components/SentryPluginPage/index.ts +++ b/plugins/sentry/src/api/mock/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { default } from './SentryPluginPage'; +export { MockSentryApi } from './mock-api'; diff --git a/plugins/sentry/src/data/mock-api.ts b/plugins/sentry/src/api/mock/mock-api.ts similarity index 93% rename from plugins/sentry/src/data/mock-api.ts rename to plugins/sentry/src/api/mock/mock-api.ts index 8026fec4ee..6743cee79f 100644 --- a/plugins/sentry/src/data/mock-api.ts +++ b/plugins/sentry/src/api/mock/mock-api.ts @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { SentryIssue } from './sentry-issue'; -import { SentryApi } from './sentry-api'; + +import { SentryIssue } from '../sentry-issue'; +import { SentryApi } from '../sentry-api'; import mockData from './sentry-issue-mock.json'; function getMockIssue(): SentryIssue { diff --git a/plugins/sentry/src/data/sentry-issue-mock.json b/plugins/sentry/src/api/mock/sentry-issue-mock.json similarity index 100% rename from plugins/sentry/src/data/sentry-issue-mock.json rename to plugins/sentry/src/api/mock/sentry-issue-mock.json diff --git a/plugins/sentry/src/data/production-api.ts b/plugins/sentry/src/api/production-api.ts similarity index 53% rename from plugins/sentry/src/data/production-api.ts rename to plugins/sentry/src/api/production-api.ts index 5d21e80604..bf6fa98bce 100644 --- a/plugins/sentry/src/data/production-api.ts +++ b/plugins/sentry/src/api/production-api.ts @@ -13,36 +13,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { SentryIssue } from './sentry-issue'; import { SentryApi } from './sentry-api'; +import { DiscoveryApi } from '@backstage/core'; export class ProductionSentryApi implements SentryApi { - private organization: string; - private backendBaseUrl: string; - - constructor(organization: string, backendBaseUrl: string) { - this.organization = organization; - this.backendBaseUrl = backendBaseUrl; - } + constructor( + private readonly discoveryApi: DiscoveryApi, + private readonly organization: string, + ) {} async fetchIssues(project: string, statsFor: string): Promise { - try { - const apiBaseUrl = `${this.backendBaseUrl}/sentry/api/0/projects/`; - - const response = await fetch( - `${apiBaseUrl}/${this.organization}/${project}/issues/?statsFor=${statsFor}`, - ); - - if (response.status >= 400 && response.status < 600) { - throw new Error('Failed fetching Sentry issues'); - } - - return (await response.json()) as SentryIssue[]; - } catch (exception) { - if (exception.detail) { - return exception; - } - throw new Error('Unknown error'); + if (!project) { + return []; } + + const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/sentry/api`; + + const response = await fetch( + `${apiUrl}/0/projects/${this.organization}/${project}/issues/?statsFor=${statsFor}`, + ); + + if (response.status >= 400 && response.status < 600) { + throw new Error('Failed fetching Sentry issues'); + } + + return (await response.json()) as SentryIssue[]; } } diff --git a/plugins/sentry/src/api/sentry-api.ts b/plugins/sentry/src/api/sentry-api.ts new file mode 100644 index 0000000000..1edb550ec4 --- /dev/null +++ b/plugins/sentry/src/api/sentry-api.ts @@ -0,0 +1,27 @@ +/* + * 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 { SentryIssue } from './sentry-issue'; +import { createApiRef } from '@backstage/core'; + +export const sentryApiRef = createApiRef({ + id: 'plugin.sentry.service', + description: 'Used by the Sentry plugin to make requests', +}); + +export interface SentryApi { + fetchIssues(project: string, statsFor: string): Promise; +} diff --git a/plugins/sentry/src/data/sentry-issue.ts b/plugins/sentry/src/api/sentry-issue.ts similarity index 99% rename from plugins/sentry/src/data/sentry-issue.ts rename to plugins/sentry/src/api/sentry-issue.ts index 14621bf629..017396229d 100644 --- a/plugins/sentry/src/data/sentry-issue.ts +++ b/plugins/sentry/src/api/sentry-issue.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + type SentryPlatform = 'javascript' | 'javascript-react' | string; type EventPoint = number[]; diff --git a/plugins/sentry/src/components/ErrorCell/ErrorCell.test.tsx b/plugins/sentry/src/components/ErrorCell/ErrorCell.test.tsx index 74f1501225..b02c2860ad 100644 --- a/plugins/sentry/src/components/ErrorCell/ErrorCell.test.tsx +++ b/plugins/sentry/src/components/ErrorCell/ErrorCell.test.tsx @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { ErrorCell } from './ErrorCell'; import React from 'react'; import { render } from '@testing-library/react'; -import mockIssue from '../../data/sentry-issue-mock.json'; +import mockIssue from '../../api/mock/sentry-issue-mock.json'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; diff --git a/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx b/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx index 617bdb3ce9..022453a7a1 100644 --- a/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx +++ b/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React, { FC } from 'react'; -import { SentryIssue } from '../../data/sentry-issue'; +import { SentryIssue } from '../../api'; import { Link, Typography } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; import { BackstageTheme } from '@backstage/theme'; diff --git a/plugins/sentry/src/components/ErrorGraph/ErrorGraph.tsx b/plugins/sentry/src/components/ErrorGraph/ErrorGraph.tsx index 7643c5ac6e..c4226780e4 100644 --- a/plugins/sentry/src/components/ErrorGraph/ErrorGraph.tsx +++ b/plugins/sentry/src/components/ErrorGraph/ErrorGraph.tsx @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React, { FC } from 'react'; -import { SentryIssue } from '../../data/sentry-issue'; +import { SentryIssue } from '../../api'; import { Sparklines, SparklinesBars } from 'react-sparklines'; export const ErrorGraph: FC<{ sentryIssue: SentryIssue }> = ({ diff --git a/plugins/sentry/src/components/Router.tsx b/plugins/sentry/src/components/Router.tsx index 6d15268629..7bed3700ab 100644 --- a/plugins/sentry/src/components/Router.tsx +++ b/plugins/sentry/src/components/Router.tsx @@ -13,28 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { Entity } from '@backstage/catalog-model'; -import { Routes, Route } from 'react-router'; -import { MissingAnnotationEmptyState } from '@backstage/core'; -import { SentryPluginWidget } from './SentryPluginWidget/SentryPluginWidget'; - -const SENTRY_ANNOTATION = 'sentry.io/project-slug'; +import { Route, Routes } from 'react-router'; +import { SentryIssuesWidget } from './SentryIssuesWidget'; export const Router = ({ entity }: { entity: Entity }) => { - const projectId = entity.metadata.annotations?.[SENTRY_ANNOTATION]; - - if (!projectId) { - return ; - } - return ( - } + element={} /> ) diff --git a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.test.tsx b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.test.tsx index 6926fdfc2a..441104c0ee 100644 --- a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.test.tsx +++ b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.test.tsx @@ -13,11 +13,12 @@ * 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 SentryIssuesTable from './SentryIssuesTable'; -import { SentryIssue } from '../../data/sentry-issue'; -import mockIssue from '../../data/sentry-issue-mock.json'; +import { SentryIssue } from '../../api'; +import mockIssue from '../../api/mock/sentry-issue-mock.json'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; diff --git a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx index aae8009d98..72081bf2da 100644 --- a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx +++ b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { Table, TableColumn } from '@backstage/core'; -import { SentryIssue } from '../../data/sentry-issue'; +import { SentryIssue } from '../../api'; import { format } from 'timeago.js'; import { ErrorCell } from '../ErrorCell/ErrorCell'; import { ErrorGraph } from '../ErrorGraph/ErrorGraph'; diff --git a/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx b/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx new file mode 100644 index 0000000000..c52b76c4bc --- /dev/null +++ b/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx @@ -0,0 +1,84 @@ +/* + * 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, { useEffect } from 'react'; +import { + EmptyState, + ErrorApi, + errorApiRef, + InfoCard, + MissingAnnotationEmptyState, + Progress, + useApi, +} from '@backstage/core'; +import SentryIssuesTable from '../SentryIssuesTable/SentryIssuesTable'; +import { useAsync } from 'react-use'; +import { sentryApiRef } from '../../api'; +import { + SENTRY_PROJECT_SLUG_ANNOTATION, + useProjectSlug, +} from '../useProjectSlug'; +import { Entity } from '@backstage/catalog-model'; + +export const SentryIssuesWidget = ({ + entity, + statsFor = '24h', + variant = 'gridItem', +}: { + entity: Entity; + statsFor?: '24h' | '12h'; + variant?: string; +}) => { + const errorApi = useApi(errorApiRef); + const sentryApi = useApi(sentryApiRef); + + const projectId = useProjectSlug(entity); + + const { loading, value, error } = useAsync( + () => sentryApi.fetchIssues(projectId, statsFor), + [sentryApi, statsFor, projectId], + ); + + useEffect(() => { + if (error) { + errorApi.post(error); + } + }, [error, errorApi]); + + if (loading || !projectId || error) { + return ( + + {loading && } + + {!loading && !projectId && ( + + )} + + {!loading && error && ( + + )} + + ); + } + + return ; +}; diff --git a/plugins/sentry/src/data/sentry-api.ts b/plugins/sentry/src/components/SentryIssuesWidget/index.ts similarity index 79% rename from plugins/sentry/src/data/sentry-api.ts rename to plugins/sentry/src/components/SentryIssuesWidget/index.ts index 538900b738..fddc1374f2 100644 --- a/plugins/sentry/src/data/sentry-api.ts +++ b/plugins/sentry/src/components/SentryIssuesWidget/index.ts @@ -13,8 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { SentryIssue } from './sentry-issue'; -export interface SentryApi { - fetchIssues(project: string, statsFor: string): Promise; -} +export { SentryIssuesWidget } from './SentryIssuesWidget'; diff --git a/plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.test.tsx b/plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.test.tsx deleted file mode 100644 index 21c21ce385..0000000000 --- a/plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.test.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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 SentryPluginPage from './SentryPluginPage'; -import { ThemeProvider } from '@material-ui/core'; -import { lightTheme } from '@backstage/theme'; -import { msw } from '@backstage/test-utils'; -import { setupServer } from 'msw/node'; -import { rest } from 'msw'; - -import { - ApiProvider, - ApiRegistry, - errorApiRef, - configApiRef, -} from '@backstage/core'; - -const errorApi = { post: () => {} }; -const ConfigApi = { getString: () => 'test' }; - -describe('SentryPluginPage', () => { - const server = setupServer(); - msw.setupDefaultHandlers(server); - - it('should render header and time switched', () => { - server.use(rest.get('/', (_req, res, ctx) => res(ctx.json({})))); - const rendered = render( - - - - - , - ); - expect(rendered.getByText('Sentry issues')).toBeInTheDocument(); - expect(rendered.getByText('24H')).toBeInTheDocument(); - expect(rendered.getByText('12H')).toBeInTheDocument(); - }); -}); diff --git a/plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.tsx b/plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.tsx deleted file mode 100644 index 763a8d753f..0000000000 --- a/plugins/sentry/src/components/SentryPluginPage/SentryPluginPage.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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, useState } from 'react'; -import { Grid } from '@material-ui/core'; -import { - Header, - Page, - Content, - ContentHeader, - SupportButton, -} from '@backstage/core'; -import { SentryPluginWidget } from '../SentryPluginWidget/SentryPluginWidget'; -import { ToggleButton, ToggleButtonGroup } from '@material-ui/lab'; - -const SentryPluginPage: FC<{}> = () => { - const [statsFor, setStatsFor] = useState<'12h' | '24h'>('12h'); - const toggleStatsFor = () => setStatsFor(statsFor === '12h' ? '12h' : '24h'); - const sentryProjectId = 'sample-sentry-project-id'; - - return ( - -
- - - - - 24H - - - 12H - - - - Sentry plugin allows you to preview issues and navigate to sentry. - - - - - - - - - - ); -}; - -export default SentryPluginPage; diff --git a/plugins/sentry/src/components/SentryPluginWidget/SentryPluginWidget.tsx b/plugins/sentry/src/components/SentryPluginWidget/SentryPluginWidget.tsx deleted file mode 100644 index 8d522df4a7..0000000000 --- a/plugins/sentry/src/components/SentryPluginWidget/SentryPluginWidget.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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, useEffect } from 'react'; -import { - ErrorApi, - errorApiRef, - InfoCard, - Progress, - useApi, - configApiRef, -} from '@backstage/core'; -import SentryIssuesTable from '../SentryIssuesTable/SentryIssuesTable'; -import { useAsync } from 'react-use'; -import { sentryApiFactory } from '../../data/api-factory'; - -export const SentryPluginWidget: FC<{ - sentryProjectId: string; - statsFor: '24h' | '12h'; -}> = ({ sentryProjectId, statsFor }) => { - const errorApi = useApi(errorApiRef); - const configApi = useApi(configApiRef); - const org = configApi.getString('sentry.organization'); - const backendBaseUrl = configApi.getString('backend.baseUrl'); - const api = sentryApiFactory(org, backendBaseUrl); - - const { loading, value, error } = useAsync( - () => api.fetchIssues(sentryProjectId, statsFor), - [statsFor, sentryProjectId], - ); - - useEffect(() => { - if (error) { - errorApi.post(error); - } - }, [error, errorApi]); - - if (loading) { - return ( - - - - ); - } - - return ; -}; diff --git a/plugins/sentry-backend/src/service/sentry-api.test.ts b/plugins/sentry/src/components/index.ts similarity index 66% rename from plugins/sentry-backend/src/service/sentry-api.test.ts rename to plugins/sentry/src/components/index.ts index f15692861f..b1588954c9 100644 --- a/plugins/sentry-backend/src/service/sentry-api.test.ts +++ b/plugins/sentry/src/components/index.ts @@ -13,14 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { getRequestHeaders } from './sentry-api'; -describe('SentryApiForwarder', () => { - it('should generate headers based on token passed in constructor', () => { - expect(getRequestHeaders('testtoken')).toEqual({ - headers: { - Authorization: `Bearer testtoken`, - }, - }); - }); -}); +export * from './SentryIssuesWidget'; diff --git a/plugins/sentry/src/components/useProjectSlug.ts b/plugins/sentry/src/components/useProjectSlug.ts new file mode 100644 index 0000000000..072d517b05 --- /dev/null +++ b/plugins/sentry/src/components/useProjectSlug.ts @@ -0,0 +1,23 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; + +export const SENTRY_PROJECT_SLUG_ANNOTATION = 'sentry.io/project-slug'; + +export const useProjectSlug = (entity: Entity) => { + return entity?.metadata.annotations?.[SENTRY_PROJECT_SLUG_ANNOTATION] ?? ''; +}; diff --git a/plugins/sentry/src/data/api-factory.ts b/plugins/sentry/src/data/api-factory.ts deleted file mode 100644 index 88e1148b72..0000000000 --- a/plugins/sentry/src/data/api-factory.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 { SentryApi } from './sentry-api'; -import { MockSentryApi } from './mock-api'; -import { ProductionSentryApi } from './production-api'; - -export function sentryApiFactory( - organization: string, - backendBaseUrl: string, -): SentryApi { - if (process.env.NODE_ENV === 'production') { - return new ProductionSentryApi(organization, backendBaseUrl); - } - return new MockSentryApi(); -} diff --git a/plugins/sentry/src/index.ts b/plugins/sentry/src/index.ts index a0d3cab1be..2b9e2186ec 100644 --- a/plugins/sentry/src/index.ts +++ b/plugins/sentry/src/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export * from './api'; +export * from './components'; export { plugin } from './plugin'; export { Router } from './components/Router'; -export { SentryPluginWidget as SentryIssuesWidget } from './components/SentryPluginWidget/SentryPluginWidget'; diff --git a/plugins/sentry/src/plugin.ts b/plugins/sentry/src/plugin.ts index fc4b1af5e0..6a4fe4e5a4 100644 --- a/plugins/sentry/src/plugin.ts +++ b/plugins/sentry/src/plugin.ts @@ -14,8 +14,14 @@ * limitations under the License. */ -import { createPlugin, createRouteRef } from '@backstage/core'; -import SentryPluginPage from './components/SentryPluginPage'; +import { + configApiRef, + createApiFactory, + createPlugin, + createRouteRef, + discoveryApiRef, +} from '@backstage/core'; +import { ProductionSentryApi, sentryApiRef } from './api'; export const rootRouteRef = createRouteRef({ path: '/sentry', @@ -24,7 +30,15 @@ export const rootRouteRef = createRouteRef({ export const plugin = createPlugin({ id: 'sentry', - register({ router }) { - router.addRoute(rootRouteRef, SentryPluginPage); - }, + apis: [ + createApiFactory({ + api: sentryApiRef, + deps: { configApi: configApiRef, discoveryApi: discoveryApiRef }, + factory: ({ configApi, discoveryApi }) => + new ProductionSentryApi( + discoveryApi, + configApi.getString('sentry.organization'), + ), + }), + ], }); From 0117081027bea41fe5913432a1780c31978ab84d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 7 Dec 2020 17:29:29 +0100 Subject: [PATCH 32/86] cli: fix for node_modules inside local packages being transformed --- .changeset/fast-frogs-wave.md | 5 +++++ packages/cli/src/lib/bundler/transforms.ts | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 .changeset/fast-frogs-wave.md diff --git a/.changeset/fast-frogs-wave.md b/.changeset/fast-frogs-wave.md new file mode 100644 index 0000000000..7dfe33aa70 --- /dev/null +++ b/.changeset/fast-frogs-wave.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fixes a big in the bundling logic that caused `node_modules` inside local monorepo packages to be transformed. diff --git a/packages/cli/src/lib/bundler/transforms.ts b/packages/cli/src/lib/bundler/transforms.ts index 6dc32e6563..19f1acff77 100644 --- a/packages/cli/src/lib/bundler/transforms.ts +++ b/packages/cli/src/lib/bundler/transforms.ts @@ -35,7 +35,12 @@ export const transforms = (options: TransformOptions): Transforms => { const extraTransforms = isDev ? ['react-hot-loader'] : []; const transformExcludeCondition = { - and: [/node_modules/, { not: externalTransforms }], + or: [ + // This makes sure we don't transform node_modules inside any of the local monorepo packages + /node_modules.*node_modules/, + // This excludes the local monorepo packages from the excludes, meaning they will be transformed + { and: [/node_modules/, { not: externalTransforms }] }, + ], }; const loaders = [ From e750f086379de5191009fa52a60d60d8fb3be676 Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Mon, 7 Dec 2020 17:38:34 +0100 Subject: [PATCH 33/86] Don't run tests in the deprecated module --- plugins/sentry-backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/sentry-backend/package.json b/plugins/sentry-backend/package.json index 5f5eafd5c6..7caee23b68 100644 --- a/plugins/sentry-backend/package.json +++ b/plugins/sentry-backend/package.json @@ -14,7 +14,7 @@ "start": "backstage-cli backend:dev", "build": "backstage-cli backend:build", "lint": "backstage-cli lint", - "test": "backstage-cli test", + "test": "backstage-cli test --passWithNoTests", "prepack": "backstage-cli prepack", "postpack": "backstage-cli postpack", "clean": "backstage-cli clean" From 5596e8698e64ed59dbe0237a4e0b41c67e6f524d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 27 Nov 2020 14:47:28 +0100 Subject: [PATCH 34/86] core-api: add basic useRouteRef and RouteResolver without support for params Co-authored-by: blam --- packages/core-api/src/routing/hooks.test.tsx | 87 ++++++++++++++++++++ packages/core-api/src/routing/hooks.tsx | 75 +++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 packages/core-api/src/routing/hooks.test.tsx create mode 100644 packages/core-api/src/routing/hooks.tsx diff --git a/packages/core-api/src/routing/hooks.test.tsx b/packages/core-api/src/routing/hooks.test.tsx new file mode 100644 index 0000000000..c08891c8cc --- /dev/null +++ b/packages/core-api/src/routing/hooks.test.tsx @@ -0,0 +1,87 @@ +/* + * 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 { render } from '@testing-library/react'; +import React, { PropsWithChildren } from 'react'; +import { MemoryRouter, Routes } from 'react-router-dom'; +import { createRoutableExtension } from '../extensions'; +import { + childDiscoverer, + routeElementDiscoverer, + traverseElementTree, +} from '../extensions/traversal'; +import { createPlugin } from '../plugin'; +import { routeCollector, routeParentCollector } from './collectors'; +import { useRouteRef, RoutingProvider } from './hooks'; +import { createRouteRef } from './RouteRef'; + +const mockConfig = () => ({ path: '/unused', title: 'Unused' }); +const MockComponent = ({ children }: PropsWithChildren<{}>) => <>{children}; + +const plugin = createPlugin({ id: 'my-plugin' }); + +const ref1 = createRouteRef(mockConfig()); +const ref2 = createRouteRef(mockConfig()); +const ref3 = createRouteRef(mockConfig()); + +const MockRouteSource = () => { + const routeFunc = useRouteRef(ref2); + expect(routeFunc?.()).toBe('/foo/bar'); + return null; +}; + +const Extension1 = plugin.provide( + createRoutableExtension({ component: MockComponent, mountPoint: ref1 }), +); +const Extension2 = plugin.provide( + createRoutableExtension({ component: MockRouteSource, mountPoint: ref2 }), +); +const Extension3 = plugin.provide( + createRoutableExtension({ component: MockComponent, mountPoint: ref3 }), +); + +describe('discovery', () => { + it('should handle all react router Route patterns', () => { + const root = ( + + + + + + + + + + ); + + const { routes, routeParents } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routes: routeCollector, + routeParents: routeParentCollector, + }, + }); + + render( + + {root} + , + ); + + expect.assertions(2); + }); +}); diff --git a/packages/core-api/src/routing/hooks.tsx b/packages/core-api/src/routing/hooks.tsx new file mode 100644 index 0000000000..8a2aa72c92 --- /dev/null +++ b/packages/core-api/src/routing/hooks.tsx @@ -0,0 +1,75 @@ +/* + * 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, { createContext, ReactNode, useContext } from 'react'; +import { RouteRef } from './types'; + +export type RouteFunc = () => string; + +class RouteResolver { + constructor( + private readonly routes: Map, + private readonly routeParents: Map, + ) {} + + resolve(routeRef: RouteRef): RouteFunc { + let currentRouteRef: RouteRef | undefined = routeRef; + let fullPath = ''; + + while (currentRouteRef) { + const path = this.routes.get(currentRouteRef); + if (!path) { + throw new Error(`No path for ${currentRouteRef}`); + } + fullPath = `${path}${fullPath}`; + currentRouteRef = this.routeParents.get(currentRouteRef); + } + + return () => { + return fullPath; + }; + } +} + +const RoutingContext = createContext(undefined); + +export function useRouteRef(routeRef: RouteRef): RouteFunc | undefined { + const resolver = useContext(RoutingContext); + if (!resolver) { + throw new Error('No route resolver found in context'); + } + + return resolver.resolve(routeRef); +} + +type ProviderProps = { + routes: Map; + routeParents: Map; + children: ReactNode; +}; + +export const RoutingProvider = ({ + routes, + routeParents, + children, +}: ProviderProps) => { + const resolver = new RouteResolver(routes, routeParents); + return ( + + {children} + + ); +}; From 57ce2b2dc5adc04fb5d507e1bef683a7d58fc3fd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 27 Nov 2020 15:49:33 +0100 Subject: [PATCH 35/86] core-api: fix accidental global singletons in collectors Co-authored-by: blam --- .../src/extensions/traversal.test.tsx | 26 ++++++++++++------- packages/core-api/src/extensions/traversal.ts | 4 +-- packages/core-api/src/plugin/collectors.ts | 2 +- packages/core-api/src/routing/collectors.tsx | 4 +-- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/core-api/src/extensions/traversal.test.tsx b/packages/core-api/src/extensions/traversal.test.tsx index a69ee9de77..38571fdcc7 100644 --- a/packages/core-api/src/extensions/traversal.test.tsx +++ b/packages/core-api/src/extensions/traversal.test.tsx @@ -41,11 +41,14 @@ describe('discovery', () => { root, discoverers: [childDiscoverer], collectors: { - names: createCollector(Array(), (acc, el) => { - if (typeof el.type === 'string') { - acc.push(el.type); - } - }), + names: createCollector( + () => Array(), + (acc, el) => { + if (typeof el.type === 'string') { + acc.push(el.type); + } + }, + ), }, }); @@ -85,11 +88,14 @@ describe('discovery', () => { ), ], collectors: { - names: createCollector(Array(), (acc, el) => { - if (typeof el.type === 'string') { - acc.push(el.type); - } - }), + names: createCollector( + () => Array(), + (acc, el) => { + if (typeof el.type === 'string') { + acc.push(el.type); + } + }, + ), }, }); diff --git a/packages/core-api/src/extensions/traversal.ts b/packages/core-api/src/extensions/traversal.ts index a1fbdab25a..054f23daca 100644 --- a/packages/core-api/src/extensions/traversal.ts +++ b/packages/core-api/src/extensions/traversal.ts @@ -120,10 +120,10 @@ export function traverseElementTree(options: { } export function createCollector( - initialResult: Result, + accumulatorFactory: () => Result, visit: ReturnType>['visit'], ): Collector { - return () => ({ accumulator: initialResult, visit }); + return () => ({ accumulator: accumulatorFactory(), visit }); } export function childDiscoverer(element: ReactElement): ReactNode { diff --git a/packages/core-api/src/plugin/collectors.ts b/packages/core-api/src/plugin/collectors.ts index a222746f9e..3eadfc0185 100644 --- a/packages/core-api/src/plugin/collectors.ts +++ b/packages/core-api/src/plugin/collectors.ts @@ -34,7 +34,7 @@ import { getComponentData } from '../extensions'; import { createCollector } from '../extensions/traversal'; export const pluginCollector = createCollector( - new Set(), + () => new Set(), (acc, node) => { const plugin = getComponentData(node, 'core.plugin'); if (plugin) { diff --git a/packages/core-api/src/routing/collectors.tsx b/packages/core-api/src/routing/collectors.tsx index fab0cbe112..34edd8a0d5 100644 --- a/packages/core-api/src/routing/collectors.tsx +++ b/packages/core-api/src/routing/collectors.tsx @@ -20,7 +20,7 @@ import { getComponentData } from '../extensions'; import { createCollector } from '../extensions/traversal'; export const routeCollector = createCollector( - new Map(), + () => new Map(), (acc, node, parent) => { if (parent.props.element === node) { return; @@ -51,7 +51,7 @@ export const routeCollector = createCollector( ); export const routeParentCollector = createCollector( - new Map(), + () => new Map(), (acc, node, parent, parentRouteRef?: RouteRef) => { if (parent.props.element === node) { return parentRouteRef; From cf2855dc9e1155ec2478e2d5d10f6f1b1662c1a6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 27 Nov 2020 15:50:36 +0100 Subject: [PATCH 36/86] core-api: add initial support for route parameters with useRouteRef Co-authored-by: blam --- packages/core-api/src/routing/hooks.test.tsx | 75 +++++++++++++++++--- packages/core-api/src/routing/hooks.tsx | 7 +- 2 files changed, 70 insertions(+), 12 deletions(-) diff --git a/packages/core-api/src/routing/hooks.test.tsx b/packages/core-api/src/routing/hooks.test.tsx index c08891c8cc..65c7858954 100644 --- a/packages/core-api/src/routing/hooks.test.tsx +++ b/packages/core-api/src/routing/hooks.test.tsx @@ -27,6 +27,7 @@ import { createPlugin } from '../plugin'; import { routeCollector, routeParentCollector } from './collectors'; import { useRouteRef, RoutingProvider } from './hooks'; import { createRouteRef } from './RouteRef'; +import { RouteRef } from './types'; const mockConfig = () => ({ path: '/unused', title: 'Unused' }); const MockComponent = ({ children }: PropsWithChildren<{}>) => <>{children}; @@ -36,11 +37,19 @@ const plugin = createPlugin({ id: 'my-plugin' }); const ref1 = createRouteRef(mockConfig()); const ref2 = createRouteRef(mockConfig()); const ref3 = createRouteRef(mockConfig()); +const ref4 = createRouteRef(mockConfig()); -const MockRouteSource = () => { - const routeFunc = useRouteRef(ref2); - expect(routeFunc?.()).toBe('/foo/bar'); - return null; +const MockRouteSource = (props: { + name: string; + routeRef: RouteRef; + params?: Record; +}) => { + const routeFunc = useRouteRef(props.routeRef); + return ( +
+ Path at {props.name}: {routeFunc?.(props.params)} +
+ ); }; const Extension1 = plugin.provide( @@ -52,18 +61,21 @@ const Extension2 = plugin.provide( const Extension3 = plugin.provide( createRoutableExtension({ component: MockComponent, mountPoint: ref3 }), ); +const Extension4 = plugin.provide( + createRoutableExtension({ component: MockRouteSource, mountPoint: ref4 }), +); describe('discovery', () => { - it('should handle all react router Route patterns', () => { + it('should handle simple routeRef path creation for routeRefs used in other parts of the app', () => { const root = ( - + - + ); @@ -76,12 +88,57 @@ describe('discovery', () => { }, }); - render( + const rendered = render( {root} , ); - expect.assertions(2); + expect(rendered.getByText('Path at inside: /foo/bar')).toBeInTheDocument(); + expect(rendered.getByText('Path at outside: /foo/bar')).toBeInTheDocument(); + }); + + it('should handle routeRefs with parameters', () => { + const root = ( + + + + + + + + + ); + + const { routes, routeParents } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routes: routeCollector, + routeParents: routeParentCollector, + }, + }); + + const rendered = render( + + {root} + , + ); + + expect( + rendered.getByText('Path at inside: /foo/bar/bleb'), + ).toBeInTheDocument(); + expect( + rendered.getByText('Path at outside: /foo/bar/blob'), + ).toBeInTheDocument(); }); }); diff --git a/packages/core-api/src/routing/hooks.tsx b/packages/core-api/src/routing/hooks.tsx index 8a2aa72c92..739d74247c 100644 --- a/packages/core-api/src/routing/hooks.tsx +++ b/packages/core-api/src/routing/hooks.tsx @@ -16,8 +16,9 @@ import React, { createContext, ReactNode, useContext } from 'react'; import { RouteRef } from './types'; +import { generatePath } from 'react-router-dom'; -export type RouteFunc = () => string; +export type RouteFunc = (params?: Record) => string; class RouteResolver { constructor( @@ -38,8 +39,8 @@ class RouteResolver { currentRouteRef = this.routeParents.get(currentRouteRef); } - return () => { - return fullPath; + return (params?: Record) => { + return generatePath(fullPath, params); }; } } From 64993c1dd246776bb9c8ab171359d8ca81c0f56b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 27 Nov 2020 16:10:19 +0100 Subject: [PATCH 37/86] core-api: add naive implementation of param-relative routing Co-authored-by: blam --- packages/core-api/src/routing/hooks.test.tsx | 40 +++++++++++++++++++- packages/core-api/src/routing/hooks.tsx | 11 +++--- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/packages/core-api/src/routing/hooks.test.tsx b/packages/core-api/src/routing/hooks.test.tsx index 65c7858954..6c38a6e18a 100644 --- a/packages/core-api/src/routing/hooks.test.tsx +++ b/packages/core-api/src/routing/hooks.test.tsx @@ -38,6 +38,7 @@ const ref1 = createRouteRef(mockConfig()); const ref2 = createRouteRef(mockConfig()); const ref3 = createRouteRef(mockConfig()); const ref4 = createRouteRef(mockConfig()); +const ref5 = createRouteRef(mockConfig()); const MockRouteSource = (props: { name: string; @@ -47,7 +48,7 @@ const MockRouteSource = (props: { const routeFunc = useRouteRef(props.routeRef); return (
- Path at {props.name}: {routeFunc?.(props.params)} + Path at {props.name}: {routeFunc(props.params)}
); }; @@ -64,6 +65,9 @@ const Extension3 = plugin.provide( const Extension4 = plugin.provide( createRoutableExtension({ component: MockRouteSource, mountPoint: ref4 }), ); +const Extension5 = plugin.provide( + createRoutableExtension({ component: MockComponent, mountPoint: ref5 }), +); describe('discovery', () => { it('should handle simple routeRef path creation for routeRefs used in other parts of the app', () => { @@ -100,7 +104,7 @@ describe('discovery', () => { it('should handle routeRefs with parameters', () => { const root = ( - + { rendered.getByText('Path at outside: /foo/bar/blob'), ).toBeInTheDocument(); }); + + it('should handle relative routing within parameterized routes', () => { + const root = ( + + + + + + + + + ); + + const { routes, routeParents } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routes: routeCollector, + routeParents: routeParentCollector, + }, + }); + + const rendered = render( + + {root} + , + ); + + expect( + rendered.getByText('Path at inside: /foo/blob/baz'), + ).toBeInTheDocument(); + }); }); diff --git a/packages/core-api/src/routing/hooks.tsx b/packages/core-api/src/routing/hooks.tsx index 739d74247c..821fd6f9de 100644 --- a/packages/core-api/src/routing/hooks.tsx +++ b/packages/core-api/src/routing/hooks.tsx @@ -16,7 +16,7 @@ import React, { createContext, ReactNode, useContext } from 'react'; import { RouteRef } from './types'; -import { generatePath } from 'react-router-dom'; +import { generatePath, useParams } from 'react-router-dom'; export type RouteFunc = (params?: Record) => string; @@ -26,7 +26,7 @@ class RouteResolver { private readonly routeParents: Map, ) {} - resolve(routeRef: RouteRef): RouteFunc { + resolve(routeRef: RouteRef, parentParams: Record): RouteFunc { let currentRouteRef: RouteRef | undefined = routeRef; let fullPath = ''; @@ -40,20 +40,21 @@ class RouteResolver { } return (params?: Record) => { - return generatePath(fullPath, params); + return generatePath(fullPath, { ...params, ...parentParams }); }; } } const RoutingContext = createContext(undefined); -export function useRouteRef(routeRef: RouteRef): RouteFunc | undefined { +export function useRouteRef(routeRef: RouteRef): RouteFunc { const resolver = useContext(RoutingContext); + const params = useParams(); if (!resolver) { throw new Error('No route resolver found in context'); } - return resolver.resolve(routeRef); + return resolver.resolve(routeRef, params); } type ProviderProps = { From 4ef2a8769b4741358cd80112af43a87feeca9acd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 27 Nov 2020 17:26:38 +0100 Subject: [PATCH 38/86] core-api: add validation of mounted route parameter uniqueness Co-authored-by: blam --- packages/core-api/src/routing/hooks.test.tsx | 27 +++++++++++++- packages/core-api/src/routing/hooks.tsx | 39 ++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/packages/core-api/src/routing/hooks.test.tsx b/packages/core-api/src/routing/hooks.test.tsx index 6c38a6e18a..c2b8679e5f 100644 --- a/packages/core-api/src/routing/hooks.test.tsx +++ b/packages/core-api/src/routing/hooks.test.tsx @@ -25,7 +25,7 @@ import { } from '../extensions/traversal'; import { createPlugin } from '../plugin'; import { routeCollector, routeParentCollector } from './collectors'; -import { useRouteRef, RoutingProvider } from './hooks'; +import { useRouteRef, RoutingProvider, validateRoutes } from './hooks'; import { createRouteRef } from './RouteRef'; import { RouteRef } from './types'; @@ -177,4 +177,29 @@ describe('discovery', () => { rendered.getByText('Path at inside: /foo/blob/baz'), ).toBeInTheDocument(); }); + + it('should handle relative routing of parameterized routes with duplicate param names', () => { + const root = ( + + + + + + + + ); + + const { routes, routeParents } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routes: routeCollector, + routeParents: routeParentCollector, + }, + }); + + expect(() => validateRoutes(routes, routeParents)).toThrow( + 'Parameter :id is duplicated in path /foo/:id/bar/:id', + ); + }); }); diff --git a/packages/core-api/src/routing/hooks.tsx b/packages/core-api/src/routing/hooks.tsx index 821fd6f9de..0c36928f10 100644 --- a/packages/core-api/src/routing/hooks.tsx +++ b/packages/core-api/src/routing/hooks.tsx @@ -75,3 +75,42 @@ export const RoutingProvider = ({ ); }; + +export function validateRoutes( + routes: Map, + routeParents: Map, +) { + const notLeafRoutes = new Set(routeParents.values()); + notLeafRoutes.delete(undefined); + + for (const route of routeParents.keys()) { + if (notLeafRoutes.has(route)) { + continue; + } + + let currentRouteRef: RouteRef | undefined = route; + + let fullPath = ''; + while (currentRouteRef) { + const path = routes.get(currentRouteRef); + if (!path) { + throw new Error(`No path for ${currentRouteRef}`); + } + fullPath = `${path}${fullPath}`; + currentRouteRef = routeParents.get(currentRouteRef); + } + + const params = fullPath.match(/:(\w+)/g); + if (params) { + for (let j = 0; j < params.length; j++) { + for (let i = j + 1; i < params.length; i++) { + if (params[i] === params[j]) { + throw new Error( + `Parameter ${params[i]} is duplicated in path ${fullPath}`, + ); + } + } + } + } + } +} From db28b524c48da657eec00e1deb92affece47a3df Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 30 Nov 2020 13:53:46 +0100 Subject: [PATCH 39/86] core-api: remove RouteRefRegistry and SubRouteRefs Co-authored-by: blam --- packages/core-api/src/routing/RouteRef.ts | 52 +----- .../src/routing/RouteRefRegistry.test.ts | 105 ------------ .../core-api/src/routing/RouteRefRegistry.ts | 155 ------------------ packages/core-api/src/routing/index.ts | 2 +- packages/core-api/src/routing/types.ts | 11 -- 5 files changed, 3 insertions(+), 322 deletions(-) delete mode 100644 packages/core-api/src/routing/RouteRefRegistry.test.ts delete mode 100644 packages/core-api/src/routing/RouteRefRegistry.ts diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts index c33335cb38..e7160fdef6 100644 --- a/packages/core-api/src/routing/RouteRef.ts +++ b/packages/core-api/src/routing/RouteRef.ts @@ -14,43 +14,9 @@ * limitations under the License. */ -import { - ConcreteRoute, - routeReference, - ReferencedRoute, - resolveRoute, - RouteRefConfig, -} from './types'; -import { generatePath } from 'react-router-dom'; +import { RouteRefConfig } from './types'; -type SubRouteConfig = { - path: string; -}; - -export class SubRouteRef - implements ReferencedRoute { - constructor( - private readonly parent: ConcreteRoute, - private readonly config: SubRouteConfig, - ) {} - - get [routeReference]() { - return this; - } - - link(...args: Args): ConcreteRoute { - return { - [routeReference]: this, - [resolveRoute]: (path: string) => { - const ownPart = generatePath(this.config.path, args[0] ?? {}); - const parentPart = this.parent[resolveRoute](path); - return parentPart + ownPart; - }, - }; - } -} - -export class AbsoluteRouteRef implements ConcreteRoute { +export class AbsoluteRouteRef { constructor(private readonly config: RouteRefConfig) {} get icon() { @@ -65,20 +31,6 @@ export class AbsoluteRouteRef implements ConcreteRoute { get title() { return this.config.title; } - - createSubRoute( - config: SubRouteConfig, - ) { - return new SubRouteRef(this, config); - } - - get [routeReference]() { - return this; - } - - [resolveRoute](path: string) { - return path; - } } export function createRouteRef(config: RouteRefConfig): AbsoluteRouteRef { diff --git a/packages/core-api/src/routing/RouteRefRegistry.test.ts b/packages/core-api/src/routing/RouteRefRegistry.test.ts deleted file mode 100644 index fa1ef584f1..0000000000 --- a/packages/core-api/src/routing/RouteRefRegistry.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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 { RouteRefRegistry } from './RouteRefRegistry'; -import { createRouteRef } from './RouteRef'; - -const dummyConfig = { path: '/', icon: () => null, title: 'my-title' }; -const ref1 = createRouteRef(dummyConfig); -const ref11 = createRouteRef(dummyConfig); -const ref12 = createRouteRef(dummyConfig); -const ref121 = createRouteRef(dummyConfig); -const ref2 = createRouteRef(dummyConfig); -const ref2a = ref2.createSubRoute({ path: '/a' }); -const ref2b = ref2.createSubRoute<{ id: string }>({ path: '/b/:id' }); - -describe('RouteRefRegistry', () => { - it('should be constructed with a root route', () => { - const registry = new RouteRefRegistry(); - expect(registry.resolveRoute([], [])).toBe(''); - }); - - it('should register and resolve some absolute routes', () => { - const registry = new RouteRefRegistry(); - expect(registry.registerRoute([ref1], '1')).toBe(true); - expect(registry.registerRoute([ref1, ref11], '11')).toBe(true); - expect(registry.registerRoute([ref1, ref12], '12')).toBe(true); - expect(registry.registerRoute([ref1, ref12, ref121], '121')).toBe(true); - expect(registry.registerRoute([ref1, ref12, ref121], 'duplicate')).toBe( - false, - ); - expect(registry.registerRoute([ref1, ref12], 'duplicate')).toBe(false); - expect(registry.registerRoute([ref2], '2')).toBe(true); - expect(registry.registerRoute([ref2], 'duplicate')).toBe(false); - expect(registry.registerRoute([ref2], '2')).toBe(true); - - expect(registry.resolveRoute([], [ref1])).toBe('/1'); - expect(registry.resolveRoute([], [ref11])).toBe(undefined); - expect(registry.resolveRoute([], [ref1, ref11])).toBe('/1/11'); - expect(registry.resolveRoute([ref1], [ref11])).toBe('/1/11'); - expect(registry.resolveRoute([ref1], [ref2])).toBe('/2'); - expect(registry.resolveRoute([ref1, ref12, ref121], [])).toBe('/1/12/121'); - expect(registry.resolveRoute([ref1, ref12, ref121], [ref121])).toBe( - '/1/12/121', - ); - expect(registry.resolveRoute([ref1, ref12, ref121], [ref12, ref121])).toBe( - '/1/12/121', - ); - expect(registry.resolveRoute([ref1, ref12, ref121], [ref12])).toBe('/1/12'); - expect(registry.resolveRoute([ref1, ref12, ref121], [ref1])).toBe('/1'); - }); - - it('should register and resolve with sub routes', () => { - const registry = new RouteRefRegistry(); - expect(registry.registerRoute([ref1], '1')).toBe(true); - expect(registry.registerRoute([ref2], '2')).toBe(true); - expect(registry.registerRoute([ref2a], '2')).toBe(true); - expect(registry.registerRoute([ref2a, ref1], '1')).toBe(true); - expect(registry.registerRoute([ref2a, ref2], '2')).toBe(true); - expect(registry.registerRoute([ref2b], '2')).toBe(true); - expect(registry.registerRoute([ref2b, ref1], '1')).toBe(true); - expect(registry.registerRoute([ref2b, ref2], '2')).toBe(true); - - expect(registry.resolveRoute([], [ref1])).toBe('/1'); - expect(registry.resolveRoute([], [ref2])).toBe('/2'); - expect(registry.resolveRoute([], [ref2a.link(), ref1])).toBe('/2/a/1'); - expect(registry.resolveRoute([], [ref2a.link(), ref2])).toBe('/2/a/2'); - expect(registry.resolveRoute([ref2a.link()], [ref2])).toBe('/2/a/2'); - expect(registry.resolveRoute([ref2a.link(), ref1], [ref2])).toBe('/2/a/2'); - expect(registry.resolveRoute([], [ref2b.link({ id: 'abc' }), ref1])).toBe( - '/2/b/abc/1', - ); - expect(registry.resolveRoute([], [ref2b.link({ id: 'xyz' }), ref2])).toBe( - '/2/b/xyz/2', - ); - expect(registry.resolveRoute([ref2b.link({ id: 'abc' })], [ref2])).toBe( - '/2/b/abc/2', - ); - expect( - registry.resolveRoute([ref2b.link({ id: 'abc' }), ref1], [ref2]), - ).toBe('/2/b/abc/2'); - }); - - it('should throw when registering routes incorrectly', () => { - const registry = new RouteRefRegistry(); - expect(() => { - registry.registerRoute([ref1, ref11], '11'); - }).toThrow('Could not find parent for new routing node'); - expect(() => { - registry.registerRoute([], '11'); - }).toThrow('Must provide at least 1 route to add routing node'); - }); -}); diff --git a/packages/core-api/src/routing/RouteRefRegistry.ts b/packages/core-api/src/routing/RouteRefRegistry.ts deleted file mode 100644 index 7e55cbe8f7..0000000000 --- a/packages/core-api/src/routing/RouteRefRegistry.ts +++ /dev/null @@ -1,155 +0,0 @@ -/* - * 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 { - ConcreteRoute, - routeReference, - resolveRoute, - ReferencedRoute, -} from './types'; - -const rootRoute: ConcreteRoute = { - get [routeReference]() { - return this; - }, - [resolveRoute]: () => '', -}; - -export type RouteRefResolver = { - resolveRoute(from: ConcreteRoute[], to: ConcreteRoute[]): string; -}; - -class Node { - readonly children = new Map(); - - constructor(readonly path: string, readonly parent: Node | undefined) {} - - /** - * Look up a node in the tree given a path. - */ - findNode(routes: ReferencedRoute[]): Node | undefined { - let node = this as Node | undefined; - - for (let i = 0; i < routes.length; i++) { - node = node?.children.get(routes[i][routeReference]); - } - - return node; - } - - /** - * Assigns a path to a leaf node in the routing tree. All ancestor - * nodes of the new leaf node must already exist, or an error will be thrown. - * - * Returns true if the node was added, or false if the node already existed. - */ - addNode(routes: ReferencedRoute[], path: string): boolean { - if (routes.length === 0) { - throw new Error('Must provide at least 1 route to add routing node'); - } - - const parentNode = this.findNode(routes.slice(0, -1)); - if (!parentNode) { - throw new Error('Could not find parent for new routing node'); - } - - const lastRoute = routes[routes.length - 1]; - const lastRouteRef = lastRoute[routeReference]; - - const existingNode = parentNode.children.get(lastRouteRef); - if (existingNode) { - return existingNode.path === path; - } - - parentNode.children.set(lastRouteRef, new Node(path, parentNode)); - return true; - } - - /** - * Resolve an absolute URL that represents this node in the routing tree, using - * using the supplied concrete routes and ancestors of this node. - * - * The length of the provided routes array must match the depth of - * the routing tree that this node is at, or an error will be thrown. - */ - resolve(routes: ConcreteRoute[]) { - const parts = Array(routes.length); - - let node = this as Node | undefined; - for (let i = routes.length - 1; i >= 0; i--) { - if (!node) { - throw new Error('Route resolve missing required parent'); - } - - const route = routes[i]; - parts[i] = route[resolveRoute](node.path); - - node = node.parent; - } - - if (node) { - throw new Error('Route resolve did not reach root'); - } - - return parts.join('/'); - } -} - -/** - * A registry for resolving route refs into concrete string routes. - */ -export class RouteRefRegistry { - private readonly root = new Node('', undefined); - - /** - * Register a new leaf path for a sequence of routes. All ancestor - * routes must already exist. - */ - registerRoute(routes: ReferencedRoute[], path: string): boolean { - return this.root.addNode(routes, path); - } - - /** - * Resolve an absolute path from a point in the routing tree. - * - * The route referenced by `from` must exist, and is the starting - * point for the search, walking up the tree until a subtree that - * matches the routes reference in `to` are found. - * - * If `from` is empty, the search starts and ends at the root node. - * If `to` is empty, the route referenced by `from` will always be returned. - */ - resolveRoute(from: ConcreteRoute[], to: ConcreteRoute[]): string | undefined { - // Keep track of the `from` routes and pop the last ones as we traverse up - // the routing tree. The list of concrete routes that we're passing to - // `node.resolve()` should only include the ones in the resolve path. - const concreteStack = from.slice(); - - let fromNode = this.root.findNode(from); - while (fromNode) { - const resolvedNode = fromNode.findNode(to); - if (resolvedNode) { - return resolvedNode.resolve([rootRoute].concat(concreteStack, to)); - } - - // Search at this level of the tree failed, move up to parent - concreteStack.pop(); - fromNode = fromNode.parent; - } - - return undefined; - } -} diff --git a/packages/core-api/src/routing/index.ts b/packages/core-api/src/routing/index.ts index 29de34ec42..45bb8975db 100644 --- a/packages/core-api/src/routing/index.ts +++ b/packages/core-api/src/routing/index.ts @@ -14,6 +14,6 @@ * limitations under the License. */ -export type { RouteRef, RouteRefConfig, ConcreteRoute } from './types'; +export type { RouteRef, RouteRefConfig } from './types'; export type { MutableRouteRef, AbsoluteRouteRef } from './RouteRef'; export { createRouteRef } from './RouteRef'; diff --git a/packages/core-api/src/routing/types.ts b/packages/core-api/src/routing/types.ts index 162ac74bde..edb1bba196 100644 --- a/packages/core-api/src/routing/types.ts +++ b/packages/core-api/src/routing/types.ts @@ -16,17 +16,6 @@ import { IconComponent } from '../icons'; -export const resolveRoute = Symbol('resolve-route'); -export const routeReference = Symbol('route-ref'); - -export type ReferencedRoute = { - [routeReference]: unknown; -}; - -export type ConcreteRoute = ReferencedRoute & { - [resolveRoute](path: string): string; -}; - export type RouteRef = { // TODO(Rugvip): Remove path, look up via registry instead path: string; From e4782fd3b92b1f56754ba99298081d07e963004c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 30 Nov 2020 14:20:05 +0100 Subject: [PATCH 40/86] core-api: rename routes to routePaths Co-authored-by: blam --- .../core-api/src/routing/collectors.test.tsx | 8 ++-- packages/core-api/src/routing/collectors.tsx | 2 +- packages/core-api/src/routing/hooks.test.tsx | 38 ++++++++++--------- packages/core-api/src/routing/hooks.tsx | 14 +++---- 4 files changed, 33 insertions(+), 29 deletions(-) diff --git a/packages/core-api/src/routing/collectors.test.tsx b/packages/core-api/src/routing/collectors.test.tsx index c2eefa7e82..2595b8c0a0 100644 --- a/packages/core-api/src/routing/collectors.test.tsx +++ b/packages/core-api/src/routing/collectors.test.tsx @@ -15,7 +15,7 @@ */ import React, { PropsWithChildren } from 'react'; -import { routeCollector, routeParentCollector } from './collectors'; +import { routePathCollector, routeParentCollector } from './collectors'; import { traverseElementTree, @@ -96,7 +96,7 @@ describe('discovery', () => { root, discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { - routes: routeCollector, + routes: routePathCollector, routeParents: routeParentCollector, }, }); @@ -147,7 +147,7 @@ describe('discovery', () => { root, discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { - routes: routeCollector, + routes: routePathCollector, routeParents: routeParentCollector, }, }); @@ -184,7 +184,7 @@ describe('discovery', () => { ), discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { - routes: routeCollector, + routes: routePathCollector, routeParents: routeParentCollector, }, }), diff --git a/packages/core-api/src/routing/collectors.tsx b/packages/core-api/src/routing/collectors.tsx index 34edd8a0d5..37f9252329 100644 --- a/packages/core-api/src/routing/collectors.tsx +++ b/packages/core-api/src/routing/collectors.tsx @@ -19,7 +19,7 @@ import { RouteRef } from '../routing/types'; import { getComponentData } from '../extensions'; import { createCollector } from '../extensions/traversal'; -export const routeCollector = createCollector( +export const routePathCollector = createCollector( () => new Map(), (acc, node, parent) => { if (parent.props.element === node) { diff --git a/packages/core-api/src/routing/hooks.test.tsx b/packages/core-api/src/routing/hooks.test.tsx index c2b8679e5f..695cd35907 100644 --- a/packages/core-api/src/routing/hooks.test.tsx +++ b/packages/core-api/src/routing/hooks.test.tsx @@ -24,12 +24,16 @@ import { traverseElementTree, } from '../extensions/traversal'; import { createPlugin } from '../plugin'; -import { routeCollector, routeParentCollector } from './collectors'; +import { routePathCollector, routeParentCollector } from './collectors'; import { useRouteRef, RoutingProvider, validateRoutes } from './hooks'; import { createRouteRef } from './RouteRef'; -import { RouteRef } from './types'; +import { RouteRef, RouteRefConfig } from './types'; -const mockConfig = () => ({ path: '/unused', title: 'Unused' }); +const mockConfig = (extra?: Partial) => ({ + path: '/unused', + title: 'Unused', + ...extra, +}); const MockComponent = ({ children }: PropsWithChildren<{}>) => <>{children}; const plugin = createPlugin({ id: 'my-plugin' }); @@ -83,17 +87,17 @@ describe('discovery', () => { ); - const { routes, routeParents } = traverseElementTree({ + const { routePaths, routeParents } = traverseElementTree({ root, discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { - routes: routeCollector, + routePaths: routePathCollector, routeParents: routeParentCollector, }, }); const rendered = render( - + {root} , ); @@ -123,17 +127,17 @@ describe('discovery', () => { ); - const { routes, routeParents } = traverseElementTree({ + const { routePaths, routeParents } = traverseElementTree({ root, discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { - routes: routeCollector, + routePaths: routePathCollector, routeParents: routeParentCollector, }, }); const rendered = render( - + {root} , ); @@ -146,7 +150,7 @@ describe('discovery', () => { ).toBeInTheDocument(); }); - it('should handle relative routing within parameterized routes', () => { + it('should handle relative routing within parameterized routePaths', () => { const root = ( @@ -158,17 +162,17 @@ describe('discovery', () => { ); - const { routes, routeParents } = traverseElementTree({ + const { routePaths, routeParents } = traverseElementTree({ root, discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { - routes: routeCollector, + routePaths: routePathCollector, routeParents: routeParentCollector, }, }); const rendered = render( - + {root} , ); @@ -178,7 +182,7 @@ describe('discovery', () => { ).toBeInTheDocument(); }); - it('should handle relative routing of parameterized routes with duplicate param names', () => { + it('should handle relative routing of parameterized routePaths with duplicate param names', () => { const root = ( @@ -189,16 +193,16 @@ describe('discovery', () => { ); - const { routes, routeParents } = traverseElementTree({ + const { routePaths, routeParents } = traverseElementTree({ root, discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { - routes: routeCollector, + routePaths: routePathCollector, routeParents: routeParentCollector, }, }); - expect(() => validateRoutes(routes, routeParents)).toThrow( + expect(() => validateRoutes(routePaths, routeParents)).toThrow( 'Parameter :id is duplicated in path /foo/:id/bar/:id', ); }); diff --git a/packages/core-api/src/routing/hooks.tsx b/packages/core-api/src/routing/hooks.tsx index 0c36928f10..22299e3305 100644 --- a/packages/core-api/src/routing/hooks.tsx +++ b/packages/core-api/src/routing/hooks.tsx @@ -22,7 +22,7 @@ export type RouteFunc = (params?: Record) => string; class RouteResolver { constructor( - private readonly routes: Map, + private readonly routePaths: Map, private readonly routeParents: Map, ) {} @@ -31,7 +31,7 @@ class RouteResolver { let fullPath = ''; while (currentRouteRef) { - const path = this.routes.get(currentRouteRef); + const path = this.routePaths.get(currentRouteRef); if (!path) { throw new Error(`No path for ${currentRouteRef}`); } @@ -58,17 +58,17 @@ export function useRouteRef(routeRef: RouteRef): RouteFunc { } type ProviderProps = { - routes: Map; + routePaths: Map; routeParents: Map; children: ReactNode; }; export const RoutingProvider = ({ - routes, + routePaths, routeParents, children, }: ProviderProps) => { - const resolver = new RouteResolver(routes, routeParents); + const resolver = new RouteResolver(routePaths, routeParents); return ( {children} @@ -77,7 +77,7 @@ export const RoutingProvider = ({ }; export function validateRoutes( - routes: Map, + routePaths: Map, routeParents: Map, ) { const notLeafRoutes = new Set(routeParents.values()); @@ -92,7 +92,7 @@ export function validateRoutes( let fullPath = ''; while (currentRouteRef) { - const path = routes.get(currentRouteRef); + const path = routePaths.get(currentRouteRef); if (!path) { throw new Error(`No path for ${currentRouteRef}`); } From 30a115d39231cf774139d731d8f983281e3e8781 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 30 Nov 2020 17:52:41 +0100 Subject: [PATCH 41/86] core-api: add collector for route objects and use react-router matching to implement relative routing Co-authored-by: blam --- packages/core-api/src/routing/RouteRef.ts | 4 + packages/core-api/src/routing/collectors.tsx | 36 +++++- packages/core-api/src/routing/hooks.test.tsx | 121 ++++++++++++++++--- packages/core-api/src/routing/hooks.tsx | 89 +++++++++++--- packages/core-api/src/routing/types.ts | 9 ++ 5 files changed, 222 insertions(+), 37 deletions(-) diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts index e7160fdef6..b9dd7c94fe 100644 --- a/packages/core-api/src/routing/RouteRef.ts +++ b/packages/core-api/src/routing/RouteRef.ts @@ -31,6 +31,10 @@ export class AbsoluteRouteRef { get title() { return this.config.title; } + + toString() { + return `routeRef{path=${this.path}}`; + } } export function createRouteRef(config: RouteRefConfig): AbsoluteRouteRef { diff --git a/packages/core-api/src/routing/collectors.tsx b/packages/core-api/src/routing/collectors.tsx index 37f9252329..38e5376547 100644 --- a/packages/core-api/src/routing/collectors.tsx +++ b/packages/core-api/src/routing/collectors.tsx @@ -15,7 +15,7 @@ */ import { isValidElement, ReactNode } from 'react'; -import { RouteRef } from '../routing/types'; +import { BackstageRouteObject, RouteRef } from '../routing/types'; import { getComponentData } from '../extensions'; import { createCollector } from '../extensions/traversal'; @@ -80,3 +80,37 @@ export const routeParentCollector = createCollector( return nextParent; }, ); + +export const routeObjectCollector = createCollector( + () => Array(), + (acc, node, parent, parentChildArr: BackstageRouteObject[] = acc) => { + if (parent.props.element === node) { + return parentChildArr; + } + + const path: string | undefined = node.props?.path; + const caseSensitive: boolean = Boolean(node.props?.caseSensitive); + const element: ReactNode = node.props?.element; + + let routeRef = getComponentData(node, 'core.mountPoint'); + if (!routeRef && isValidElement(element)) { + routeRef = getComponentData(element, 'core.mountPoint'); + } + if (routeRef) { + const children: BackstageRouteObject[] = []; + if (!path) { + throw new Error(`No path found for mount point ${routeRef}`); + } + parentChildArr.push({ + caseSensitive, + path, + element: null, + routeRef, + children, + }); + return children; + } + + return parentChildArr; + }, +); diff --git a/packages/core-api/src/routing/hooks.test.tsx b/packages/core-api/src/routing/hooks.test.tsx index 695cd35907..68d7abf38a 100644 --- a/packages/core-api/src/routing/hooks.test.tsx +++ b/packages/core-api/src/routing/hooks.test.tsx @@ -24,7 +24,11 @@ import { traverseElementTree, } from '../extensions/traversal'; import { createPlugin } from '../plugin'; -import { routePathCollector, routeParentCollector } from './collectors'; +import { + routePathCollector, + routeParentCollector, + routeObjectCollector, +} from './collectors'; import { useRouteRef, RoutingProvider, validateRoutes } from './hooks'; import { createRouteRef } from './RouteRef'; import { RouteRef, RouteRefConfig } from './types'; @@ -38,23 +42,31 @@ const MockComponent = ({ children }: PropsWithChildren<{}>) => <>{children}; const plugin = createPlugin({ id: 'my-plugin' }); -const ref1 = createRouteRef(mockConfig()); -const ref2 = createRouteRef(mockConfig()); -const ref3 = createRouteRef(mockConfig()); -const ref4 = createRouteRef(mockConfig()); -const ref5 = createRouteRef(mockConfig()); +const ref1 = createRouteRef(mockConfig({ path: '/wat1' })); +const ref2 = createRouteRef(mockConfig({ path: '/wat2' })); +const ref3 = createRouteRef(mockConfig({ path: '/wat3' })); +const ref4 = createRouteRef(mockConfig({ path: '/wat4' })); +const ref5 = createRouteRef(mockConfig({ path: '/wat5' })); const MockRouteSource = (props: { name: string; routeRef: RouteRef; params?: Record; }) => { - const routeFunc = useRouteRef(props.routeRef); - return ( -
- Path at {props.name}: {routeFunc(props.params)} -
- ); + try { + const routeFunc = useRouteRef(props.routeRef); + return ( +
+ Path at {props.name}: {routeFunc(props.params)} +
+ ); + } catch (ex) { + return ( +
+ Error at {props.name}: {ex.message} +
+ ); + } }; const Extension1 = plugin.provide( @@ -87,17 +99,22 @@ describe('discovery', () => { ); - const { routePaths, routeParents } = traverseElementTree({ + const { routePaths, routeParents, routeObjects } = traverseElementTree({ root, discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { routePaths: routePathCollector, routeParents: routeParentCollector, + routeObjects: routeObjectCollector, }, }); const rendered = render( - + {root} , ); @@ -127,17 +144,22 @@ describe('discovery', () => { ); - const { routePaths, routeParents } = traverseElementTree({ + const { routePaths, routeParents, routeObjects } = traverseElementTree({ root, discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { routePaths: routePathCollector, routeParents: routeParentCollector, + routeObjects: routeObjectCollector, }, }); const rendered = render( - + {root} , ); @@ -152,27 +174,38 @@ describe('discovery', () => { it('should handle relative routing within parameterized routePaths', () => { const root = ( - + + + ); - const { routePaths, routeParents } = traverseElementTree({ + const { routePaths, routeParents, routeObjects } = traverseElementTree({ root, discoverers: [childDiscoverer, routeElementDiscoverer], collectors: { routePaths: routePathCollector, routeParents: routeParentCollector, + routeObjects: routeObjectCollector, }, }); const rendered = render( - + {root} , ); @@ -182,6 +215,56 @@ describe('discovery', () => { ).toBeInTheDocument(); }); + it('should throw errors for routing to other routeRefs with unsupported parameters', () => { + const root = ( + + + + + + + + + + + ); + + const { routePaths, routeParents, routeObjects } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routePaths: routePathCollector, + routeParents: routeParentCollector, + routeObjects: routeObjectCollector, + }, + }); + + const rendered = render( + + {root} + , + ); + + expect( + rendered.getByText( + `Error at outsideWithParams: Cannot route to ${ref3} with parent ${ref5} as it has parameters`, + ), + ).toBeInTheDocument(); + expect( + rendered.getByText( + `Error at outsideNoParams: Cannot route to ${ref3} with parent ${ref5} as it has parameters`, + ), + ).toBeInTheDocument(); + }); + it('should handle relative routing of parameterized routePaths with duplicate param names', () => { const root = ( diff --git a/packages/core-api/src/routing/hooks.tsx b/packages/core-api/src/routing/hooks.tsx index 22299e3305..5dbaed4545 100644 --- a/packages/core-api/src/routing/hooks.tsx +++ b/packages/core-api/src/routing/hooks.tsx @@ -14,9 +14,9 @@ * limitations under the License. */ -import React, { createContext, ReactNode, useContext } from 'react'; -import { RouteRef } from './types'; -import { generatePath, useParams } from 'react-router-dom'; +import React, { createContext, ReactNode, useContext, useMemo } from 'react'; +import { BackstageRouteObject, RouteRef } from './types'; +import { generatePath, matchRoutes, useLocation } from 'react-router-dom'; export type RouteFunc = (params?: Record) => string; @@ -24,23 +24,71 @@ class RouteResolver { constructor( private readonly routePaths: Map, private readonly routeParents: Map, + private readonly routeObjects: BackstageRouteObject[], ) {} - resolve(routeRef: RouteRef, parentParams: Record): RouteFunc { - let currentRouteRef: RouteRef | undefined = routeRef; - let fullPath = ''; + resolve( + routeRef: RouteRef, + sourceLocation: ReturnType, + ): RouteFunc { + const match = matchRoutes(this.routeObjects, sourceLocation) ?? []; - while (currentRouteRef) { - const path = this.routePaths.get(currentRouteRef); - if (!path) { - throw new Error(`No path for ${currentRouteRef}`); + const lastPath = this.routePaths.get(routeRef); + if (!lastPath) { + throw new Error(`No path for ${routeRef}`); + } + const targetRefStack = Array(); + let matchIndex = -1; + + for ( + let currentRouteRef: RouteRef | undefined = routeRef; + currentRouteRef; + currentRouteRef = this.routeParents.get(currentRouteRef) + ) { + matchIndex = match.findIndex( + m => (m.route as BackstageRouteObject).routeRef === currentRouteRef, + ); + if (matchIndex !== -1) { + break; } - fullPath = `${path}${fullPath}`; - currentRouteRef = this.routeParents.get(currentRouteRef); + + targetRefStack.unshift(currentRouteRef); } + // If our target route is present in the initial match we need to construct the final path + // from the parent of the matched route segment. That's to allow the caller of the route + // function to supply their own params. + if (targetRefStack.length === 0) { + matchIndex -= 1; + } + + // This is the part of the route tree that the target and source locations have in common. + // We re-use the existing pathname directly along with all params. + const parentPath = matchIndex === -1 ? '' : match[matchIndex].pathname; + + // This constructs the mid section of the path using paths resolved from all route refs + // we need to traverse to reach our target except for the very last one. None of these + // paths are allowed to require any parameters, as the called would have no way of knowing + // what parameters those are. + const prefixPath = targetRefStack + .slice(0, -1) + .map(ref => { + const path = this.routePaths.get(ref); + if (!path) { + throw new Error(`No path for ${ref}`); + } + if (path.includes(':')) { + throw new Error( + `Cannot route to ${routeRef} with parent ${ref} as it has parameters`, + ); + } + return path; + }) + .join('/') + .replace(/\/\/+/g, '/'); // Normalize path to not contain repeated /'s + return (params?: Record) => { - return generatePath(fullPath, { ...params, ...parentParams }); + return `${parentPath}${prefixPath}${generatePath(lastPath, params)}`; }; } } @@ -48,27 +96,34 @@ class RouteResolver { const RoutingContext = createContext(undefined); export function useRouteRef(routeRef: RouteRef): RouteFunc { + const sourceLocation = useLocation(); const resolver = useContext(RoutingContext); - const params = useParams(); - if (!resolver) { + const routeFunc = useMemo( + () => resolver && resolver.resolve(routeRef, sourceLocation), + [resolver, routeRef, sourceLocation], + ); + + if (!routeFunc) { throw new Error('No route resolver found in context'); } - return resolver.resolve(routeRef, params); + return routeFunc; } type ProviderProps = { routePaths: Map; routeParents: Map; + routeObjects: BackstageRouteObject[]; children: ReactNode; }; export const RoutingProvider = ({ routePaths, routeParents, + routeObjects, children, }: ProviderProps) => { - const resolver = new RouteResolver(routePaths, routeParents); + const resolver = new RouteResolver(routePaths, routeParents, routeObjects); return ( {children} diff --git a/packages/core-api/src/routing/types.ts b/packages/core-api/src/routing/types.ts index edb1bba196..2f13a97a91 100644 --- a/packages/core-api/src/routing/types.ts +++ b/packages/core-api/src/routing/types.ts @@ -28,3 +28,12 @@ export type RouteRefConfig = { icon?: IconComponent; title: string; }; + +// A duplicate of the react-router RouteObject, but with routeRef added +export interface BackstageRouteObject { + caseSensitive: boolean; + children?: BackstageRouteObject[]; + element: React.ReactNode; + path: string; + routeRef: RouteRef; +} From 9a918668948d2092e56420e0bc08e22bb6fdc999 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 30 Nov 2020 17:58:40 +0100 Subject: [PATCH 42/86] core-api: dry up route collectors --- packages/core-api/src/routing/collectors.tsx | 50 +++++++------------- 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/packages/core-api/src/routing/collectors.tsx b/packages/core-api/src/routing/collectors.tsx index 38e5376547..db6673536d 100644 --- a/packages/core-api/src/routing/collectors.tsx +++ b/packages/core-api/src/routing/collectors.tsx @@ -14,11 +14,22 @@ * limitations under the License. */ -import { isValidElement, ReactNode } from 'react'; +import { isValidElement, ReactElement, ReactNode } from 'react'; import { BackstageRouteObject, RouteRef } from '../routing/types'; import { getComponentData } from '../extensions'; import { createCollector } from '../extensions/traversal'; +function getMountPoint(node: ReactElement): RouteRef | undefined { + const element: ReactNode = node.props?.element; + + let routeRef = getComponentData(node, 'core.mountPoint'); + if (!routeRef && isValidElement(element)) { + routeRef = getComponentData(element, 'core.mountPoint'); + } + + return routeRef; +} + export const routePathCollector = createCollector( () => new Map(), (acc, node, parent) => { @@ -26,26 +37,13 @@ export const routePathCollector = createCollector( return; } - const path: string | undefined = node.props?.path; - const element: ReactNode = node.props?.element; - - const routeRef = getComponentData(node, 'core.mountPoint'); + const routeRef = getMountPoint(node); if (routeRef) { + const path: string | undefined = node.props?.path; if (!path) { throw new Error('Mounted routable extension must have a path'); } acc.set(routeRef, path); - } else if (isValidElement(element)) { - const elementRouteRef = getComponentData( - element, - 'core.mountPoint', - ); - if (elementRouteRef) { - if (!path) { - throw new Error('Route element must have a path'); - } - acc.set(elementRouteRef, path); - } } }, ); @@ -57,24 +55,12 @@ export const routeParentCollector = createCollector( return parentRouteRef; } - const element: ReactNode = node.props?.element; - let nextParent = parentRouteRef; - const routeRef = getComponentData(node, 'core.mountPoint'); + const routeRef = getMountPoint(node); if (routeRef) { acc.set(routeRef, parentRouteRef); nextParent = routeRef; - } else if (isValidElement(element)) { - const elementRouteRef = getComponentData( - element, - 'core.mountPoint', - ); - - if (elementRouteRef) { - acc.set(elementRouteRef, parentRouteRef); - nextParent = elementRouteRef; - } } return nextParent; @@ -90,12 +76,8 @@ export const routeObjectCollector = createCollector( const path: string | undefined = node.props?.path; const caseSensitive: boolean = Boolean(node.props?.caseSensitive); - const element: ReactNode = node.props?.element; - let routeRef = getComponentData(node, 'core.mountPoint'); - if (!routeRef && isValidElement(element)) { - routeRef = getComponentData(element, 'core.mountPoint'); - } + const routeRef = getMountPoint(node); if (routeRef) { const children: BackstageRouteObject[] = []; if (!path) { From ef63f951ea27161ff413ebd56cccbf912b3fe8c8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 30 Nov 2020 18:01:50 +0100 Subject: [PATCH 43/86] core-api: dry up routing hooks tests --- packages/core-api/src/routing/hooks.test.tsx | 104 +++++-------------- 1 file changed, 27 insertions(+), 77 deletions(-) diff --git a/packages/core-api/src/routing/hooks.test.tsx b/packages/core-api/src/routing/hooks.test.tsx index 68d7abf38a..4f79fea303 100644 --- a/packages/core-api/src/routing/hooks.test.tsx +++ b/packages/core-api/src/routing/hooks.test.tsx @@ -15,7 +15,7 @@ */ import { render } from '@testing-library/react'; -import React, { PropsWithChildren } from 'react'; +import React, { PropsWithChildren, ReactElement } from 'react'; import { MemoryRouter, Routes } from 'react-router-dom'; import { createRoutableExtension } from '../extensions'; import { @@ -85,6 +85,28 @@ const Extension5 = plugin.provide( createRoutableExtension({ component: MockComponent, mountPoint: ref5 }), ); +function withRoutingProvider(root: ReactElement) { + const { routePaths, routeParents, routeObjects } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routePaths: routePathCollector, + routeParents: routeParentCollector, + routeObjects: routeObjectCollector, + }, + }); + + return ( + + {root} + + ); +} + describe('discovery', () => { it('should handle simple routeRef path creation for routeRefs used in other parts of the app', () => { const root = ( @@ -99,25 +121,7 @@ describe('discovery', () => { ); - const { routePaths, routeParents, routeObjects } = traverseElementTree({ - root, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routePaths: routePathCollector, - routeParents: routeParentCollector, - routeObjects: routeObjectCollector, - }, - }); - - const rendered = render( - - {root} - , - ); + const rendered = render(withRoutingProvider(root)); expect(rendered.getByText('Path at inside: /foo/bar')).toBeInTheDocument(); expect(rendered.getByText('Path at outside: /foo/bar')).toBeInTheDocument(); @@ -144,25 +148,7 @@ describe('discovery', () => { ); - const { routePaths, routeParents, routeObjects } = traverseElementTree({ - root, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routePaths: routePathCollector, - routeParents: routeParentCollector, - routeObjects: routeObjectCollector, - }, - }); - - const rendered = render( - - {root} - , - ); + const rendered = render(withRoutingProvider(root)); expect( rendered.getByText('Path at inside: /foo/bar/bleb'), @@ -190,25 +176,7 @@ describe('discovery', () => { ); - const { routePaths, routeParents, routeObjects } = traverseElementTree({ - root, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routePaths: routePathCollector, - routeParents: routeParentCollector, - routeObjects: routeObjectCollector, - }, - }); - - const rendered = render( - - {root} - , - ); + const rendered = render(withRoutingProvider(root)); expect( rendered.getByText('Path at inside: /foo/blob/baz'), @@ -233,25 +201,7 @@ describe('discovery', () => { ); - const { routePaths, routeParents, routeObjects } = traverseElementTree({ - root, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routePaths: routePathCollector, - routeParents: routeParentCollector, - routeObjects: routeObjectCollector, - }, - }); - - const rendered = render( - - {root} - , - ); + const rendered = render(withRoutingProvider(root)); expect( rendered.getByText( From 3e7378953e1682dd15d94610e519e9d79823f854 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 1 Dec 2020 18:39:23 +0100 Subject: [PATCH 44/86] core-api: add optional typed params for RouteRefs and useRouteRef Co-authored-by: blam --- packages/core-api/src/routing/RouteRef.ts | 22 +++++----- packages/core-api/src/routing/hooks.test.tsx | 17 +++++--- packages/core-api/src/routing/hooks.tsx | 44 +++++++++++++------- packages/core-api/src/routing/index.ts | 1 - packages/core-api/src/routing/types.ts | 10 +++-- 5 files changed, 58 insertions(+), 36 deletions(-) diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts index b9dd7c94fe..41384cafe2 100644 --- a/packages/core-api/src/routing/RouteRef.ts +++ b/packages/core-api/src/routing/RouteRef.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { RouteRefConfig } from './types'; +import { RouteRefConfig, RouteRef } from './types'; -export class AbsoluteRouteRef { - constructor(private readonly config: RouteRefConfig) {} +export class AbsoluteRouteRef { + constructor(private readonly config: RouteRefConfig) {} get icon() { return this.config.icon; @@ -32,16 +32,18 @@ export class AbsoluteRouteRef { return this.config.title; } + get P(): Params { + throw new Error("Don't call me maybe"); + } + toString() { return `routeRef{path=${this.path}}`; } } -export function createRouteRef(config: RouteRefConfig): AbsoluteRouteRef { - return new AbsoluteRouteRef(config); +export function createRouteRef< + ParamKeys extends string, + Params extends { [param in string]: string } = { [name in ParamKeys]: string } +>(config: RouteRefConfig): RouteRef { + return new AbsoluteRouteRef(config); } - -// TODO(Rugvip): Added for backwards compatibility, remove once old usage is gone -// We may want to avoid exporting the AbsoluteRouteRef itself though, and consider -// a different model for how to create sub routes, just avoid this -export type MutableRouteRef = AbsoluteRouteRef; diff --git a/packages/core-api/src/routing/hooks.test.tsx b/packages/core-api/src/routing/hooks.test.tsx index 4f79fea303..ae1053b58d 100644 --- a/packages/core-api/src/routing/hooks.test.tsx +++ b/packages/core-api/src/routing/hooks.test.tsx @@ -29,11 +29,16 @@ import { routeParentCollector, routeObjectCollector, } from './collectors'; -import { useRouteRef, RoutingProvider, validateRoutes } from './hooks'; +import { + useRouteRef, + RoutingProvider, + validateRoutes, + RouteFunc, +} from './hooks'; import { createRouteRef } from './RouteRef'; import { RouteRef, RouteRefConfig } from './types'; -const mockConfig = (extra?: Partial) => ({ +const mockConfig = (extra?: Partial>) => ({ path: '/unused', title: 'Unused', ...extra, @@ -48,13 +53,13 @@ const ref3 = createRouteRef(mockConfig({ path: '/wat3' })); const ref4 = createRouteRef(mockConfig({ path: '/wat4' })); const ref5 = createRouteRef(mockConfig({ path: '/wat5' })); -const MockRouteSource = (props: { +const MockRouteSource = (props: { name: string; - routeRef: RouteRef; - params?: Record; + routeRef: RouteRef; + params?: T; }) => { try { - const routeFunc = useRouteRef(props.routeRef); + const routeFunc = useRouteRef(props.routeRef) as RouteFunc; return (
Path at {props.name}: {routeFunc(props.params)} diff --git a/packages/core-api/src/routing/hooks.tsx b/packages/core-api/src/routing/hooks.tsx index 5dbaed4545..563bc84a76 100644 --- a/packages/core-api/src/routing/hooks.tsx +++ b/packages/core-api/src/routing/hooks.tsx @@ -15,33 +15,42 @@ */ import React, { createContext, ReactNode, useContext, useMemo } from 'react'; -import { BackstageRouteObject, RouteRef } from './types'; +import { AnyRouteRef, BackstageRouteObject, RouteRef } from './types'; import { generatePath, matchRoutes, useLocation } from 'react-router-dom'; -export type RouteFunc = (params?: Record) => string; +// The extra TS magic here is to require a single params argument if the RouteRef +// had at least one param defined, but require 0 arguments if there are no params defined. +// Without this we'd have to pass in empty object to all parameter-less RouteRefs +// just to make TypeScript happy, or we would have to make the argument optional in +// which case you might forget to pass it in when it is actually required. +export type RouteFunc = ( + ...[params]: Params[keyof Params] extends never + ? readonly [] + : readonly [Params] +) => string; class RouteResolver { constructor( - private readonly routePaths: Map, - private readonly routeParents: Map, + private readonly routePaths: Map, + private readonly routeParents: Map, private readonly routeObjects: BackstageRouteObject[], ) {} - resolve( - routeRef: RouteRef, + resolve( + routeRef: RouteRef, sourceLocation: ReturnType, - ): RouteFunc { + ): RouteFunc { const match = matchRoutes(this.routeObjects, sourceLocation) ?? []; const lastPath = this.routePaths.get(routeRef); if (!lastPath) { throw new Error(`No path for ${routeRef}`); } - const targetRefStack = Array(); + const targetRefStack = Array(); let matchIndex = -1; for ( - let currentRouteRef: RouteRef | undefined = routeRef; + let currentRouteRef: AnyRouteRef | undefined = routeRef; currentRouteRef; currentRouteRef = this.routeParents.get(currentRouteRef) ) { @@ -87,15 +96,18 @@ class RouteResolver { .join('/') .replace(/\/\/+/g, '/'); // Normalize path to not contain repeated /'s - return (params?: Record) => { + const routeFunc: RouteFunc = (...[params]) => { return `${parentPath}${prefixPath}${generatePath(lastPath, params)}`; }; + return routeFunc; } } const RoutingContext = createContext(undefined); -export function useRouteRef(routeRef: RouteRef): RouteFunc { +export function useRouteRef( + routeRef: RouteRef, +): RouteFunc { const sourceLocation = useLocation(); const resolver = useContext(RoutingContext); const routeFunc = useMemo( @@ -111,8 +123,8 @@ export function useRouteRef(routeRef: RouteRef): RouteFunc { } type ProviderProps = { - routePaths: Map; - routeParents: Map; + routePaths: Map; + routeParents: Map; routeObjects: BackstageRouteObject[]; children: ReactNode; }; @@ -132,8 +144,8 @@ export const RoutingProvider = ({ }; export function validateRoutes( - routePaths: Map, - routeParents: Map, + routePaths: Map, + routeParents: Map, ) { const notLeafRoutes = new Set(routeParents.values()); notLeafRoutes.delete(undefined); @@ -143,7 +155,7 @@ export function validateRoutes( continue; } - let currentRouteRef: RouteRef | undefined = route; + let currentRouteRef: AnyRouteRef | undefined = route; let fullPath = ''; while (currentRouteRef) { diff --git a/packages/core-api/src/routing/index.ts b/packages/core-api/src/routing/index.ts index 45bb8975db..1493507f4e 100644 --- a/packages/core-api/src/routing/index.ts +++ b/packages/core-api/src/routing/index.ts @@ -15,5 +15,4 @@ */ export type { RouteRef, RouteRefConfig } from './types'; -export type { MutableRouteRef, AbsoluteRouteRef } from './RouteRef'; export { createRouteRef } from './RouteRef'; diff --git a/packages/core-api/src/routing/types.ts b/packages/core-api/src/routing/types.ts index 2f13a97a91..2726504ad2 100644 --- a/packages/core-api/src/routing/types.ts +++ b/packages/core-api/src/routing/types.ts @@ -16,14 +16,18 @@ import { IconComponent } from '../icons'; -export type RouteRef = { +export type RouteRef = { // TODO(Rugvip): Remove path, look up via registry instead path: string; icon?: IconComponent; title: string; + P: Params; }; -export type RouteRefConfig = { +export type AnyRouteRef = RouteRef; + +export type RouteRefConfig = { + params?: Array; path: string; icon?: IconComponent; title: string; @@ -35,5 +39,5 @@ export interface BackstageRouteObject { children?: BackstageRouteObject[]; element: React.ReactNode; path: string; - routeRef: RouteRef; + routeRef: AnyRouteRef; } From 950a3d3828c20156f397cedf6999d5a2355b703b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 7 Dec 2020 14:38:21 +0100 Subject: [PATCH 45/86] core-api: pass ReactNode as parent to collectors --- packages/core-api/src/extensions/traversal.ts | 8 ++++---- packages/core-api/src/routing/collectors.tsx | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core-api/src/extensions/traversal.ts b/packages/core-api/src/extensions/traversal.ts index 054f23daca..bdf02d16c8 100644 --- a/packages/core-api/src/extensions/traversal.ts +++ b/packages/core-api/src/extensions/traversal.ts @@ -23,7 +23,7 @@ export type Collector = () => { visit( accumulator: Result, element: ReactElement, - parent: ReactElement, + parent: ReactElement | undefined, context: Context, ): Context; }; @@ -33,7 +33,7 @@ export type Collector = () => { * varying methods to discover child nodes and collect data along the way. */ export function traverseElementTree(options: { - root: ReactElement; + root: ReactNode; discoverers: Discoverer[]; collectors: { [name in keyof Results]: Collector }; }): Results { @@ -52,14 +52,14 @@ export function traverseElementTree(options: { // Internal representation of an element in the tree that we're iterating over type QueueItem = { node: ReactNode; - parent: ReactElement; + parent: ReactElement | undefined; contexts: { [name in string]: unknown }; }; const queue = [ { node: Children.toArray(options.root), - parent: options.root, + parent: undefined, contexts: {}, } as QueueItem, ]; diff --git a/packages/core-api/src/routing/collectors.tsx b/packages/core-api/src/routing/collectors.tsx index db6673536d..e47a59aa85 100644 --- a/packages/core-api/src/routing/collectors.tsx +++ b/packages/core-api/src/routing/collectors.tsx @@ -33,7 +33,7 @@ function getMountPoint(node: ReactElement): RouteRef | undefined { export const routePathCollector = createCollector( () => new Map(), (acc, node, parent) => { - if (parent.props.element === node) { + if (parent?.props.element === node) { return; } @@ -51,7 +51,7 @@ export const routePathCollector = createCollector( export const routeParentCollector = createCollector( () => new Map(), (acc, node, parent, parentRouteRef?: RouteRef) => { - if (parent.props.element === node) { + if (parent?.props.element === node) { return parentRouteRef; } @@ -70,7 +70,7 @@ export const routeParentCollector = createCollector( export const routeObjectCollector = createCollector( () => Array(), (acc, node, parent, parentChildArr: BackstageRouteObject[] = acc) => { - if (parent.props.element === node) { + if (parent?.props.element === node) { return parentChildArr; } From d8d5a17dade9cece3479268122a722ccd191e4ee Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 7 Dec 2020 18:35:11 +0100 Subject: [PATCH 46/86] changeset: add core-api changeset for RouteRef changes --- .changeset/red-worms-fold.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/red-worms-fold.md diff --git a/.changeset/red-worms-fold.md b/.changeset/red-worms-fold.md new file mode 100644 index 0000000000..081f686659 --- /dev/null +++ b/.changeset/red-worms-fold.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-api': minor +--- + +Remove exported `ConcreteRoute`, `MutableRouteRef`, `AbsoluteRouteRef` types, and add as of yet unused `params` option to `createRouteRef`. From 0873ed2d06b885c29a445cfdb2f8425ad18b96ec Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 7 Dec 2020 19:06:24 +0100 Subject: [PATCH 47/86] core-api: switch to deprecating routing types rather than removing --- .changeset/red-worms-fold.md | 8 ++++++-- packages/core-api/src/routing/RouteRef.ts | 10 +++++++-- packages/core-api/src/routing/index.ts | 8 +++++++- packages/core-api/src/routing/types.ts | 25 ++++++++++++++++++++++- 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/.changeset/red-worms-fold.md b/.changeset/red-worms-fold.md index 081f686659..41dcc75c4c 100644 --- a/.changeset/red-worms-fold.md +++ b/.changeset/red-worms-fold.md @@ -1,5 +1,9 @@ --- -'@backstage/core-api': minor +'@backstage/core-api': patch --- -Remove exported `ConcreteRoute`, `MutableRouteRef`, `AbsoluteRouteRef` types, and add as of yet unused `params` option to `createRouteRef`. +Deprecated the `ConcreteRoute`, `MutableRouteRef`, `AbsoluteRouteRef` types and added a new `RouteRef` type as replacement. + +Deprecated and disabled the `createSubRoute` method of `AbsoluteRouteRef`. + +Add an as of yet unused `params` option to `createRouteRef`. diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts index 41384cafe2..0fbc57c6f1 100644 --- a/packages/core-api/src/routing/RouteRef.ts +++ b/packages/core-api/src/routing/RouteRef.ts @@ -32,8 +32,14 @@ export class AbsoluteRouteRef { return this.config.title; } - get P(): Params { - throw new Error("Don't call me maybe"); + /** + * This function should not be used, create a separate RouteRef instead + * @deprecated + */ + createSubRoute(): any { + throw new Error( + 'This method should not be called, create a separate RouteRef instead', + ); } toString() { diff --git a/packages/core-api/src/routing/index.ts b/packages/core-api/src/routing/index.ts index 1493507f4e..d682aa9348 100644 --- a/packages/core-api/src/routing/index.ts +++ b/packages/core-api/src/routing/index.ts @@ -14,5 +14,11 @@ * limitations under the License. */ -export type { RouteRef, RouteRefConfig } from './types'; +export type { + RouteRef, + RouteRefConfig, + AbsoluteRouteRef, + ConcreteRoute, + MutableRouteRef, +} from './types'; export { createRouteRef } from './RouteRef'; diff --git a/packages/core-api/src/routing/types.ts b/packages/core-api/src/routing/types.ts index 2726504ad2..e91ca7ea53 100644 --- a/packages/core-api/src/routing/types.ts +++ b/packages/core-api/src/routing/types.ts @@ -16,16 +16,39 @@ import { IconComponent } from '../icons'; +// @ts-ignore, we're just embedding the Params type for usage in other places export type RouteRef = { // TODO(Rugvip): Remove path, look up via registry instead path: string; icon?: IconComponent; title: string; - P: Params; + /** + * This function should not be used, create a separate RouteRef instead + * @deprecated + */ + createSubRoute(): any; }; export type AnyRouteRef = RouteRef; +/** + * This type should not be used + * @deprecated + */ +export type ConcreteRoute = {}; + +/** + * This type should not be used, use RouteRef instead + * @deprecated + */ +export type AbsoluteRouteRef = RouteRef<{}>; + +/** + * This type should not be used, use RouteRef instead + * @deprecated + */ +export type MutableRouteRef = RouteRef<{}>; + export type RouteRefConfig = { params?: Array; path: string; From 8f97b76bf52da2c13371e7846927b724c8447721 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Mon, 7 Dec 2020 14:07:07 -0500 Subject: [PATCH 48/86] Comment out unused var --- microsite/siteConfig.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/siteConfig.js b/microsite/siteConfig.js index 517cac2496..e847c8c985 100644 --- a/microsite/siteConfig.js +++ b/microsite/siteConfig.js @@ -9,7 +9,7 @@ // site configuration options. // List of projects/orgs using your project for the users page. -const users = []; +//const users = []; const siteConfig = { title: 'Backstage', // Title for your website. From cddcd2852d73712a9980b2da6045dd0ac1c64b01 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Mon, 7 Dec 2020 14:07:33 -0500 Subject: [PATCH 49/86] Remove dupe which is overwritten --- microsite/siteConfig.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/microsite/siteConfig.js b/microsite/siteConfig.js index e847c8c985..9d8d4f83e3 100644 --- a/microsite/siteConfig.js +++ b/microsite/siteConfig.js @@ -72,11 +72,6 @@ const siteConfig = { navGroupSubcategoryTitleColor: '#9e9e9e', }, - /* Colors for syntax highlighting */ - highlight: { - theme: 'dark', - }, - // This copyright info is used in /core/Footer.js and blog RSS/Atom feeds. copyright: `Copyright © ${new Date().getFullYear()} Backstage Project Authors. All rights reserved. The Linux Foundation has registered trademarks and uses trademarks. For a list of trademarks of The Linux Foundation, please see our Trademark Usage page: https://www.linuxfoundation.org/trademark-usage`, From b2db892869334f9a4323229b53cc05fa819e6b26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 7 Dec 2020 21:43:17 +0100 Subject: [PATCH 50/86] integration: addressed comments --- .changeset/nine-forks-sleep.md | 2 +- ...IntegrationsImpl.ts => ScmIntegrations.ts} | 12 ++++------ .../src/azure/AzureIntegration.test.ts | 2 +- .../integration/src/azure/AzureIntegration.ts | 2 +- .../bitbucket/BitbucketIntegration.test.ts | 2 +- .../src/bitbucket/BitbucketIntegration.ts | 2 +- .../src/github/GitHubIntegration.test.ts | 2 +- .../src/github/GitHubIntegration.ts | 2 +- .../src/gitlab/GitLabIntegration.test.ts | 2 +- .../src/gitlab/GitLabIntegration.ts | 2 +- packages/integration/src/index.ts | 3 ++- packages/integration/src/types.ts | 23 ++++++++++--------- 12 files changed, 27 insertions(+), 29 deletions(-) rename packages/integration/src/{ScmIntegrationsImpl.ts => ScmIntegrations.ts} (82%) diff --git a/.changeset/nine-forks-sleep.md b/.changeset/nine-forks-sleep.md index 7edbca2169..ce8051d35c 100644 --- a/.changeset/nine-forks-sleep.md +++ b/.changeset/nine-forks-sleep.md @@ -1,5 +1,5 @@ --- -'@backstage/integration': minor +'@backstage/integration': patch --- Add the basics of cross-integration concerns diff --git a/packages/integration/src/ScmIntegrationsImpl.ts b/packages/integration/src/ScmIntegrations.ts similarity index 82% rename from packages/integration/src/ScmIntegrationsImpl.ts rename to packages/integration/src/ScmIntegrations.ts index ef6a6835b9..515a32502d 100644 --- a/packages/integration/src/ScmIntegrationsImpl.ts +++ b/packages/integration/src/ScmIntegrations.ts @@ -22,12 +22,12 @@ import { GitLabIntegration } from './gitlab/GitLabIntegration'; import { ScmIntegration, ScmIntegrationPredicateTuple, - ScmIntegrations, + ScmIntegrationRegistry, } from './types'; -export class ScmIntegrationsImpl implements ScmIntegrations { - static fromConfig(config: Config): ScmIntegrationsImpl { - return new ScmIntegrationsImpl([ +export class ScmIntegrations implements ScmIntegrationRegistry { + static fromConfig(config: Config): ScmIntegrations { + return new ScmIntegrations([ ...AzureIntegration.factory({ config }), ...BitbucketIntegration.factory({ config }), ...GitHubIntegration.factory({ config }), @@ -41,10 +41,6 @@ export class ScmIntegrationsImpl implements ScmIntegrations { return this.integrations.map(i => i.integration); } - byName(name: string): ScmIntegration | undefined { - return this.integrations.map(i => i.integration).find(i => i.name === name); - } - byUrl(url: string): ScmIntegration | undefined { return this.integrations.find(i => i.predicate(new URL(url)))?.integration; } diff --git a/packages/integration/src/azure/AzureIntegration.test.ts b/packages/integration/src/azure/AzureIntegration.test.ts index d0d3cf65ad..aec37ce905 100644 --- a/packages/integration/src/azure/AzureIntegration.test.ts +++ b/packages/integration/src/azure/AzureIntegration.test.ts @@ -43,6 +43,6 @@ describe('AzureIntegration', () => { it('returns the basics', () => { const integration = new AzureIntegration({ host: 'h.com' } as any); expect(integration.type).toBe('azure'); - expect(integration.name).toBe('h.com'); + expect(integration.title).toBe('h.com'); }); }); diff --git a/packages/integration/src/azure/AzureIntegration.ts b/packages/integration/src/azure/AzureIntegration.ts index 2e1e4d9795..84dc3800d2 100644 --- a/packages/integration/src/azure/AzureIntegration.ts +++ b/packages/integration/src/azure/AzureIntegration.ts @@ -34,7 +34,7 @@ export class AzureIntegration implements ScmIntegration { return 'azure'; } - get name(): string { + get title(): string { return this.config.host; } } diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.test.ts b/packages/integration/src/bitbucket/BitbucketIntegration.test.ts index 9678b35014..174b6ab232 100644 --- a/packages/integration/src/bitbucket/BitbucketIntegration.test.ts +++ b/packages/integration/src/bitbucket/BitbucketIntegration.test.ts @@ -46,6 +46,6 @@ describe('BitbucketIntegration', () => { it('returns the basics', () => { const integration = new BitbucketIntegration({ host: 'h.com' } as any); expect(integration.type).toBe('bitbucket'); - expect(integration.name).toBe('h.com'); + expect(integration.title).toBe('h.com'); }); }); diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.ts b/packages/integration/src/bitbucket/BitbucketIntegration.ts index e06b539611..b271e2f408 100644 --- a/packages/integration/src/bitbucket/BitbucketIntegration.ts +++ b/packages/integration/src/bitbucket/BitbucketIntegration.ts @@ -37,7 +37,7 @@ export class BitbucketIntegration implements ScmIntegration { return 'bitbucket'; } - get name(): string { + get title(): string { return this.config.host; } } diff --git a/packages/integration/src/github/GitHubIntegration.test.ts b/packages/integration/src/github/GitHubIntegration.test.ts index 5a8800200f..3383c9ebe7 100644 --- a/packages/integration/src/github/GitHubIntegration.test.ts +++ b/packages/integration/src/github/GitHubIntegration.test.ts @@ -45,6 +45,6 @@ describe('GitHubIntegration', () => { it('returns the basics', () => { const integration = new GitHubIntegration({ host: 'h.com' } as any); expect(integration.type).toBe('github'); - expect(integration.name).toBe('h.com'); + expect(integration.title).toBe('h.com'); }); }); diff --git a/packages/integration/src/github/GitHubIntegration.ts b/packages/integration/src/github/GitHubIntegration.ts index f70684975d..92c5951873 100644 --- a/packages/integration/src/github/GitHubIntegration.ts +++ b/packages/integration/src/github/GitHubIntegration.ts @@ -37,7 +37,7 @@ export class GitHubIntegration implements ScmIntegration { return 'github'; } - get name(): string { + get title(): string { return this.config.host; } } diff --git a/packages/integration/src/gitlab/GitLabIntegration.test.ts b/packages/integration/src/gitlab/GitLabIntegration.test.ts index 5c46610c53..4a23f55816 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.test.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.test.ts @@ -43,6 +43,6 @@ describe('GitLabIntegration', () => { it('returns the basics', () => { const integration = new GitLabIntegration({ host: 'h.com' } as any); expect(integration.type).toBe('gitlab'); - expect(integration.name).toBe('h.com'); + expect(integration.title).toBe('h.com'); }); }); diff --git a/packages/integration/src/gitlab/GitLabIntegration.ts b/packages/integration/src/gitlab/GitLabIntegration.ts index 6e94e084cc..4d035cb24e 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.ts @@ -37,7 +37,7 @@ export class GitLabIntegration implements ScmIntegration { return 'gitlab'; } - get name(): string { + get title(): string { return this.config.host; } } diff --git a/packages/integration/src/index.ts b/packages/integration/src/index.ts index 9d42914fdb..fdcd7da676 100644 --- a/packages/integration/src/index.ts +++ b/packages/integration/src/index.ts @@ -18,4 +18,5 @@ export * from './azure'; export * from './bitbucket'; export * from './github'; export * from './gitlab'; -export type { ScmIntegration, ScmIntegrations } from './types'; +export { ScmIntegrations } from './ScmIntegrations'; +export type { ScmIntegration, ScmIntegrationRegistry } from './types'; diff --git a/packages/integration/src/types.ts b/packages/integration/src/types.ts index 6f9290a948..ae9c360980 100644 --- a/packages/integration/src/types.ts +++ b/packages/integration/src/types.ts @@ -20,30 +20,31 @@ import { Config } from '@backstage/config'; * Encapsulates a single SCM integration. */ export type ScmIntegration = { + /** + * The type of integration, e.g. "github". + */ type: string; - name: string; + + /** + * A human readable title for the integration, that can be shown to users to + * differentiate between different integrations. + */ + title: string; }; /** * Holds all registered SCM integrations. */ -export type ScmIntegrations = { +export type ScmIntegrationRegistry = { /** * Lists all registered integrations. */ list(): ScmIntegration[]; /** - * Fetches a named integration + * Fetches an integration by URL. * - * @param name The name of a registered integration - */ - byName(name: string): ScmIntegration | undefined; - - /** - * Fetches an integration by URL - * - * @param name A URL that matches a registered integration + * @param url A URL that matches a registered integration */ byUrl(url: string): ScmIntegration | undefined; }; From fd4a2cc5cb6baa85d796ab6567af134bc35ff90d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Dec 2020 09:12:50 +0100 Subject: [PATCH 51/86] build(deps-dev): bump js-yaml from 3.14.0 to 3.14.1 in /microsite (#3610) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.14.0 to 3.14.1. - [Release notes](https://github.com/nodeca/js-yaml/releases) - [Changelog](https://github.com/nodeca/js-yaml/blob/3.14.1/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/3.14.0...3.14.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- microsite/package.json | 2 +- microsite/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/microsite/package.json b/microsite/package.json index b0390e8c20..e1860b8314 100644 --- a/microsite/package.json +++ b/microsite/package.json @@ -16,7 +16,7 @@ "devDependencies": { "@spotify/prettier-config": "^9.0.0", "docusaurus": "^2.0.0-alpha.378053ac5", - "js-yaml": "^3.14.0", + "js-yaml": "^3.14.1", "prettier": "^2.2.1" }, "prettier": "@spotify/prettier-config" diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 02e7be47ef..e41db2a665 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -3921,10 +3921,10 @@ js-tokens@^3.0.2: resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= -js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.8.1: - version "3.14.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" - integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== +js-yaml@^3.13.1, js-yaml@^3.14.1, js-yaml@^3.8.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== dependencies: argparse "^1.0.7" esprima "^4.0.0" From 6c1aa706853f95b5ef7cbeb52ece1c8b12a7b18c Mon Sep 17 00:00:00 2001 From: David Tuite Date: Tue, 8 Dec 2020 08:19:08 +0000 Subject: [PATCH 52/86] Improve SEO of microsite title (#3608) Many people will be searching for "service catalog" and "developer platform" when they are looking for something like Backstage. They may not have ever heard of Backstage before so they will not know to search for it. A title which includes the keywords "Service Catalog" and "developer platform" will rank better for those queries. This increases the change of Backstage being found by people who could make good use of it. [Standard advice](https://moz.com/learn/seo/title-tag) is to keep page titles under 60 chars. The proposed title is 49 chars. --- microsite/siteConfig.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/siteConfig.js b/microsite/siteConfig.js index 517cac2496..3e246172f0 100644 --- a/microsite/siteConfig.js +++ b/microsite/siteConfig.js @@ -12,7 +12,7 @@ const users = []; const siteConfig = { - title: 'Backstage', // Title for your website. + title: 'Backstage Service Catalog and Developer Platform', // Title for your website. tagline: 'An open platform for building developer portals', url: 'https://backstage.io', // Your website URL cname: 'backstage.io', From 9a78b2f09c551ed2723ba5cfb3291e9803730b99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Dec 2020 09:20:01 +0100 Subject: [PATCH 53/86] build(deps): bump eslint-plugin-react-hooks from 4.0.4 to 4.2.0 (#3611) Bumps [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) from 4.0.4 to 4.2.0. - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/master/packages/eslint-plugin-react-hooks/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/HEAD/packages/eslint-plugin-react-hooks) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index fc53712aae..d39c3292f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11342,9 +11342,9 @@ eslint-plugin-notice@^0.9.10: metric-lcs "^0.1.2" eslint-plugin-react-hooks@^4.0.0: - version "4.0.4" - resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.0.4.tgz#aed33b4254a41b045818cacb047b81e6df27fa58" - integrity sha512-equAdEIsUETLFNCmmCkiCGq6rkSK5MoJhXFPFYeUebcjKgBmWWcgVOqZyQC8Bv1BwVCnTq9tBxgJFgAJTWoJtA== + version "4.2.0" + resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556" + integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ== eslint-plugin-react@^7.12.4: version "7.21.5" From 1e50e1f853a1c1dbf65a98c5ff579ae984a2cd82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Ca=C5=82us?= Date: Tue, 8 Dec 2020 12:33:44 +0100 Subject: [PATCH 54/86] Add argo-cd to plugin marketplace (#3604) * Add argo-cd to plugin marketplace * Correct the Argo CD title * Add tags to Argo CD plugin description --- microsite/data/plugins/argo-cd.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 microsite/data/plugins/argo-cd.yaml diff --git a/microsite/data/plugins/argo-cd.yaml b/microsite/data/plugins/argo-cd.yaml new file mode 100644 index 0000000000..a47fa08d82 --- /dev/null +++ b/microsite/data/plugins/argo-cd.yaml @@ -0,0 +1,12 @@ +--- +title: Argo CD +author: roadie.io +authorUrl: https://roadie.io +category: CI +description: View Argo CD status for your projects in Backstage. +documentation: https://roadie.io/backstage/plugins/argo-cd +iconUrl: https://roadie.io/images/logos/argo.png +npmPackageName: '@roadiehq/backstage-plugin-argo-cd' +tags: + - cd + - ci From 90458fed68cb06a2a65e7ba55c95bbe33dbf83f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 8 Dec 2020 13:06:17 +0100 Subject: [PATCH 55/86] cost-insights: fix react-hooks/exhaustive-deps error --- .changeset/friendly-news-know.md | 5 +++++ .../components/ProductInsightsCard/ProductInsightsChart.tsx | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 .changeset/friendly-news-know.md diff --git a/.changeset/friendly-news-know.md b/.changeset/friendly-news-know.md new file mode 100644 index 0000000000..521ac5cc28 --- /dev/null +++ b/.changeset/friendly-news-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +fix react-hooks/exhaustive-deps error diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx index 08a4c5efa2..ace2766277 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx @@ -70,8 +70,10 @@ export const ProductInsightsChart = ({ const layoutClasses = useLayoutStyles(); // Only a single entities Record for the root product entity is supported - const entityLabel = assertAlways(findAnyKey(entity.entities)); - const entities = entity.entities[entityLabel] ?? []; + const entities = useMemo(() => { + const entityLabel = assertAlways(findAnyKey(entity.entities)); + return entity.entities[entityLabel] ?? []; + }, [entity]); const [activeLabel, setActive] = useState>(); const [selectLabel, setSelected] = useState>(); From e3885c0d4ba8cf26eeb5bab1d497bc1eb8eb5128 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 8 Dec 2020 14:22:57 +0100 Subject: [PATCH 56/86] docs/FAQ: add keeping up to date and dynamic plugin installation Qs (#3615) --- docs/FAQ.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/FAQ.md b/docs/FAQ.md index 9c11b015ea..90f642e591 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -117,6 +117,41 @@ through the proxy. Learn more about [the different components](overview/what-is-backstage.md) that make up Backstage. +### How do I keep my Backstage app up to date? + +In many ways one can view Backstage as a library rather than an application or +service. The `@backstage/create-app` tool that is used to create your own +Backstage app is similar to +[`create-react-app`](https://github.com/facebook/create-react-app) in that it +gives you a starting point. The code you get is meant to be evolved, and most of +the functionality you get out of the box is brought in via NPM dependencies. +Keeping your app up to date generally means keeping your dependencies up to +date. The Backstage CLI provides a command to help you with that. Simply run +`yarn backstage-cli versions:bump` at the root of your repo, and the latest +versions of all Backstage packages will be installed. + +While staying up to date with new releases and changes will keep your app up to +date, it can often be convenient to use the changes done to the +`@backstage/create-app` template as another method to stay up to date. For that +purpose, any changes done to the template are documented along with upgrade +instructions in the +[changelog](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md) +of the `@backstage/create-app` package. + +### Why can't I dynamically install plugins without modifications the app? + +This decision is part of the core architecture and development flow of +Backstage. Plugins have a lot of freedom in what they provide and how they are +integrated into the app, and it would therefore add a lot of complexity to allow +plugins to be integrated via configuration the same way as they can be +integrated with code. + +By bundling all plugins and their dependencies into one app bundle it is also +possible to do significant optimizations to the app load time by allowing +plugins to share dependencies between each other when possible. This contributes +to Backstage being fast, which is an important part of the user and developer +experience. + ### Do I have to write plugins in TypeScript? No, you can use JavaScript if you prefer. We want to keep the Backstage core From 04f26f88d0eda8c31f123a0219cd1d87d7ab69ee Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Tue, 8 Dec 2020 14:28:21 +0100 Subject: [PATCH 57/86] Export the defaultConfigLoader (#3603) --- .changeset/tall-hairs-switch.md | 5 +++++ packages/core/src/api-wrappers/index.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/tall-hairs-switch.md diff --git a/.changeset/tall-hairs-switch.md b/.changeset/tall-hairs-switch.md new file mode 100644 index 0000000000..2a2f915a6c --- /dev/null +++ b/.changeset/tall-hairs-switch.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Export the `defaultConfigLoader` implementation diff --git a/packages/core/src/api-wrappers/index.ts b/packages/core/src/api-wrappers/index.ts index b8136305b0..42c423d868 100644 --- a/packages/core/src/api-wrappers/index.ts +++ b/packages/core/src/api-wrappers/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { createApp } from './createApp'; +export { createApp, defaultConfigLoader } from './createApp'; From e2c3f2864f20fe09c4259583c0f6557ad80c8320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikael=20=C3=96stberg?= Date: Tue, 8 Dec 2020 14:43:48 +0100 Subject: [PATCH 58/86] Update ADOPTERS.md (#3612) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update ADOPTERS.md * Update vocab.txt * Ran prettier Co-authored-by: Stefan Ålund --- .github/styles/vocab.txt | 1 + ADOPTERS.md | 31 ++++++++++++++++--------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 7f889392ac..35251ef502 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -198,6 +198,7 @@ talkdesk Talkdesk tasklist techdocs +Telenor templated templater Templater diff --git a/ADOPTERS.md b/ADOPTERS.md index 0ba3e3196b..b6455e09b0 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -1,15 +1,16 @@ -| Organization | Contact | Description of Use | -| -------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | -| [Spotify](https://www.spotify.com) | [@stefanalund](https://github.com/stefanalund) | Main interface towards all of Spotify's infrastructure and technical documentation. | -| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. | -| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | -| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | -| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | -| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. | -| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | -| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | -| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. | -| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | -| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D | -| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling | -| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling | +| Organization | Contact | Description of Use | +| -------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | +| [Spotify](https://www.spotify.com) | [@stefanalund](https://github.com/stefanalund) | Main interface towards all of Spotify's infrastructure and technical documentation. | +| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. | +| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | +| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | +| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | +| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. | +| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | +| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | +| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. | +| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | +| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D | +| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling | +| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling | +| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks | From e708679d7706829a56d4d1e68ce442691ef2c876 Mon Sep 17 00:00:00 2001 From: Govindarajan Nagarajan Date: Tue, 8 Dec 2020 14:13:09 +0100 Subject: [PATCH 59/86] `refreshAllLocations` uses a child logger with meta. Refs #3602 When there are a large number of location, `refreshAllLocations` can emit a lot of logs that are noisy and can make debugging hard. To solve this problem, create a child logger with a meta : `component` = `refreshAllLocations`. That can be used to filter out logs if needed --- .changeset/flat-flowers-learn.md | 5 +++++ .../src/ingestion/HigherOrderOperations.ts | 22 ++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 .changeset/flat-flowers-learn.md diff --git a/.changeset/flat-flowers-learn.md b/.changeset/flat-flowers-learn.md new file mode 100644 index 0000000000..cf93da57cf --- /dev/null +++ b/.changeset/flat-flowers-learn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +refreshAllLocations uses a child logger of the HigherOrderOperation with a meta `component` : `catalog-all-locations-refresh` diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts index ca19a1d1c5..565c3671ee 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts @@ -116,28 +116,34 @@ export class HigherOrderOperations implements HigherOrderOperation { */ async refreshAllLocations(): Promise { const startTimestamp = process.hrtime(); - this.logger.info('Beginning locations refresh'); + const logger = this.logger.child({ + component: 'catalog-all-locations-refresh', + }); + + logger.info('Locations Refresh: Beginning locations refresh'); const locations = await this.locationsCatalog.locations(); - this.logger.info(`Visiting ${locations.length} locations`); + logger.info(`Locations Refresh: Visiting ${locations.length} locations`); for (const { data: location } of locations) { - this.logger.info( - `Refreshing location ${location.type}:${location.target}`, + logger.info( + `Locations Refresh: Refreshing location ${location.type}:${location.target}`, ); try { await this.refreshSingleLocation(location); await this.locationsCatalog.logUpdateSuccess(location.id, undefined); } catch (e) { - this.logger.warn( - `Failed to refresh location ${location.type}:${location.target}, ${e.stack}`, + logger.warn( + `Locations Refresh: Failed to refresh location ${location.type}:${location.target}, ${e.stack}`, ); await this.locationsCatalog.logUpdateFailure(location.id, e); } } - this.logger.info( - `Completed locations refresh in ${durationText(startTimestamp)}`, + logger.info( + `Locations Refresh: Completed locations refresh in ${durationText( + startTimestamp, + )}`, ); } From 9b3442f53626be7829f9ccfc8aa994eb4ce68275 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Tue, 8 Dec 2020 09:15:18 -0500 Subject: [PATCH 60/86] Fully remove sample users var --- microsite/siteConfig.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/microsite/siteConfig.js b/microsite/siteConfig.js index 9d8d4f83e3..4a49dcac25 100644 --- a/microsite/siteConfig.js +++ b/microsite/siteConfig.js @@ -8,9 +8,6 @@ // See https://docusaurus.io/docs/site-config for all the possible // site configuration options. -// List of projects/orgs using your project for the users page. -//const users = []; - const siteConfig = { title: 'Backstage', // Title for your website. tagline: 'An open platform for building developer portals', From 1f0f4ed3bcd1983819448f0918039dc6473fdbc2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 8 Dec 2020 16:26:53 +0100 Subject: [PATCH 61/86] Update Footer.js --- microsite/core/Footer.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/microsite/core/Footer.js b/microsite/core/Footer.js index 4a5bca6071..b97d1a5253 100644 --- a/microsite/core/Footer.js +++ b/microsite/core/Footer.js @@ -1,6 +1,4 @@ /* -* Made with <3 at Spotify - * Copyright 2020 Backstage Project Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -88,6 +86,9 @@ class Footer extends React.Component { )}
+

+ Made with ❤️  at Spotify +

{this.props.config.copyright}

); From cd4eba4afc976f7317a234088ac36f5283c645d6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 8 Dec 2020 16:30:42 +0100 Subject: [PATCH 62/86] Update Footer.js - formatting fix --- microsite/core/Footer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/core/Footer.js b/microsite/core/Footer.js index b97d1a5253..d2d0f525ee 100644 --- a/microsite/core/Footer.js +++ b/microsite/core/Footer.js @@ -86,7 +86,7 @@ class Footer extends React.Component { )} -

+

Made with ❤️  at Spotify

{this.props.config.copyright}

From 0bc10faf6f24617de14cc72e76e7d92e60e7ac0c Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Tue, 8 Dec 2020 14:42:02 +0100 Subject: [PATCH 63/86] Setup a devapp for SonarQube that shows the different states of the widget --- plugins/sonarqube/dev/index.tsx | 146 +++++++++++++++++- plugins/sonarqube/src/api/SonarQubeApi.ts | 42 +++++ ...{index.test.ts => SonarQubeClient.test.ts} | 8 +- plugins/sonarqube/src/api/SonarQubeClient.ts | 101 ++++++++++++ plugins/sonarqube/src/api/index.ts | 112 +------------- plugins/sonarqube/src/plugin.ts | 4 +- 6 files changed, 296 insertions(+), 117 deletions(-) create mode 100644 plugins/sonarqube/src/api/SonarQubeApi.ts rename plugins/sonarqube/src/api/{index.test.ts => SonarQubeClient.test.ts} (96%) create mode 100644 plugins/sonarqube/src/api/SonarQubeClient.ts diff --git a/plugins/sonarqube/dev/index.tsx b/plugins/sonarqube/dev/index.tsx index 812a5585d4..830d8aa575 100644 --- a/plugins/sonarqube/dev/index.tsx +++ b/plugins/sonarqube/dev/index.tsx @@ -14,7 +14,149 @@ * limitations under the License. */ +import { Entity } from '@backstage/catalog-model'; +import { + Content, + createPlugin, + createRouteRef, + Header, + Page, +} from '@backstage/core'; import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { Grid } from '@material-ui/core'; +import React from 'react'; +import { SonarQubeCard } from '../src'; +import { FindingSummary, SonarQubeApi, sonarQubeApiRef } from '../src/api'; +import { SONARQUBE_PROJECT_KEY_ANNOTATION } from '../src/components/useProjectKey'; -createDevApp().registerPlugin(plugin).render(); +createDevApp() + .registerApi({ + api: sonarQubeApiRef, + deps: {}, + factory: () => + ({ + getFindingSummary: async componentKey => { + switch (componentKey) { + case 'error': + throw new Error('Error!'); + + case 'never': + return new Promise(() => {}); + + case 'not-computed': + return { + lastAnalysis: new Date().toISOString(), + metrics: { + bugs: '0', + reliability_rating: '1.0', + vulnerabilities: '0', + security_rating: '1.0', + code_smells: '0', + sqale_rating: '1.0', + coverage: '0.0', + duplicated_lines_density: '0.0', + }, + projectUrl: `/#${componentKey}`, + getIssuesUrl: i => `/#${componentKey}/issues/${i}`, + getComponentMeasuresUrl: i => `/#${componentKey}/measures/${i}`, + } as FindingSummary; + + case 'failed': + return { + lastAnalysis: new Date().toISOString(), + metrics: { + alert_status: 'FAILED', + bugs: '4', + reliability_rating: '2.0', + vulnerabilities: '18', + security_rating: '3.0', + code_smells: '22', + sqale_rating: '5.0', + coverage: '15.7', + duplicated_lines_density: '15.6', + }, + projectUrl: `/#${componentKey}`, + getIssuesUrl: i => `/#${componentKey}/issues/${i}`, + getComponentMeasuresUrl: i => `/#${componentKey}/measures/${i}`, + } as FindingSummary; + + case 'passed': + return { + lastAnalysis: new Date().toISOString(), + metrics: { + alert_status: 'OK', + bugs: '0', + reliability_rating: '1.0', + vulnerabilities: '0', + security_rating: '1.0', + code_smells: '0', + sqale_rating: '1.0', + coverage: '100.0', + duplicated_lines_density: '0.0', + }, + projectUrl: `/#${componentKey}`, + getIssuesUrl: i => `/#${componentKey}/issues/${i}`, + getComponentMeasuresUrl: i => `/#${componentKey}/measures/${i}`, + } as FindingSummary; + + default: + return undefined; + } + }, + } as SonarQubeApi), + }) + .registerPlugin( + createPlugin({ + id: 'defectdojo-demo', + register({ router }) { + const entity = (name?: string) => + ({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + annotations: { + [SONARQUBE_PROJECT_KEY_ANNOTATION]: name, + }, + name: name, + }, + } as Entity); + + const ExamplePage = () => ( + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + ); + + router.addRoute( + createRouteRef({ path: '/', title: 'SonarQube' }), + ExamplePage, + ); + }, + }), + ) + .render(); diff --git a/plugins/sonarqube/src/api/SonarQubeApi.ts b/plugins/sonarqube/src/api/SonarQubeApi.ts new file mode 100644 index 0000000000..5734f9a3ca --- /dev/null +++ b/plugins/sonarqube/src/api/SonarQubeApi.ts @@ -0,0 +1,42 @@ +/* + * 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 { MetricKey, SonarUrlProcessorFunc } from './types'; + +/** + * Define a type to make sure that all metrics are used + */ +export type Metrics = { + [key in MetricKey]: string | undefined; +}; + +export interface FindingSummary { + lastAnalysis: string; + metrics: Metrics; + projectUrl: string; + getIssuesUrl: SonarUrlProcessorFunc; + getComponentMeasuresUrl: SonarUrlProcessorFunc; +} + +export const sonarQubeApiRef = createApiRef({ + id: 'plugin.sonarqube.service', + description: 'Used by the SonarQube plugin to make requests', +}); + +export type SonarQubeApi = { + getFindingSummary(componentKey?: string): Promise; +}; diff --git a/plugins/sonarqube/src/api/index.test.ts b/plugins/sonarqube/src/api/SonarQubeClient.test.ts similarity index 96% rename from plugins/sonarqube/src/api/index.test.ts rename to plugins/sonarqube/src/api/SonarQubeClient.test.ts index 2d96fc2135..a3ae24de52 100644 --- a/plugins/sonarqube/src/api/index.test.ts +++ b/plugins/sonarqube/src/api/SonarQubeClient.test.ts @@ -18,12 +18,12 @@ import { UrlPatternDiscovery } from '@backstage/core'; import { msw } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { FindingSummary, SonarQubeApi } from './index'; +import { FindingSummary, SonarQubeClient } from './index'; import { ComponentWrapper, MeasuresWrapper } from './types'; const server = setupServer(); -describe('SonarQubeApi', () => { +describe('SonarQubeClient', () => { msw.setupDefaultHandlers(server); const mockBaseUrl = 'http://backstage:9191/api/proxy'; @@ -111,7 +111,7 @@ describe('SonarQubeApi', () => { it('should report finding summary', async () => { setupHandlers(); - const client = new SonarQubeApi({ discoveryApi }); + const client = new SonarQubeClient({ discoveryApi }); const summary = await client.getFindingSummary('our-service'); expect(summary).toEqual( @@ -142,7 +142,7 @@ describe('SonarQubeApi', () => { it('should report finding summary (custom baseUrl)', async () => { setupHandlers(); - const client = new SonarQubeApi({ + const client = new SonarQubeClient({ discoveryApi, baseUrl: 'http://a.instance.local', }); diff --git a/plugins/sonarqube/src/api/SonarQubeClient.ts b/plugins/sonarqube/src/api/SonarQubeClient.ts new file mode 100644 index 0000000000..893f32b0a6 --- /dev/null +++ b/plugins/sonarqube/src/api/SonarQubeClient.ts @@ -0,0 +1,101 @@ +/* + * 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 { DiscoveryApi } from '@backstage/core'; +import fetch from 'cross-fetch'; +import { FindingSummary, Metrics, SonarQubeApi } from './SonarQubeApi'; +import { ComponentWrapper, MeasuresWrapper } from './types'; + +export class SonarQubeClient implements SonarQubeApi { + discoveryApi: DiscoveryApi; + baseUrl: string; + + constructor({ + discoveryApi, + baseUrl = 'https://sonarcloud.io/', + }: { + discoveryApi: DiscoveryApi; + baseUrl?: string; + }) { + this.discoveryApi = discoveryApi; + this.baseUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; + } + + private async callApi(path: string): Promise { + const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/sonarqube`; + const response = await fetch(`${apiUrl}/${path}`); + if (response.status === 200) { + return (await response.json()) as T; + } + return undefined; + } + + async getFindingSummary( + componentKey?: string, + ): Promise { + if (!componentKey) { + return undefined; + } + + const component = await this.callApi( + `components/show?component=${componentKey}`, + ); + if (!component) { + return undefined; + } + + const metrics: Metrics = { + alert_status: undefined, + bugs: undefined, + reliability_rating: undefined, + vulnerabilities: undefined, + security_rating: undefined, + code_smells: undefined, + sqale_rating: undefined, + coverage: undefined, + duplicated_lines_density: undefined, + }; + + const measures = await this.callApi( + `measures/search?projectKeys=${componentKey}&metricKeys=${Object.keys( + metrics, + ).join(',')}`, + ); + if (!measures) { + return undefined; + } + + measures.measures + .filter(m => m.component === componentKey) + .forEach(m => { + metrics[m.metric] = m.value; + }); + + return { + lastAnalysis: component.component.analysisDate, + metrics, + projectUrl: `${this.baseUrl}dashboard?id=${componentKey}`, + getIssuesUrl: identifier => + `${ + this.baseUrl + }project/issues?id=${componentKey}&types=${identifier.toUpperCase()}&resolved=false`, + getComponentMeasuresUrl: (identifier: string) => + `${ + this.baseUrl + }component_measures?id=${componentKey}&metric=${identifier.toLowerCase()}&resolved=false&view=list`, + }; + } +} diff --git a/plugins/sonarqube/src/api/index.ts b/plugins/sonarqube/src/api/index.ts index f2c616f53c..8442465dee 100644 --- a/plugins/sonarqube/src/api/index.ts +++ b/plugins/sonarqube/src/api/index.ts @@ -14,112 +14,6 @@ * limitations under the License. */ -import { createApiRef, DiscoveryApi } from '@backstage/core'; -import fetch from 'cross-fetch'; -import { - ComponentWrapper, - MeasuresWrapper, - MetricKey, - SonarUrlProcessorFunc, -} from './types'; - -/** - * Define a type to make sure that all metrics are used - */ -type Metrics = { - [key in MetricKey]: string | undefined; -}; - -export interface FindingSummary { - lastAnalysis: string; - metrics: Metrics; - projectUrl: string; - getIssuesUrl: SonarUrlProcessorFunc; - getComponentMeasuresUrl: SonarUrlProcessorFunc; -} - -export const sonarQubeApiRef = createApiRef({ - id: 'plugin.sonarqube.service', - description: 'Used by the SonarQube plugin to make requests', -}); - -export class SonarQubeApi { - discoveryApi: DiscoveryApi; - baseUrl: string; - - constructor({ - discoveryApi, - baseUrl = 'https://sonarcloud.io/', - }: { - discoveryApi: DiscoveryApi; - baseUrl?: string; - }) { - this.discoveryApi = discoveryApi; - this.baseUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; - } - - private async callApi(path: string): Promise { - const apiUrl = `${await this.discoveryApi.getBaseUrl('proxy')}/sonarqube`; - const response = await fetch(`${apiUrl}/${path}`); - if (response.status === 200) { - return (await response.json()) as T; - } - return undefined; - } - - async getFindingSummary( - componentKey?: string, - ): Promise { - if (!componentKey) { - return undefined; - } - - const component = await this.callApi( - `components/show?component=${componentKey}`, - ); - if (!component) { - return undefined; - } - - const metrics: Metrics = { - alert_status: undefined, - bugs: undefined, - reliability_rating: undefined, - vulnerabilities: undefined, - security_rating: undefined, - code_smells: undefined, - sqale_rating: undefined, - coverage: undefined, - duplicated_lines_density: undefined, - }; - - const measures = await this.callApi( - `measures/search?projectKeys=${componentKey}&metricKeys=${Object.keys( - metrics, - ).join(',')}`, - ); - if (!measures) { - return undefined; - } - - measures.measures - .filter(m => m.component === componentKey) - .forEach(m => { - metrics[m.metric] = m.value; - }); - - return { - lastAnalysis: component.component.analysisDate, - metrics, - projectUrl: `${this.baseUrl}dashboard?id=${componentKey}`, - getIssuesUrl: identifier => - `${ - this.baseUrl - }project/issues?id=${componentKey}&types=${identifier.toUpperCase()}&resolved=false`, - getComponentMeasuresUrl: (identifier: string) => - `${ - this.baseUrl - }component_measures?id=${componentKey}&metric=${identifier.toLowerCase()}&resolved=false&view=list`, - }; - } -} +export type { Metrics, FindingSummary, SonarQubeApi } from './SonarQubeApi'; +export { sonarQubeApiRef } from './SonarQubeApi'; +export { SonarQubeClient } from './SonarQubeClient'; diff --git a/plugins/sonarqube/src/plugin.ts b/plugins/sonarqube/src/plugin.ts index 154d4ee3b8..f8b8cafc5c 100644 --- a/plugins/sonarqube/src/plugin.ts +++ b/plugins/sonarqube/src/plugin.ts @@ -20,7 +20,7 @@ import { createPlugin, discoveryApiRef, } from '@backstage/core'; -import { SonarQubeApi, sonarQubeApiRef } from './api'; +import { sonarQubeApiRef, SonarQubeClient } from './api'; export const plugin = createPlugin({ id: 'sonarqube', @@ -29,7 +29,7 @@ export const plugin = createPlugin({ api: sonarQubeApiRef, deps: { configApi: configApiRef, discoveryApi: discoveryApiRef }, factory: ({ configApi, discoveryApi }) => - new SonarQubeApi({ + new SonarQubeClient({ discoveryApi, baseUrl: configApi.getOptionalString('sonarQube.baseUrl'), }), From 447c9d0a842e08031169f25512cf58d23ef490e5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 8 Dec 2020 16:34:04 +0100 Subject: [PATCH 64/86] Update Footer.js - another formatting fix --- microsite/core/Footer.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/microsite/core/Footer.js b/microsite/core/Footer.js index d2d0f525ee..1471d7b4d1 100644 --- a/microsite/core/Footer.js +++ b/microsite/core/Footer.js @@ -54,14 +54,12 @@ class Footer extends React.Component { Open Source @ {this.props.config.organizationName} - + Spotify Engineering Blog - - Spotify for Developers - - + Spotify for Developers + GitHub Date: Tue, 8 Dec 2020 16:53:51 +0100 Subject: [PATCH 65/86] Setup a devapp for Sentry that shows the different states of the widget --- plugins/sentry/dev/index.tsx | 91 +++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/plugins/sentry/dev/index.tsx b/plugins/sentry/dev/index.tsx index 812a5585d4..fc75008040 100644 --- a/plugins/sentry/dev/index.tsx +++ b/plugins/sentry/dev/index.tsx @@ -14,7 +14,94 @@ * limitations under the License. */ +import { Entity } from '@backstage/catalog-model'; +import { + Content, + createPlugin, + createRouteRef, + Header, + Page, +} from '@backstage/core'; import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { Grid } from '@material-ui/core'; +import React from 'react'; +import { + MockSentryApi, + SentryApi, + sentryApiRef, + SentryIssuesWidget, +} from '../src'; +import { SENTRY_PROJECT_SLUG_ANNOTATION } from '../src/components/useProjectSlug'; -createDevApp().registerPlugin(plugin).render(); +createDevApp() + .registerApi({ + api: sentryApiRef, + deps: {}, + factory: () => + ({ + fetchIssues: async (project: string) => { + switch (project) { + case 'error': + throw new Error('Error!'); + + case 'never': + return new Promise(() => {}); + + case 'with-values': + return new MockSentryApi().fetchIssues(); + + default: + return []; + } + }, + } as SentryApi), + }) + .registerPlugin( + createPlugin({ + id: 'sentry-demo', + register({ router }) { + const entity = (name?: string) => + ({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + annotations: { + [SENTRY_PROJECT_SLUG_ANNOTATION]: name, + }, + name: name, + }, + } as Entity); + + const ExamplePage = () => ( + +
+ + + + + + + + + + + + + + + + + + + + + ); + + router.addRoute( + createRouteRef({ path: '/', title: 'Sentry' }), + ExamplePage, + ); + }, + }), + ) + .render(); From 8f996c88b09b08c4d7f5648715001a1747eb98ba Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Tue, 8 Dec 2020 12:15:12 -0500 Subject: [PATCH 66/86] Add test for quarter end date function --- .../cost-insights/src/utils/duration.test.ts | 17 ++++++++++++++++- plugins/cost-insights/src/utils/duration.ts | 14 ++++++++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/plugins/cost-insights/src/utils/duration.test.ts b/plugins/cost-insights/src/utils/duration.test.ts index 45d8e5b6a3..47769a45f1 100644 --- a/plugins/cost-insights/src/utils/duration.test.ts +++ b/plugins/cost-insights/src/utils/duration.test.ts @@ -15,7 +15,11 @@ */ import { Duration } from '../types'; -import { inclusiveEndDateOf, inclusiveStartDateOf } from './duration'; +import { + inclusiveEndDateOf, + inclusiveStartDateOf, + quarterEndDate, +} from './duration'; const lastCompleteBillingDate = '2020-06-05'; @@ -32,3 +36,14 @@ describe.each` expect(inclusiveEndDateOf(duration, lastCompleteBillingDate)).toBe(endDate); }); }); + +describe.each` + inclusiveEndDate | expectedQuarterEndDate + ${'2020-12-31'} | ${'2020-12-31'} + ${'2020-12-30'} | ${'2020-09-30'} + ${'2021-02-19'} | ${'2020-12-31'} +`('quarterEndDate', ({ inclusiveEndDate, expectedQuarterEndDate }) => { + it(`calculates quarter end date correctly from inclusive end date ${inclusiveEndDate}`, () => { + expect(quarterEndDate(inclusiveEndDate)).toBe(expectedQuarterEndDate); + }); +}); diff --git a/plugins/cost-insights/src/utils/duration.ts b/plugins/cost-insights/src/utils/duration.ts index 0d9d6aa179..7a330b6f91 100644 --- a/plugins/cost-insights/src/utils/duration.ts +++ b/plugins/cost-insights/src/utils/duration.ts @@ -60,7 +60,10 @@ export function exclusiveEndDateOf( .add(1, 'day') .format(DEFAULT_DATE_FORMAT); case Duration.P3M: - return quarterEndDate(inclusiveEndDate); + return moment(quarterEndDate(inclusiveEndDate)) + .utc() + .add(1, 'day') + .format(DEFAULT_DATE_FORMAT); default: return assertNever(duration); } @@ -81,11 +84,14 @@ export function intervalsOf(duration: Duration, inclusiveEndDate: string) { return `R2/${duration}/${exclusiveEndDateOf(duration, inclusiveEndDate)}`; } -function quarterEndDate(inclusiveEndDate: string): string { +export function quarterEndDate(inclusiveEndDate: string): string { const endDate = moment(inclusiveEndDate).utc(); const endOfQuarter = endDate.endOf('quarter').format(DEFAULT_DATE_FORMAT); if (endOfQuarter === inclusiveEndDate) { - return endDate.add(1, 'day').format(DEFAULT_DATE_FORMAT); + return endDate.format(DEFAULT_DATE_FORMAT); } - return endDate.startOf('quarter').format(DEFAULT_DATE_FORMAT); + return endDate + .startOf('quarter') + .subtract(1, 'day') + .format(DEFAULT_DATE_FORMAT); } From 8b395bee268a5c6ed22b0f622c7cb19c021de2cc Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Tue, 8 Dec 2020 13:42:23 -0500 Subject: [PATCH 67/86] Add standard NPM metadata --- plugins/app-backend/package.json | 9 +++++++++ plugins/auth-backend/package.json | 9 +++++++++ plugins/catalog-backend/package.json | 9 +++++++++ plugins/catalog-graphql/package.json | 10 ++++++++++ plugins/catalog-import/package.json | 9 +++++++++ plugins/catalog/package.json | 9 +++++++++ plugins/circleci/package.json | 10 ++++++++++ plugins/cloudbuild/package.json | 10 ++++++++++ plugins/cost-insights/package.json | 9 +++++++++ plugins/explore/package.json | 9 +++++++++ plugins/gcp-projects/package.json | 10 ++++++++++ plugins/github-actions/package.json | 11 +++++++++++ plugins/gitops-profiles/package.json | 10 ++++++++++ plugins/graphql/package.json | 10 ++++++++++ plugins/jenkins/package.json | 10 ++++++++++ plugins/kubernetes-backend/package.json | 10 ++++++++++ plugins/kubernetes/package.json | 10 ++++++++++ plugins/lighthouse/package.json | 10 ++++++++++ plugins/newrelic/package.json | 10 ++++++++++ plugins/pagerduty/package.json | 10 ++++++++++ plugins/proxy-backend/package.json | 9 +++++++++ plugins/register-component/package.json | 9 +++++++++ plugins/rollbar-backend/package.json | 10 ++++++++++ plugins/rollbar/package.json | 10 ++++++++++ plugins/scaffolder-backend/package.json | 9 +++++++++ plugins/scaffolder/package.json | 9 +++++++++ plugins/search/package.json | 9 +++++++++ plugins/sentry/package.json | 10 ++++++++++ plugins/sonarqube/package.json | 11 +++++++++++ plugins/tech-radar/package.json | 9 +++++++++ plugins/techdocs-backend/package.json | 10 ++++++++++ plugins/techdocs/package.json | 10 ++++++++++ plugins/user-settings/package.json | 9 +++++++++ plugins/welcome/package.json | 9 +++++++++ 34 files changed, 327 insertions(+) diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 6d2fdcb67c..d64a58e017 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -10,6 +10,15 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/app-backend" + }, + "keywords": [ + "backstage" + ], "scripts": { "start": "backstage-cli backend:dev", "build": "backstage-cli backend:build", diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index ddca6a24b4..5440a1342d 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -10,6 +10,15 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/auth-backend" + }, + "keywords": [ + "backstage" + ], "scripts": { "start": "backstage-cli backend:dev", "build": "backstage-cli backend:build", diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 69f260d020..19947d65f7 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -10,6 +10,15 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog-backend" + }, + "keywords": [ + "backstage" + ], "scripts": { "start": "backstage-cli backend:dev", "build": "backstage-cli backend:build", diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index 9b12250ca0..3f28c023fd 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -9,6 +9,16 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog-graphql" + }, + "keywords": [ + "backstage", + "graphql" + ], "scripts": { "start": "backstage-cli backend:dev", "build": "backstage-cli backend:build", diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 74cb3635be..9f5f9b9913 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -10,6 +10,15 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog-import" + }, + "keywords": [ + "backstage" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 6887f71b3b..93d8638353 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -10,6 +10,15 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog" + }, + "keywords": [ + "backstage" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index c67696244c..d62dac8d21 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -10,6 +10,16 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/circleci" + }, + "keywords": [ + "backstage", + "circleci" + ], "scripts": { "build": "backstage-cli plugin:build", "lint": "backstage-cli lint", diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 3e514a5eb8..8e52b29442 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -9,6 +9,16 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/cloudbuild" + }, + "keywords": [ + "backstage", + "google cloud" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 9e0f890fcd..f4db2c98e0 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -10,6 +10,15 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/cost-insights" + }, + "keywords": [ + "backstage" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 45cbb310f8..abf730a66f 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -10,6 +10,15 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/explore" + }, + "keywords": [ + "backstage" + ], "scripts": { "build": "backstage-cli plugin:build", "lint": "backstage-cli lint", diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index faa79ed647..95a226ef3a 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -9,6 +9,16 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/gcp-projects" + }, + "keywords": [ + "backstage", + "google cloud" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 5822a0e608..7f4999f107 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -10,6 +10,17 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/github-actions" + }, + "keywords": [ + "backstage", + "github", + "github actions" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 3adf7291db..e4662e2465 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -10,6 +10,16 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/gitops-profiles" + }, + "keywords": [ + "backstage", + "gitops" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/graphql/package.json b/plugins/graphql/package.json index f9121ebd9a..f746a612f1 100644 --- a/plugins/graphql/package.json +++ b/plugins/graphql/package.json @@ -9,6 +9,16 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/graphql-backend" + }, + "keywords": [ + "backstage", + "graphql" + ], "scripts": { "start": "backstage-cli backend:dev", "build": "backstage-cli backend:build", diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 64bb7dfb20..f38fd28552 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -10,6 +10,16 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/jenkins" + }, + "keywords": [ + "backstage", + "jenkins" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 9d1ff2eed4..92439b8994 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -10,6 +10,16 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/kubernetes-backend" + }, + "keywords": [ + "backstage", + "kubernetes" + ], "configSchema": "schema.d.ts", "scripts": { "start": "backstage-cli backend:dev", diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 6fa2f7e3ff..5fbb764092 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -9,6 +9,16 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/kubernetes" + }, + "keywords": [ + "backstage", + "kubernetes" + ], "configSchema": "schema.d.ts", "scripts": { "build": "backstage-cli plugin:build", diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index da824044a9..34e2bff874 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -10,6 +10,16 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/lighthouse" + }, + "keywords": [ + "backstage", + "lighthouse" + ], "scripts": { "build": "backstage-cli plugin:build", "lint": "backstage-cli lint", diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 9cfb810f4a..952a8c2388 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -10,6 +10,16 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/newrelic" + }, + "keywords": [ + "backstage", + "newrelic" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 4d58a1da42..d7e2fd7b6f 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -9,6 +9,16 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/pagerduty" + }, + "keywords": [ + "backstage", + "pagerduty" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 68c89169c5..5929dd55d0 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -9,6 +9,15 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/proxy-backend" + }, + "keywords": [ + "backstage" + ], "scripts": { "start": "backstage-cli backend:dev", "build": "backstage-cli backend:build", diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 18a97e36f7..f40f86af5a 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -10,6 +10,15 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/register-component" + }, + "keywords": [ + "backstage" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index edaaa3a9b5..74f83cb0d3 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -10,6 +10,16 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/rollbar-backend" + }, + "keywords": [ + "backstage", + "rollbar" + ], "scripts": { "start": "backstage-cli backend:dev", "build": "backstage-cli backend:build", diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 9cdd7895bf..bf60120486 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -10,6 +10,16 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/rollbar" + }, + "keywords": [ + "backstage", + "rollbar" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 7aa8fc873a..3cfcee3fd9 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -10,6 +10,15 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/scaffolder-backend" + }, + "keywords": [ + "backstage" + ], "scripts": { "start": "backstage-cli backend:dev", "build": "backstage-cli backend:build", diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 381f2bfdbf..869e0ef09c 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -10,6 +10,15 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/scaffolder" + }, + "keywords": [ + "backstage" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/search/package.json b/plugins/search/package.json index 490d5003ef..5e34731cdb 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -9,6 +9,15 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/search" + }, + "keywords": [ + "backstage" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index c9708ee875..6b235243ea 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -10,6 +10,16 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/sentry" + }, + "keywords": [ + "backstage", + "sentry" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 2dd52ab021..46168fe617 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -10,6 +10,17 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/sonarqube" + }, + "keywords": [ + "backstage", + "sonarqube", + "sonarcloud" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 269e0e0b4f..740ab24887 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -10,6 +10,15 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/tech-radar" + }, + "keywords": [ + "backstage" + ], "scripts": { "build": "backstage-cli plugin:build", "lint": "backstage-cli lint", diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 47b4a7cd9a..54abcf7031 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -10,6 +10,16 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/techdocs-backend" + }, + "keywords": [ + "backstage", + "techdocs" + ], "scripts": { "start": "backstage-cli backend:dev", "build": "backstage-cli backend:build", diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index d3cd6cdf64..d1579a5072 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -10,6 +10,16 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/techdocs" + }, + "keywords": [ + "backstage", + "techdocs" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index e1b94e85d9..493f9b646b 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -10,6 +10,15 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/user-settings" + }, + "keywords": [ + "backstage" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index fe1d2e1604..d44ea57e81 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -10,6 +10,15 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/welcome" + }, + "keywords": [ + "backstage" + ], "scripts": { "build": "backstage-cli plugin:build", "lint": "backstage-cli lint", From c67d3143ef29b9c7d90e495cc28c5c30c08c8a60 Mon Sep 17 00:00:00 2001 From: AlleyWei <64219471+AlleyWei@users.noreply.github.com> Date: Tue, 8 Dec 2020 17:49:49 -0500 Subject: [PATCH 68/86] Update 2020-03-16-announcing-backstage.md --- microsite/blog/2020-03-16-announcing-backstage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/blog/2020-03-16-announcing-backstage.md b/microsite/blog/2020-03-16-announcing-backstage.md index 5824b76a57..5094de7b37 100644 --- a/microsite/blog/2020-03-16-announcing-backstage.md +++ b/microsite/blog/2020-03-16-announcing-backstage.md @@ -1,6 +1,6 @@ --- title: Announcing Backstage -author: Stefan Ålund +author: Stefan Ålund, Spotify authorURL: http://twitter.com/stalund authorImageURL: https://pbs.twimg.com/profile_images/121166861/6919c047c0d0edaace78c3009b28e917-user-full-200-130.generated_400x400.jpg --- From c2c1943b77f3b93dd8fb1d82288addbf65872fb1 Mon Sep 17 00:00:00 2001 From: AlleyWei <64219471+AlleyWei@users.noreply.github.com> Date: Tue, 8 Dec 2020 17:50:44 -0500 Subject: [PATCH 69/86] Update 2020-03-18-what-is-backstage.md --- microsite/blog/2020-03-18-what-is-backstage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/blog/2020-03-18-what-is-backstage.md b/microsite/blog/2020-03-18-what-is-backstage.md index f4f62e2cf7..2c2b81d64d 100644 --- a/microsite/blog/2020-03-18-what-is-backstage.md +++ b/microsite/blog/2020-03-18-what-is-backstage.md @@ -1,6 +1,6 @@ --- title: What the heck is Backstage anyway? -author: Stefan Ålund +author: Stefan Ålund, Spotify authorURL: http://twitter.com/stalund authorImageURL: https://pbs.twimg.com/profile_images/121166861/6919c047c0d0edaace78c3009b28e917-user-full-200-130.generated_400x400.jpg --- From af2f2c0154ed620e16014a38d714cb113c2b6273 Mon Sep 17 00:00:00 2001 From: AlleyWei <64219471+AlleyWei@users.noreply.github.com> Date: Tue, 8 Dec 2020 17:51:48 -0500 Subject: [PATCH 70/86] Update 2020-04-06-lighthouse-plugin.md --- microsite/blog/2020-04-06-lighthouse-plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/blog/2020-04-06-lighthouse-plugin.md b/microsite/blog/2020-04-06-lighthouse-plugin.md index b8fd68e783..dcdb4b78e5 100644 --- a/microsite/blog/2020-04-06-lighthouse-plugin.md +++ b/microsite/blog/2020-04-06-lighthouse-plugin.md @@ -1,6 +1,6 @@ --- title: Introducing Lighthouse for Backstage -author: Paul Marbach +author: Paul Marbach, Spotify authorURL: http://twitter.com/fastfrwrd authorImageURL: https://pbs.twimg.com/profile_images/1224058798958088192/JPxS8uzR_400x400.jpg --- From d1529d2a98e6bd6aae9fc294cdb87b1ec44c3ff6 Mon Sep 17 00:00:00 2001 From: AlleyWei <64219471+AlleyWei@users.noreply.github.com> Date: Tue, 8 Dec 2020 19:03:05 -0500 Subject: [PATCH 71/86] Update 2020-10-22-cost-insights-plugin.md --- microsite/blog/2020-10-22-cost-insights-plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/blog/2020-10-22-cost-insights-plugin.md b/microsite/blog/2020-10-22-cost-insights-plugin.md index 15647f8418..c7ae9c492d 100644 --- a/microsite/blog/2020-10-22-cost-insights-plugin.md +++ b/microsite/blog/2020-10-22-cost-insights-plugin.md @@ -1,6 +1,6 @@ --- title: New Cost Insights plugin: The engineer’s solution to taming cloud costs -author: Janisa Anandamohan +author: Janisa Anandamohan, Spotify authorURL: https://twitter.com/janisa_a --- From 0b08d427c00e5d4ed84711d8daa771a1065402e9 Mon Sep 17 00:00:00 2001 From: AlleyWei <64219471+AlleyWei@users.noreply.github.com> Date: Tue, 8 Dec 2020 19:04:38 -0500 Subject: [PATCH 72/86] Update 2020-04-30-how-to-quickly-set-up-backstage.md --- microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.md b/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.md index 85f40dd9ea..65102c919f 100644 --- a/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.md +++ b/microsite/blog/2020-04-30-how-to-quickly-set-up-backstage.md @@ -1,6 +1,6 @@ --- title: How to quickly set up Backstage -author: Marcus Eide +author: Marcus Eide, Spotify authorURL: https://github.com/marcuseide authorImageURL: https://secure.gravatar.com/avatar/20223f1e03673c7c1e6282fbebaf6942 --- From 7008818262403d4f95b0eaf4278f32e6b9f7a967 Mon Sep 17 00:00:00 2001 From: AlleyWei <64219471+AlleyWei@users.noreply.github.com> Date: Tue, 8 Dec 2020 19:05:17 -0500 Subject: [PATCH 73/86] Update 2020-05-14-tech-radar-plugin.md --- microsite/blog/2020-05-14-tech-radar-plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/blog/2020-05-14-tech-radar-plugin.md b/microsite/blog/2020-05-14-tech-radar-plugin.md index 80c1eb8b5a..b78cf9004f 100644 --- a/microsite/blog/2020-05-14-tech-radar-plugin.md +++ b/microsite/blog/2020-05-14-tech-radar-plugin.md @@ -1,6 +1,6 @@ --- title: Introducing Tech Radar for Backstage -author: Bilawal Hameed +author: Bilawal Hameed, Spotify authorURL: http://twitter.com/bilawalhameed authorImageURL: https://avatars0.githubusercontent.com/bih --- From 3ec1a0d6a8292aa3d2198c8ceeda10716996011f Mon Sep 17 00:00:00 2001 From: AlleyWei <64219471+AlleyWei@users.noreply.github.com> Date: Tue, 8 Dec 2020 19:05:54 -0500 Subject: [PATCH 74/86] Update 2020-05-14-weaveworks-covid-19-app-uses-backstage-ui.md --- .../2020-05-14-weaveworks-covid-19-app-uses-backstage-ui.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/blog/2020-05-14-weaveworks-covid-19-app-uses-backstage-ui.md b/microsite/blog/2020-05-14-weaveworks-covid-19-app-uses-backstage-ui.md index 68e3f903c7..3e3d8a537a 100644 --- a/microsite/blog/2020-05-14-weaveworks-covid-19-app-uses-backstage-ui.md +++ b/microsite/blog/2020-05-14-weaveworks-covid-19-app-uses-backstage-ui.md @@ -1,6 +1,6 @@ --- title: Weaveworks’ COVID-19 app uses Backstage UI -author: Jeff Feng +author: Jeff Feng, Spotify authorURL: https://github.com/fengypants authorImageURL: https://avatars2.githubusercontent.com/u/46946747 --- From 5d9fb3f7252926b05b255d6b7e8813eeeb2e144e Mon Sep 17 00:00:00 2001 From: AlleyWei <64219471+AlleyWei@users.noreply.github.com> Date: Tue, 8 Dec 2020 19:07:09 -0500 Subject: [PATCH 75/86] Update 2020-05-22-phase-2-service-catalog.md --- microsite/blog/2020-05-22-phase-2-service-catalog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/blog/2020-05-22-phase-2-service-catalog.md b/microsite/blog/2020-05-22-phase-2-service-catalog.md index 103750dcc6..520a2a5f10 100644 --- a/microsite/blog/2020-05-22-phase-2-service-catalog.md +++ b/microsite/blog/2020-05-22-phase-2-service-catalog.md @@ -1,6 +1,6 @@ --- title: Starting Phase 2: The Service Catalog -author: Stefan Ålund +author: Stefan Ålund, Spotify authorURL: http://twitter.com/stalund authorImageURL: https://pbs.twimg.com/profile_images/121166861/6919c047c0d0edaace78c3009b28e917-user-full-200-130.generated_400x400.jpg --- From 38851663dafbd1481a717ebc1e8e47fad38c31ca Mon Sep 17 00:00:00 2001 From: AlleyWei <64219471+AlleyWei@users.noreply.github.com> Date: Tue, 8 Dec 2020 19:07:55 -0500 Subject: [PATCH 76/86] Update 2020-06-22-backstage-service-catalog-alpha.md --- microsite/blog/2020-06-22-backstage-service-catalog-alpha.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/blog/2020-06-22-backstage-service-catalog-alpha.md b/microsite/blog/2020-06-22-backstage-service-catalog-alpha.md index e50d7a6d47..4519f16d96 100644 --- a/microsite/blog/2020-06-22-backstage-service-catalog-alpha.md +++ b/microsite/blog/2020-06-22-backstage-service-catalog-alpha.md @@ -1,6 +1,6 @@ --- title: Backstage Service Catalog released in alpha -author: Stefan Ålund +author: Stefan Ålund, Spotify authorURL: http://twitter.com/stalund image: https://backstage.io/blog/assets/6/header.png --- From eb54e4e4fc672c1ad78c6933cce6fb1c7f5287a3 Mon Sep 17 00:00:00 2001 From: AlleyWei <64219471+AlleyWei@users.noreply.github.com> Date: Tue, 8 Dec 2020 19:08:18 -0500 Subject: [PATCH 77/86] Update 2020-07-01-how-to-enable-authentication-in-backstage-using-passport.md --- ...-how-to-enable-authentication-in-backstage-using-passport.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/blog/2020-07-01-how-to-enable-authentication-in-backstage-using-passport.md b/microsite/blog/2020-07-01-how-to-enable-authentication-in-backstage-using-passport.md index 9d41a21d05..ce778c9dc2 100644 --- a/microsite/blog/2020-07-01-how-to-enable-authentication-in-backstage-using-passport.md +++ b/microsite/blog/2020-07-01-how-to-enable-authentication-in-backstage-using-passport.md @@ -1,6 +1,6 @@ --- title: How to enable authentication in Backstage using Passport -author: Lee Mills +author: Lee Mills, Spotify authorURL: https://github.com/leemills83 authorImageURL: https://avatars1.githubusercontent.com/u/1236238?s=460&v=4 --- From 4918fe85d5a9b8b2d0320ce3a3de46608f716f3b Mon Sep 17 00:00:00 2001 From: AlleyWei <64219471+AlleyWei@users.noreply.github.com> Date: Tue, 8 Dec 2020 19:08:44 -0500 Subject: [PATCH 78/86] Update 2020-08-05-announcing-backstage-software-templates.md --- .../blog/2020-08-05-announcing-backstage-software-templates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/blog/2020-08-05-announcing-backstage-software-templates.md b/microsite/blog/2020-08-05-announcing-backstage-software-templates.md index bd6ae8eeeb..afda20e499 100644 --- a/microsite/blog/2020-08-05-announcing-backstage-software-templates.md +++ b/microsite/blog/2020-08-05-announcing-backstage-software-templates.md @@ -1,6 +1,6 @@ --- title: Announcing Backstage Software Templates -author: Stefan Ålund +author: Stefan Ålund, Spotify authorURL: https://twitter.com/stalund --- From 5006788c378c2a60c4983376addc308ec2f429dc Mon Sep 17 00:00:00 2001 From: AlleyWei <64219471+AlleyWei@users.noreply.github.com> Date: Tue, 8 Dec 2020 19:09:10 -0500 Subject: [PATCH 79/86] Update 2020-09-08-announcing-tech-docs.md --- microsite/blog/2020-09-08-announcing-tech-docs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/blog/2020-09-08-announcing-tech-docs.md b/microsite/blog/2020-09-08-announcing-tech-docs.md index ceab17d7be..f09c73fd83 100644 --- a/microsite/blog/2020-09-08-announcing-tech-docs.md +++ b/microsite/blog/2020-09-08-announcing-tech-docs.md @@ -1,6 +1,6 @@ --- title: Announcing TechDocs: Spotify’s docs-like-code plugin for Backstage -author: Gary Niemen +author: Gary Niemen, Spotify authorURL: https://github.com/garyniemen --- From 6d95b4d2647c703413459413ae91710da14eee94 Mon Sep 17 00:00:00 2001 From: AlleyWei <64219471+AlleyWei@users.noreply.github.com> Date: Tue, 8 Dec 2020 19:09:36 -0500 Subject: [PATCH 80/86] Update 2020-09-23-backstage-cncf-sandbox.md --- microsite/blog/2020-09-23-backstage-cncf-sandbox.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/blog/2020-09-23-backstage-cncf-sandbox.md b/microsite/blog/2020-09-23-backstage-cncf-sandbox.md index 8fc459cb62..48a67e878c 100644 --- a/microsite/blog/2020-09-23-backstage-cncf-sandbox.md +++ b/microsite/blog/2020-09-23-backstage-cncf-sandbox.md @@ -1,6 +1,6 @@ --- title: Backstage has been accepted into the CNCF Sandbox -author: Stefan Ålund +author: Stefan Ålund, Spotify authorURL: https://twitter.com/stalund --- From ba1d85cb8078688b592135d1f73558489b6c366c Mon Sep 17 00:00:00 2001 From: AlleyWei <64219471+AlleyWei@users.noreply.github.com> Date: Tue, 8 Dec 2020 19:10:01 -0500 Subject: [PATCH 81/86] Update 2020-09-30-backstage-design-system.md --- microsite/blog/2020-09-30-backstage-design-system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/blog/2020-09-30-backstage-design-system.md b/microsite/blog/2020-09-30-backstage-design-system.md index a1d087f755..fc227ea5d3 100644 --- a/microsite/blog/2020-09-30-backstage-design-system.md +++ b/microsite/blog/2020-09-30-backstage-design-system.md @@ -1,6 +1,6 @@ --- title: How to design for Backstage (even if you’re not a designer) -author: Kat Zhou +author: Kat Zhou, Spotify authorURL: http://twitter.com/katherinemzhou --- From aaaa326f7de802c0443518a4f01ba469ab206d18 Mon Sep 17 00:00:00 2001 From: AlleyWei <64219471+AlleyWei@users.noreply.github.com> Date: Tue, 8 Dec 2020 19:10:24 -0500 Subject: [PATCH 82/86] Update 2020-09-30-plugin-marketplace.md --- microsite/blog/2020-09-30-plugin-marketplace.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/blog/2020-09-30-plugin-marketplace.md b/microsite/blog/2020-09-30-plugin-marketplace.md index 10928112b8..f4e9b749e2 100644 --- a/microsite/blog/2020-09-30-plugin-marketplace.md +++ b/microsite/blog/2020-09-30-plugin-marketplace.md @@ -1,6 +1,6 @@ --- title: The Plugin Marketplace is open -author: Stefan Ålund +author: Stefan Ålund, Spotify authorURL: https://twitter.com/stalund --- From 1171608d9d0716fb762463c4ad0c03154d14d92f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Dec 2020 07:44:02 +0100 Subject: [PATCH 83/86] build(deps): bump jest from 26.5.3 to 26.6.3 (#3644) Bumps [jest](https://github.com/facebook/jest) from 26.5.3 to 26.6.3. - [Release notes](https://github.com/facebook/jest/releases) - [Changelog](https://github.com/facebook/jest/blob/master/CHANGELOG.md) - [Commits](https://github.com/facebook/jest/compare/v26.5.3...v26.6.3) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 683 +++++++++++++++++++++++++++++------------------------- 1 file changed, 373 insertions(+), 310 deletions(-) diff --git a/yarn.lock b/yarn.lock index 55e57be235..5eca6925bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -716,6 +716,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz#dd6c0b357ac1bb142d98537450a319625d13d2a0" + integrity sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-typescript@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.10.4.tgz#2f55e770d3501e83af217d782cb7517d7bb34d25" @@ -2316,46 +2323,46 @@ resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== -"@jest/console@^26.5.2": - version "26.5.2" - resolved "https://registry.npmjs.org/@jest/console/-/console-26.5.2.tgz#94fc4865b1abed7c352b5e21e6c57be4b95604a6" - integrity sha512-lJELzKINpF1v74DXHbCRIkQ/+nUV1M+ntj+X1J8LxCgpmJZjfLmhFejiMSbjjD66fayxl5Z06tbs3HMyuik6rw== +"@jest/console@^26.6.2": + version "26.6.2" + resolved "https://registry.npmjs.org/@jest/console/-/console-26.6.2.tgz#4e04bc464014358b03ab4937805ee36a0aeb98f2" + integrity sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g== dependencies: - "@jest/types" "^26.5.2" + "@jest/types" "^26.6.2" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^26.5.2" - jest-util "^26.5.2" + jest-message-util "^26.6.2" + jest-util "^26.6.2" slash "^3.0.0" -"@jest/core@^26.5.3": - version "26.5.3" - resolved "https://registry.npmjs.org/@jest/core/-/core-26.5.3.tgz#712ed4adb64c3bda256a3f400ff1d3eb2a031f13" - integrity sha512-CiU0UKFF1V7KzYTVEtFbFmGLdb2g4aTtY0WlyUfLgj/RtoTnJFhh50xKKr7OYkdmBUlGFSa2mD1TU3UZ6OLd4g== +"@jest/core@^26.6.3": + version "26.6.3" + resolved "https://registry.npmjs.org/@jest/core/-/core-26.6.3.tgz#7639fcb3833d748a4656ada54bde193051e45fad" + integrity sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw== dependencies: - "@jest/console" "^26.5.2" - "@jest/reporters" "^26.5.3" - "@jest/test-result" "^26.5.2" - "@jest/transform" "^26.5.2" - "@jest/types" "^26.5.2" + "@jest/console" "^26.6.2" + "@jest/reporters" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.4" - jest-changed-files "^26.5.2" - jest-config "^26.5.3" - jest-haste-map "^26.5.2" - jest-message-util "^26.5.2" + jest-changed-files "^26.6.2" + jest-config "^26.6.3" + jest-haste-map "^26.6.2" + jest-message-util "^26.6.2" jest-regex-util "^26.0.0" - jest-resolve "^26.5.2" - jest-resolve-dependencies "^26.5.3" - jest-runner "^26.5.3" - jest-runtime "^26.5.3" - jest-snapshot "^26.5.3" - jest-util "^26.5.2" - jest-validate "^26.5.3" - jest-watcher "^26.5.2" + jest-resolve "^26.6.2" + jest-resolve-dependencies "^26.6.3" + jest-runner "^26.6.3" + jest-runtime "^26.6.3" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" + jest-watcher "^26.6.2" micromatch "^4.0.2" p-each-series "^2.1.0" rimraf "^3.0.0" @@ -2367,47 +2374,47 @@ resolved "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-26.5.0.tgz#1d07947adc51ea17766d9f0ccf5a8d6ea94c47dc" integrity sha512-DJ+pEBUIqarrbv1W/C39f9YH0rJ4wsXZ/VC6JafJPlHW2HOucKceeaqTOQj9MEDQZjySxMLkOq5mfXZXNZcmWw== -"@jest/environment@^26.5.2": - version "26.5.2" - resolved "https://registry.npmjs.org/@jest/environment/-/environment-26.5.2.tgz#eba3cfc698f6e03739628f699c28e8a07f5e65fe" - integrity sha512-YjhCD/Zhkz0/1vdlS/QN6QmuUdDkpgBdK4SdiVg4Y19e29g4VQYN5Xg8+YuHjdoWGY7wJHMxc79uDTeTOy9Ngw== +"@jest/environment@^26.6.2": + version "26.6.2" + resolved "https://registry.npmjs.org/@jest/environment/-/environment-26.6.2.tgz#ba364cc72e221e79cc8f0a99555bf5d7577cf92c" + integrity sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA== dependencies: - "@jest/fake-timers" "^26.5.2" - "@jest/types" "^26.5.2" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" - jest-mock "^26.5.2" + jest-mock "^26.6.2" -"@jest/fake-timers@^26.5.2": - version "26.5.2" - resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.5.2.tgz#1291ac81680ceb0dc7daa1f92c059307eea6400a" - integrity sha512-09Hn5Oraqt36V1akxQeWMVL0fR9c6PnEhpgLaYvREXZJAh2H2Y+QLCsl0g7uMoJeoWJAuz4tozk1prbR1Fc1sw== +"@jest/fake-timers@^26.6.2": + version "26.6.2" + resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.6.2.tgz#459c329bcf70cee4af4d7e3f3e67848123535aad" + integrity sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA== dependencies: - "@jest/types" "^26.5.2" + "@jest/types" "^26.6.2" "@sinonjs/fake-timers" "^6.0.1" "@types/node" "*" - jest-message-util "^26.5.2" - jest-mock "^26.5.2" - jest-util "^26.5.2" + jest-message-util "^26.6.2" + jest-mock "^26.6.2" + jest-util "^26.6.2" -"@jest/globals@^26.5.3": - version "26.5.3" - resolved "https://registry.npmjs.org/@jest/globals/-/globals-26.5.3.tgz#90769b40e0af3fa0b28f6d8c5bbe3712467243fd" - integrity sha512-7QztI0JC2CuB+Wx1VdnOUNeIGm8+PIaqngYsZXQCkH2QV0GFqzAYc9BZfU0nuqA6cbYrWh5wkuMzyii3P7deug== +"@jest/globals@^26.6.2": + version "26.6.2" + resolved "https://registry.npmjs.org/@jest/globals/-/globals-26.6.2.tgz#5b613b78a1aa2655ae908eba638cc96a20df720a" + integrity sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA== dependencies: - "@jest/environment" "^26.5.2" - "@jest/types" "^26.5.2" - expect "^26.5.3" + "@jest/environment" "^26.6.2" + "@jest/types" "^26.6.2" + expect "^26.6.2" -"@jest/reporters@^26.5.3": - version "26.5.3" - resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-26.5.3.tgz#e810e9c2b670f33f1c09e9975749260ca12f1c17" - integrity sha512-X+vR0CpfMQzYcYmMFKNY9n4jklcb14Kffffp7+H/MqitWnb0440bW2L76NGWKAa+bnXhNoZr+lCVtdtPmfJVOQ== +"@jest/reporters@^26.6.2": + version "26.6.2" + resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-26.6.2.tgz#1f518b99637a5f18307bd3ecf9275f6882a667f6" + integrity sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^26.5.2" - "@jest/test-result" "^26.5.2" - "@jest/transform" "^26.5.2" - "@jest/types" "^26.5.2" + "@jest/console" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" chalk "^4.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" @@ -2418,70 +2425,70 @@ istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" istanbul-reports "^3.0.2" - jest-haste-map "^26.5.2" - jest-resolve "^26.5.2" - jest-util "^26.5.2" - jest-worker "^26.5.0" + jest-haste-map "^26.6.2" + jest-resolve "^26.6.2" + jest-util "^26.6.2" + jest-worker "^26.6.2" slash "^3.0.0" source-map "^0.6.0" string-length "^4.0.1" terminal-link "^2.0.0" - v8-to-istanbul "^6.0.1" + v8-to-istanbul "^7.0.0" optionalDependencies: node-notifier "^8.0.0" -"@jest/source-map@^26.5.0": - version "26.5.0" - resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-26.5.0.tgz#98792457c85bdd902365cd2847b58fff05d96367" - integrity sha512-jWAw9ZwYHJMe9eZq/WrsHlwF8E3hM9gynlcDpOyCb9bR8wEd9ZNBZCi7/jZyzHxC7t3thZ10gO2IDhu0bPKS5g== +"@jest/source-map@^26.6.2": + version "26.6.2" + resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-26.6.2.tgz#29af5e1e2e324cafccc936f218309f54ab69d535" + integrity sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA== dependencies: callsites "^3.0.0" graceful-fs "^4.2.4" source-map "^0.6.0" -"@jest/test-result@^26.5.2": - version "26.5.2" - resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-26.5.2.tgz#cc1a44cfd4db2ecee3fb0bc4e9fe087aa54b5230" - integrity sha512-E/Zp6LURJEGSCWpoMGmCFuuEI1OWuI3hmZwmULV0GsgJBh7u0rwqioxhRU95euUuviqBDN8ruX/vP/4bwYolXw== +"@jest/test-result@^26.6.2": + version "26.6.2" + resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-26.6.2.tgz#55da58b62df134576cc95476efa5f7949e3f5f18" + integrity sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ== dependencies: - "@jest/console" "^26.5.2" - "@jest/types" "^26.5.2" + "@jest/console" "^26.6.2" + "@jest/types" "^26.6.2" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^26.5.3": - version "26.5.3" - resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.5.3.tgz#9ae0ab9bc37d5171b28424029192e50229814f8d" - integrity sha512-Wqzb7aQ13L3T47xHdpUqYMOpiqz6Dx2QDDghp5AV/eUDXR7JieY+E1s233TQlNyl+PqtqgjVokmyjzX/HA51BA== +"@jest/test-sequencer@^26.6.3": + version "26.6.3" + resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz#98e8a45100863886d074205e8ffdc5a7eb582b17" + integrity sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw== dependencies: - "@jest/test-result" "^26.5.2" + "@jest/test-result" "^26.6.2" graceful-fs "^4.2.4" - jest-haste-map "^26.5.2" - jest-runner "^26.5.3" - jest-runtime "^26.5.3" + jest-haste-map "^26.6.2" + jest-runner "^26.6.3" + jest-runtime "^26.6.3" -"@jest/transform@^26.5.2": - version "26.5.2" - resolved "https://registry.npmjs.org/@jest/transform/-/transform-26.5.2.tgz#6a0033a1d24316a1c75184d010d864f2c681bef5" - integrity sha512-AUNjvexh+APhhmS8S+KboPz+D3pCxPvEAGduffaAJYxIFxGi/ytZQkrqcKDUU0ERBAo5R7087fyOYr2oms1seg== +"@jest/transform@^26.6.2": + version "26.6.2" + resolved "https://registry.npmjs.org/@jest/transform/-/transform-26.6.2.tgz#5ac57c5fa1ad17b2aae83e73e45813894dcf2e4b" + integrity sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA== dependencies: "@babel/core" "^7.1.0" - "@jest/types" "^26.5.2" + "@jest/types" "^26.6.2" babel-plugin-istanbul "^6.0.0" chalk "^4.0.0" convert-source-map "^1.4.0" fast-json-stable-stringify "^2.0.0" graceful-fs "^4.2.4" - jest-haste-map "^26.5.2" + jest-haste-map "^26.6.2" jest-regex-util "^26.0.0" - jest-util "^26.5.2" + jest-util "^26.6.2" micromatch "^4.0.2" pirates "^4.0.1" slash "^3.0.0" source-map "^0.6.1" write-file-atomic "^3.0.0" -"@jest/types@^26.5.2", "@jest/types@^26.6.1": +"@jest/types@^26.6.1": version "26.6.1" resolved "https://registry.npmjs.org/@jest/types/-/types-26.6.1.tgz#2638890e8031c0bc8b4681e0357ed986e2f866c5" integrity sha512-ywHavIKNpAVrStiRY5wiyehvcktpijpItvGiK72RAn5ctqmzvPk8OvKnvHeBqa1XdQr959CTWAJMqxI8BTibyg== @@ -2492,6 +2499,17 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@jest/types@^26.6.2": + version "26.6.2" + resolved "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + "@jsdevtools/ono@^7.1.3": version "7.1.3" resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" @@ -7404,16 +7422,16 @@ babel-helper-to-multiple-sequence-expressions@^0.5.0: resolved "https://registry.npmjs.org/babel-helper-to-multiple-sequence-expressions/-/babel-helper-to-multiple-sequence-expressions-0.5.0.tgz#a3f924e3561882d42fcf48907aa98f7979a4588d" integrity sha512-m2CvfDW4+1qfDdsrtf4dwOslQC3yhbgyBFptncp4wvtdrDHqueW7slsYv4gArie056phvQFhT2nRcGS4bnm6mA== -babel-jest@^26.5.2: - version "26.5.2" - resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-26.5.2.tgz#164f367a35946c6cf54eaccde8762dec50422250" - integrity sha512-U3KvymF3SczA3vOL/cgiUFOznfMET+XDIXiWnoJV45siAp2pLMG8i2+/MGZlAC3f/F6Q40LR4M4qDrWZ9wkK8A== +babel-jest@^26.6.3: + version "26.6.3" + resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056" + integrity sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA== dependencies: - "@jest/transform" "^26.5.2" - "@jest/types" "^26.5.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" "@types/babel__core" "^7.1.7" babel-plugin-istanbul "^6.0.0" - babel-preset-jest "^26.5.0" + babel-preset-jest "^26.6.2" chalk "^4.0.0" graceful-fs "^4.2.4" slash "^3.0.0" @@ -7468,10 +7486,10 @@ babel-plugin-istanbul@^6.0.0: istanbul-lib-instrument "^4.0.0" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^26.5.0: - version "26.5.0" - resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.5.0.tgz#3916b3a28129c29528de91e5784a44680db46385" - integrity sha512-ck17uZFD3CDfuwCLATWZxkkuGGFhMij8quP8CNhwj8ek1mqFgbFzRJ30xwC04LLscj/aKsVFfRST+b5PT7rSuw== +babel-plugin-jest-hoist@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz#8185bd030348d254c6d7dd974355e6a28b21e62d" + integrity sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw== dependencies: "@babel/template" "^7.3.3" "@babel/types" "^7.3.3" @@ -7646,10 +7664,10 @@ babel-plugin-transform-undefined-to-void@^6.9.4: resolved "https://registry.npmjs.org/babel-plugin-transform-undefined-to-void/-/babel-plugin-transform-undefined-to-void-6.9.4.tgz#be241ca81404030678b748717322b89d0c8fe280" integrity sha1-viQcqBQEAwZ4t0hxcyK4nQyP4oA= -babel-preset-current-node-syntax@^0.1.3: - version "0.1.3" - resolved "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.3.tgz#b4b547acddbf963cba555ba9f9cbbb70bfd044da" - integrity sha512-uyexu1sVwcdFnyq9o8UQYsXwXflIh8LvrF5+cKrYam93ned1CStffB3+BEcsxGSgagoA3GEyjDqO4a/58hyPYQ== +babel-preset-current-node-syntax@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.0.tgz#cf5feef29551253471cfa82fc8e0f5063df07a77" + integrity sha512-mGkvkpocWJes1CmMKtgGUwCeeq0pOhALyymozzDWYomHTbDLwueDYG6p4TK1YOeYHCzBzYPsWkgTto10JubI1Q== dependencies: "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-bigint" "^7.8.3" @@ -7662,6 +7680,7 @@ babel-preset-current-node-syntax@^0.1.3: "@babel/plugin-syntax-object-rest-spread" "^7.8.3" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" babel-preset-fbjs@^3.3.0: version "3.3.0" @@ -7696,13 +7715,13 @@ babel-preset-fbjs@^3.3.0: "@babel/plugin-transform-template-literals" "^7.0.0" babel-plugin-syntax-trailing-function-commas "^7.0.0-beta.0" -babel-preset-jest@^26.5.0: - version "26.5.0" - resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.5.0.tgz#f1b166045cd21437d1188d29f7fba470d5bdb0e7" - integrity sha512-F2vTluljhqkiGSJGBg/jOruA8vIIIL11YrxRcO7nviNTMbbofPSHwnm8mgP7d/wS7wRSexRoI6X1A6T74d4LQA== +babel-preset-jest@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz#747872b1171df032252426586881d62d31798fee" + integrity sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ== dependencies: - babel-plugin-jest-hoist "^26.5.0" - babel-preset-current-node-syntax "^0.1.3" + babel-plugin-jest-hoist "^26.6.2" + babel-preset-current-node-syntax "^1.0.0" "babel-preset-minify@^0.5.0 || 0.6.0-alpha.5": version "0.5.1" @@ -8690,6 +8709,11 @@ circleci-api@^4.0.0: dependencies: axios "^0.20.0" +cjs-module-lexer@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz#4186fcca0eae175970aee870b9fe2d6cf8d5655f" + integrity sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw== + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -10531,6 +10555,11 @@ diff-sequences@^26.5.0: resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.5.0.tgz#ef766cf09d43ed40406611f11c6d8d9dd8b2fefd" integrity sha512-ZXx86srb/iYy6jG71k++wBN9P9J05UNQ5hQHQd9MtMPvcqXPx/vKU69jfHV637D00Q2gSgPk2D+jSx3l1lDW/Q== +diff-sequences@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" + integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== + diff@^4.0.1, diff@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -11669,16 +11698,16 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -expect@^26.5.3: - version "26.5.3" - resolved "https://registry.npmjs.org/expect/-/expect-26.5.3.tgz#89d9795036f7358b0a9a5243238eb8086482d741" - integrity sha512-kkpOhGRWGOr+TEFUnYAjfGvv35bfP+OlPtqPIJpOCR9DVtv8QV+p8zG0Edqafh80fsjeE+7RBcVUq1xApnYglw== +expect@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417" + integrity sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA== dependencies: - "@jest/types" "^26.5.2" + "@jest/types" "^26.6.2" ansi-styles "^4.0.0" jest-get-type "^26.3.0" - jest-matcher-utils "^26.5.2" - jest-message-util "^26.5.2" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" jest-regex-util "^26.0.0" express-promise-router@^3.0.3: @@ -14699,57 +14728,57 @@ jenkins@^0.28.0: dependencies: papi "^0.29.0" -jest-changed-files@^26.5.2: - version "26.5.2" - resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.5.2.tgz#330232c6a5c09a7f040a5870e8f0a9c6abcdbed5" - integrity sha512-qSmssmiIdvM5BWVtyK/nqVpN3spR5YyvkvPqz1x3BR1bwIxsWmU/MGwLoCrPNLbkG2ASAKfvmJpOduEApBPh2w== +jest-changed-files@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0" + integrity sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ== dependencies: - "@jest/types" "^26.5.2" + "@jest/types" "^26.6.2" execa "^4.0.0" throat "^5.0.0" -jest-cli@^26.5.3: - version "26.5.3" - resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-26.5.3.tgz#f936b98f247b76b7bc89c7af50af82c88e356a80" - integrity sha512-HkbSvtugpSXBf2660v9FrNVUgxvPkssN8CRGj9gPM8PLhnaa6zziFiCEKQAkQS4uRzseww45o0TR+l6KeRYV9A== +jest-cli@^26.6.3: + version "26.6.3" + resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-26.6.3.tgz#43117cfef24bc4cd691a174a8796a532e135e92a" + integrity sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg== dependencies: - "@jest/core" "^26.5.3" - "@jest/test-result" "^26.5.2" - "@jest/types" "^26.5.2" + "@jest/core" "^26.6.3" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.4" import-local "^3.0.2" is-ci "^2.0.0" - jest-config "^26.5.3" - jest-util "^26.5.2" - jest-validate "^26.5.3" + jest-config "^26.6.3" + jest-util "^26.6.2" + jest-validate "^26.6.2" prompts "^2.0.1" yargs "^15.4.1" -jest-config@^26.5.3: - version "26.5.3" - resolved "https://registry.npmjs.org/jest-config/-/jest-config-26.5.3.tgz#baf51c9be078c2c755c8f8a51ec0f06c762c1d3f" - integrity sha512-NVhZiIuN0GQM6b6as4CI5FSCyXKxdrx5ACMCcv/7Pf+TeCajJhJc+6dwgdAVPyerUFB9pRBIz3bE7clSrRge/w== +jest-config@^26.6.3: + version "26.6.3" + resolved "https://registry.npmjs.org/jest-config/-/jest-config-26.6.3.tgz#64f41444eef9eb03dc51d5c53b75c8c71f645349" + integrity sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg== dependencies: "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^26.5.3" - "@jest/types" "^26.5.2" - babel-jest "^26.5.2" + "@jest/test-sequencer" "^26.6.3" + "@jest/types" "^26.6.2" + babel-jest "^26.6.3" chalk "^4.0.0" deepmerge "^4.2.2" glob "^7.1.1" graceful-fs "^4.2.4" - jest-environment-jsdom "^26.5.2" - jest-environment-node "^26.5.2" + jest-environment-jsdom "^26.6.2" + jest-environment-node "^26.6.2" jest-get-type "^26.3.0" - jest-jasmine2 "^26.5.3" + jest-jasmine2 "^26.6.3" jest-regex-util "^26.0.0" - jest-resolve "^26.5.2" - jest-util "^26.5.2" - jest-validate "^26.5.3" + jest-resolve "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" micromatch "^4.0.2" - pretty-format "^26.5.2" + pretty-format "^26.6.2" jest-css-modules@^2.1.0: version "2.1.0" @@ -14758,7 +14787,7 @@ jest-css-modules@^2.1.0: dependencies: identity-obj-proxy "3.0.0" -jest-diff@^26.0.0, jest-diff@^26.5.2: +jest-diff@^26.0.0: version "26.6.1" resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.1.tgz#38aa194979f454619bb39bdee299fb64ede5300c" integrity sha512-BBNy/zin2m4kG5In126O8chOBxLLS/XMTuuM2+YhgyHk87ewPzKTuTJcqj3lOWOi03NNgrl+DkMeV/exdvG9gg== @@ -14768,6 +14797,16 @@ jest-diff@^26.0.0, jest-diff@^26.5.2: jest-get-type "^26.3.0" pretty-format "^26.6.1" +jest-diff@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" + integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== + dependencies: + chalk "^4.0.0" + diff-sequences "^26.6.2" + jest-get-type "^26.3.0" + pretty-format "^26.6.2" + jest-docblock@^26.0.0: version "26.0.0" resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5" @@ -14775,41 +14814,41 @@ jest-docblock@^26.0.0: dependencies: detect-newline "^3.0.0" -jest-each@^26.5.2: - version "26.5.2" - resolved "https://registry.npmjs.org/jest-each/-/jest-each-26.5.2.tgz#35e68d6906a7f826d3ca5803cfe91d17a5a34c31" - integrity sha512-w7D9FNe0m2D3yZ0Drj9CLkyF/mGhmBSULMQTypzAKR746xXnjUrK8GUJdlLTWUF6dd0ks3MtvGP7/xNFr9Aphg== +jest-each@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-each/-/jest-each-26.6.2.tgz#02526438a77a67401c8a6382dfe5999952c167cb" + integrity sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A== dependencies: - "@jest/types" "^26.5.2" + "@jest/types" "^26.6.2" chalk "^4.0.0" jest-get-type "^26.3.0" - jest-util "^26.5.2" - pretty-format "^26.5.2" + jest-util "^26.6.2" + pretty-format "^26.6.2" -jest-environment-jsdom@^26.5.2: - version "26.5.2" - resolved "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.5.2.tgz#5feab05b828fd3e4b96bee5e0493464ddd2bb4bc" - integrity sha512-fWZPx0bluJaTQ36+PmRpvUtUlUFlGGBNyGX1SN3dLUHHMcQ4WseNEzcGGKOw4U5towXgxI4qDoI3vwR18H0RTw== +jest-environment-jsdom@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz#78d09fe9cf019a357009b9b7e1f101d23bd1da3e" + integrity sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q== dependencies: - "@jest/environment" "^26.5.2" - "@jest/fake-timers" "^26.5.2" - "@jest/types" "^26.5.2" + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" - jest-mock "^26.5.2" - jest-util "^26.5.2" + jest-mock "^26.6.2" + jest-util "^26.6.2" jsdom "^16.4.0" -jest-environment-node@^26.5.2: - version "26.5.2" - resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.5.2.tgz#275a0f01b5e47447056f1541a15ed4da14acca03" - integrity sha512-YHjnDsf/GKFCYMGF1V+6HF7jhY1fcLfLNBDjhAOvFGvt6d8vXvNdJGVM7uTZ2VO/TuIyEFhPGaXMX5j3h7fsrA== +jest-environment-node@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.6.2.tgz#824e4c7fb4944646356f11ac75b229b0035f2b0c" + integrity sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag== dependencies: - "@jest/environment" "^26.5.2" - "@jest/fake-timers" "^26.5.2" - "@jest/types" "^26.5.2" + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" - jest-mock "^26.5.2" - jest-util "^26.5.2" + jest-mock "^26.6.2" + jest-util "^26.6.2" jest-esm-transformer@^1.0.0: version "1.0.0" @@ -14824,89 +14863,90 @@ jest-get-type@^26.3.0: resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== -jest-haste-map@^26.5.2: - version "26.5.2" - resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.5.2.tgz#a15008abfc502c18aa56e4919ed8c96304ceb23d" - integrity sha512-lJIAVJN3gtO3k4xy+7i2Xjtwh8CfPcH08WYjZpe9xzveDaqGw9fVNCpkYu6M525wKFVkLmyi7ku+DxCAP1lyMA== +jest-haste-map@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa" + integrity sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w== dependencies: - "@jest/types" "^26.5.2" + "@jest/types" "^26.6.2" "@types/graceful-fs" "^4.1.2" "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" graceful-fs "^4.2.4" jest-regex-util "^26.0.0" - jest-serializer "^26.5.0" - jest-util "^26.5.2" - jest-worker "^26.5.0" + jest-serializer "^26.6.2" + jest-util "^26.6.2" + jest-worker "^26.6.2" micromatch "^4.0.2" sane "^4.0.3" walker "^1.0.7" optionalDependencies: fsevents "^2.1.2" -jest-jasmine2@^26.5.3: - version "26.5.3" - resolved "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.5.3.tgz#baad2114ce32d16aff25aeb877d18bb4e332dc4c" - integrity sha512-nFlZOpnGlNc7y/+UkkeHnvbOM+rLz4wB1AimgI9QhtnqSZte0wYjbAm8hf7TCwXlXgDwZxAXo6z0a2Wzn9FoOg== +jest-jasmine2@^26.6.3: + version "26.6.3" + resolved "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz#adc3cf915deacb5212c93b9f3547cd12958f2edd" + integrity sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg== dependencies: "@babel/traverse" "^7.1.0" - "@jest/environment" "^26.5.2" - "@jest/source-map" "^26.5.0" - "@jest/test-result" "^26.5.2" - "@jest/types" "^26.5.2" + "@jest/environment" "^26.6.2" + "@jest/source-map" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" - expect "^26.5.3" + expect "^26.6.2" is-generator-fn "^2.0.0" - jest-each "^26.5.2" - jest-matcher-utils "^26.5.2" - jest-message-util "^26.5.2" - jest-runtime "^26.5.3" - jest-snapshot "^26.5.3" - jest-util "^26.5.2" - pretty-format "^26.5.2" + jest-each "^26.6.2" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-runtime "^26.6.3" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + pretty-format "^26.6.2" throat "^5.0.0" -jest-leak-detector@^26.5.2: - version "26.5.2" - resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.5.2.tgz#83fcf9a4a6ef157549552cb4f32ca1d6221eea69" - integrity sha512-h7ia3dLzBFItmYERaLPEtEKxy3YlcbcRSjj0XRNJgBEyODuu+3DM2o62kvIFvs3PsaYoIIv+e+nLRI61Dj1CNw== +jest-leak-detector@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz#7717cf118b92238f2eba65054c8a0c9c653a91af" + integrity sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg== dependencies: jest-get-type "^26.3.0" - pretty-format "^26.5.2" + pretty-format "^26.6.2" -jest-matcher-utils@^26.5.2: - version "26.5.2" - resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.5.2.tgz#6aa2c76ce8b9c33e66f8856ff3a52bab59e6c85a" - integrity sha512-W9GO9KBIC4gIArsNqDUKsLnhivaqf8MSs6ujO/JDcPIQrmY+aasewweXVET8KdrJ6ADQaUne5UzysvF/RR7JYA== +jest-matcher-utils@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz#8e6fd6e863c8b2d31ac6472eeb237bc595e53e7a" + integrity sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw== dependencies: chalk "^4.0.0" - jest-diff "^26.5.2" + jest-diff "^26.6.2" jest-get-type "^26.3.0" - pretty-format "^26.5.2" + pretty-format "^26.6.2" -jest-message-util@^26.5.2: - version "26.5.2" - resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.5.2.tgz#6c4c4c46dcfbabb47cd1ba2f6351559729bc11bb" - integrity sha512-Ocp9UYZ5Jl15C5PNsoDiGEk14A4NG0zZKknpWdZGoMzJuGAkVt10e97tnEVMYpk7LnQHZOfuK2j/izLBMcuCZw== +jest-message-util@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" + integrity sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA== dependencies: "@babel/code-frame" "^7.0.0" - "@jest/types" "^26.5.2" + "@jest/types" "^26.6.2" "@types/stack-utils" "^2.0.0" chalk "^4.0.0" graceful-fs "^4.2.4" micromatch "^4.0.2" + pretty-format "^26.6.2" slash "^3.0.0" stack-utils "^2.0.2" -jest-mock@^26.5.2: - version "26.5.2" - resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-26.5.2.tgz#c9302e8ef807f2bfc749ee52e65ad11166a1b6a1" - integrity sha512-9SiU4b5PtO51v0MtJwVRqeGEroH66Bnwtq4ARdNP7jNXbpT7+ByeWNAk4NeT/uHfNSVDXEXgQo1XRuwEqS6Rdw== +jest-mock@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-26.6.2.tgz#d6cb712b041ed47fe0d9b6fc3474bc6543feb302" + integrity sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew== dependencies: - "@jest/types" "^26.5.2" + "@jest/types" "^26.6.2" "@types/node" "*" jest-pnp-resolver@^1.2.2: @@ -14919,118 +14959,119 @@ jest-regex-util@^26.0.0: resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== -jest-resolve-dependencies@^26.5.3: - version "26.5.3" - resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.5.3.tgz#11483f91e534bdcd257ab21e8622799e59701aba" - integrity sha512-+KMDeke/BFK+mIQ2IYSyBz010h7zQaVt4Xie6cLqUGChorx66vVeQVv4ErNoMwInnyYHi1Ud73tDS01UbXbfLQ== +jest-resolve-dependencies@^26.6.3: + version "26.6.3" + resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz#6680859ee5d22ee5dcd961fe4871f59f4c784fb6" + integrity sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg== dependencies: - "@jest/types" "^26.5.2" + "@jest/types" "^26.6.2" jest-regex-util "^26.0.0" - jest-snapshot "^26.5.3" + jest-snapshot "^26.6.2" -jest-resolve@^26.5.2: - version "26.5.2" - resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.5.2.tgz#0d719144f61944a428657b755a0e5c6af4fc8602" - integrity sha512-XsPxojXGRA0CoDD7Vis59ucz2p3cQFU5C+19tz3tLEAlhYKkK77IL0cjYjikY9wXnOaBeEdm1rOgSJjbZWpcZg== +jest-resolve@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.2.tgz#a3ab1517217f469b504f1b56603c5bb541fbb507" + integrity sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ== dependencies: - "@jest/types" "^26.5.2" + "@jest/types" "^26.6.2" chalk "^4.0.0" graceful-fs "^4.2.4" jest-pnp-resolver "^1.2.2" - jest-util "^26.5.2" + jest-util "^26.6.2" read-pkg-up "^7.0.1" - resolve "^1.17.0" + resolve "^1.18.1" slash "^3.0.0" -jest-runner@^26.5.3: - version "26.5.3" - resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-26.5.3.tgz#800787459ea59c68e7505952933e33981dc3db38" - integrity sha512-qproP0Pq7IIule+263W57k2+8kWCszVJTC9TJWGUz0xJBr+gNiniGXlG8rotd0XxwonD5UiJloYoSO5vbUr5FQ== +jest-runner@^26.6.3: + version "26.6.3" + resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-26.6.3.tgz#2d1fed3d46e10f233fd1dbd3bfaa3fe8924be159" + integrity sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ== dependencies: - "@jest/console" "^26.5.2" - "@jest/environment" "^26.5.2" - "@jest/test-result" "^26.5.2" - "@jest/types" "^26.5.2" + "@jest/console" "^26.6.2" + "@jest/environment" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" chalk "^4.0.0" emittery "^0.7.1" exit "^0.1.2" graceful-fs "^4.2.4" - jest-config "^26.5.3" + jest-config "^26.6.3" jest-docblock "^26.0.0" - jest-haste-map "^26.5.2" - jest-leak-detector "^26.5.2" - jest-message-util "^26.5.2" - jest-resolve "^26.5.2" - jest-runtime "^26.5.3" - jest-util "^26.5.2" - jest-worker "^26.5.0" + jest-haste-map "^26.6.2" + jest-leak-detector "^26.6.2" + jest-message-util "^26.6.2" + jest-resolve "^26.6.2" + jest-runtime "^26.6.3" + jest-util "^26.6.2" + jest-worker "^26.6.2" source-map-support "^0.5.6" throat "^5.0.0" -jest-runtime@^26.5.3: - version "26.5.3" - resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.5.3.tgz#5882ae91fd88304310f069549e6bf82f3f198bea" - integrity sha512-IDjalmn2s/Tc4GvUwhPHZ0iaXCdMRq5p6taW9P8RpU+FpG01O3+H8z+p3rDCQ9mbyyyviDgxy/LHPLzrIOKBkQ== +jest-runtime@^26.6.3: + version "26.6.3" + resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.6.3.tgz#4f64efbcfac398331b74b4b3c82d27d401b8fa2b" + integrity sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw== dependencies: - "@jest/console" "^26.5.2" - "@jest/environment" "^26.5.2" - "@jest/fake-timers" "^26.5.2" - "@jest/globals" "^26.5.3" - "@jest/source-map" "^26.5.0" - "@jest/test-result" "^26.5.2" - "@jest/transform" "^26.5.2" - "@jest/types" "^26.5.2" + "@jest/console" "^26.6.2" + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/globals" "^26.6.2" + "@jest/source-map" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" "@types/yargs" "^15.0.0" chalk "^4.0.0" + cjs-module-lexer "^0.6.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" glob "^7.1.3" graceful-fs "^4.2.4" - jest-config "^26.5.3" - jest-haste-map "^26.5.2" - jest-message-util "^26.5.2" - jest-mock "^26.5.2" + jest-config "^26.6.3" + jest-haste-map "^26.6.2" + jest-message-util "^26.6.2" + jest-mock "^26.6.2" jest-regex-util "^26.0.0" - jest-resolve "^26.5.2" - jest-snapshot "^26.5.3" - jest-util "^26.5.2" - jest-validate "^26.5.3" + jest-resolve "^26.6.2" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" slash "^3.0.0" strip-bom "^4.0.0" yargs "^15.4.1" -jest-serializer@^26.5.0: - version "26.5.0" - resolved "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.5.0.tgz#f5425cc4c5f6b4b355f854b5f0f23ec6b962bc13" - integrity sha512-+h3Gf5CDRlSLdgTv7y0vPIAoLgX/SI7T4v6hy+TEXMgYbv+ztzbg5PSN6mUXAT/hXYHvZRWm+MaObVfqkhCGxA== +jest-serializer@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz#d139aafd46957d3a448f3a6cdabe2919ba0742d1" + integrity sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g== dependencies: "@types/node" "*" graceful-fs "^4.2.4" -jest-snapshot@^26.5.3: - version "26.5.3" - resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.5.3.tgz#f6b4b4b845f85d4b0dadd7cf119c55d0c1688601" - integrity sha512-ZgAk0Wm0JJ75WS4lGaeRfa0zIgpL0KD595+XmtwlIEMe8j4FaYHyZhP1LNOO+8fXq7HJ3hll54+sFV9X4+CGVw== +jest-snapshot@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.6.2.tgz#f3b0af1acb223316850bd14e1beea9837fb39c84" + integrity sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og== dependencies: "@babel/types" "^7.0.0" - "@jest/types" "^26.5.2" + "@jest/types" "^26.6.2" "@types/babel__traverse" "^7.0.4" "@types/prettier" "^2.0.0" chalk "^4.0.0" - expect "^26.5.3" + expect "^26.6.2" graceful-fs "^4.2.4" - jest-diff "^26.5.2" + jest-diff "^26.6.2" jest-get-type "^26.3.0" - jest-haste-map "^26.5.2" - jest-matcher-utils "^26.5.2" - jest-message-util "^26.5.2" - jest-resolve "^26.5.2" + jest-haste-map "^26.6.2" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-resolve "^26.6.2" natural-compare "^1.4.0" - pretty-format "^26.5.2" + pretty-format "^26.6.2" semver "^7.3.2" -jest-util@^26.1.0, jest-util@^26.5.2: +jest-util@^26.1.0: version "26.6.1" resolved "https://registry.npmjs.org/jest-util/-/jest-util-26.6.1.tgz#4cc0d09ec57f28d12d053887eec5dc976a352e9b" integrity sha512-xCLZUqVoqhquyPLuDXmH7ogceGctbW8SMyQVjD9o+1+NPWI7t0vO08udcFLVPLgKWcvc+zotaUv/RuaR6l8HIA== @@ -15042,29 +15083,41 @@ jest-util@^26.1.0, jest-util@^26.5.2: is-ci "^2.0.0" micromatch "^4.0.2" -jest-validate@^26.5.3: - version "26.5.3" - resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-26.5.3.tgz#eefd5a5c87059550548c5ad8d6589746c66929e3" - integrity sha512-LX07qKeAtY+lsU0o3IvfDdN5KH9OulEGOMN1sFo6PnEf5/qjS1LZIwNk9blcBeW94pQUI9dLN9FlDYDWI5tyaA== +jest-util@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" + integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== dependencies: - "@jest/types" "^26.5.2" + "@jest/types" "^26.6.2" + "@types/node" "*" + chalk "^4.0.0" + graceful-fs "^4.2.4" + is-ci "^2.0.0" + micromatch "^4.0.2" + +jest-validate@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-26.6.2.tgz#23d380971587150467342911c3d7b4ac57ab20ec" + integrity sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ== + dependencies: + "@jest/types" "^26.6.2" camelcase "^6.0.0" chalk "^4.0.0" jest-get-type "^26.3.0" leven "^3.1.0" - pretty-format "^26.5.2" + pretty-format "^26.6.2" -jest-watcher@^26.5.2: - version "26.5.2" - resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.5.2.tgz#2957f4461007e0769d74b537379ecf6b7c696916" - integrity sha512-i3m1NtWzF+FXfJ3ljLBB/WQEp4uaNhX7QcQUWMokcifFTUQBDFyUMEwk0JkJ1kopHbx7Een3KX0Q7+9koGM/Pw== +jest-watcher@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.6.2.tgz#a5b683b8f9d68dbcb1d7dae32172d2cca0592975" + integrity sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ== dependencies: - "@jest/test-result" "^26.5.2" - "@jest/types" "^26.5.2" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" - jest-util "^26.5.2" + jest-util "^26.6.2" string-length "^4.0.1" jest-worker@^26.2.1: @@ -15076,23 +15129,23 @@ jest-worker@^26.2.1: merge-stream "^2.0.0" supports-color "^7.0.0" -jest-worker@^26.5.0: - version "26.5.0" - resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-26.5.0.tgz#87deee86dbbc5f98d9919e0dadf2c40e3152fa30" - integrity sha512-kTw66Dn4ZX7WpjZ7T/SUDgRhapFRKWmisVAF0Rv4Fu8SLFD7eLbqpLvbxVqYhSgaWa7I+bW7pHnbyfNsH6stug== +jest-worker@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== dependencies: "@types/node" "*" merge-stream "^2.0.0" supports-color "^7.0.0" jest@^26.0.1: - version "26.5.3" - resolved "https://registry.npmjs.org/jest/-/jest-26.5.3.tgz#5e7a322d16f558dc565ca97639e85993ef5affe6" - integrity sha512-uJi3FuVSLmkZrWvaDyaVTZGLL8WcfynbRnFXyAHuEtYiSZ+ijDDIMOw1ytmftK+y/+OdAtsG9QrtbF7WIBmOyA== + version "26.6.3" + resolved "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz#40e8fdbe48f00dfa1f0ce8121ca74b88ac9148ef" + integrity sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q== dependencies: - "@jest/core" "^26.5.3" + "@jest/core" "^26.6.3" import-local "^3.0.2" - jest-cli "^26.5.3" + jest-cli "^26.6.3" jose@^1.27.1: version "1.27.1" @@ -19394,7 +19447,7 @@ pretty-error@^2.1.1: renderkid "^2.0.1" utila "~0.4" -pretty-format@^26.0.0, pretty-format@^26.4.2, pretty-format@^26.5.2, pretty-format@^26.6.1: +pretty-format@^26.0.0, pretty-format@^26.4.2, pretty-format@^26.6.1: version "26.6.1" resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.1.tgz#af9a2f63493a856acddeeb11ba6bcf61989660a8" integrity sha512-MeqqsP5PYcRBbGMvwzsyBdmAJ4EFX7pWFyl7x4+dMVg5pE0ZDdBIvEH2ergvIO+Gvwv1wh64YuOY9y5LuyY/GA== @@ -19404,6 +19457,16 @@ pretty-format@^26.0.0, pretty-format@^26.4.2, pretty-format@^26.5.2, pretty-form ansi-styles "^4.0.0" react-is "^17.0.1" +pretty-format@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" + integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== + dependencies: + "@jest/types" "^26.6.2" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^17.0.1" + pretty-hrtime@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" @@ -23994,10 +24057,10 @@ v8-compile-cache@^2.0.3: resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g== -v8-to-istanbul@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-6.0.1.tgz#7ef0e32faa10f841fe4c1b0f8de96ed067c0be1e" - integrity sha512-PzM1WlqquhBvsV+Gco6WSFeg1AGdD53ccMRkFeyHRE/KRZaVacPOmQYP3EeVgDBtKD2BJ8kgynBQ5OtKiHCH+w== +v8-to-istanbul@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-7.0.0.tgz#b4fe00e35649ef7785a9b7fcebcea05f37c332fc" + integrity sha512-fLL2rFuQpMtm9r8hrAV2apXX/WqHJ6+IC4/eQVdMDGBUgH/YMV4Gv3duk3kjmyg6uiQWBAA9nJwue4iJUOkHeA== dependencies: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" From c6ea43470b65e5f0693c97a5ead7544828977f40 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Dec 2020 07:44:33 +0100 Subject: [PATCH 84/86] build(deps): bump @svgr/rollup from 5.4.0 to 5.5.0 (#3643) Bumps [@svgr/rollup](https://github.com/gregberge/svgr) from 5.4.0 to 5.5.0. - [Release notes](https://github.com/gregberge/svgr/releases) - [Changelog](https://github.com/gregberge/svgr/blob/master/CHANGELOG.md) - [Commits](https://github.com/gregberge/svgr/compare/v5.4.0...v5.5.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- packages/cli/package.json | 2 +- yarn.lock | 1043 +++++++++++++++++++++++++------------ 2 files changed, 720 insertions(+), 325 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 0d585436bb..a4ddf2e519 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -44,7 +44,7 @@ "@sucrase/webpack-loader": "^2.0.0", "@svgr/plugin-jsx": "5.4.x", "@svgr/plugin-svgo": "5.4.x", - "@svgr/rollup": "5.4.x", + "@svgr/rollup": "5.5.x", "@svgr/webpack": "5.4.x", "@types/start-server-webpack-plugin": "^2.2.0", "@types/webpack-env": "^1.15.2", diff --git a/yarn.lock b/yarn.lock index 5eca6925bd..73405beac2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -126,28 +126,24 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/compat-data@^7.10.4", "@babel/compat-data@^7.11.0": - version "7.11.0" - resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.11.0.tgz#e9f73efe09af1355b723a7f39b11bad637d7c99c" - integrity sha512-TPSvJfv73ng0pfnEOh17bYMPQbI95+nGWc71Ss4vZdRBHTDqmM9Z8ZV4rYz8Ks7sfzc95n30k6ODIq5UGnXcYQ== - dependencies: - browserslist "^4.12.0" - invariant "^2.2.4" - semver "^5.5.0" +"@babel/compat-data@^7.12.5", "@babel/compat-data@^7.12.7": + version "7.12.7" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.12.7.tgz#9329b4782a7d6bbd7eef57e11addf91ee3ef1e41" + integrity sha512-YaxPMGs/XIWtYqrdEOZOCPsVWfEoriXopnsz3/i7apYPXQ3698UFhS6dVT1KN5qOsWmVgw/FOrmQgpRaZayGsw== -"@babel/core@^7.0.0", "@babel/core@^7.9.0": - version "7.11.1" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.11.1.tgz#2c55b604e73a40dc21b0e52650b11c65cf276643" - integrity sha512-XqF7F6FWQdKGGWAzGELL+aCO1p+lRY5Tj5/tbT3St1G8NaH70jhhDIKknIZaDans0OQBG5wRAldROLHSt44BgQ== +"@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.4.4", "@babel/core@^7.7.5", "@babel/core@^7.9.0": + version "7.12.9" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.12.9.tgz#fd450c4ec10cdbb980e2928b7aa7a28484593fc8" + integrity sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ== dependencies: "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.11.0" - "@babel/helper-module-transforms" "^7.11.0" - "@babel/helpers" "^7.10.4" - "@babel/parser" "^7.11.1" - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.11.0" - "@babel/types" "^7.11.0" + "@babel/generator" "^7.12.5" + "@babel/helper-module-transforms" "^7.12.1" + "@babel/helpers" "^7.12.5" + "@babel/parser" "^7.12.7" + "@babel/template" "^7.12.7" + "@babel/traverse" "^7.12.9" + "@babel/types" "^7.12.7" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.1" @@ -157,28 +153,6 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/core@^7.1.0", "@babel/core@^7.4.4", "@babel/core@^7.7.5": - version "7.9.6" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.9.6.tgz#d9aa1f580abf3b2286ef40b6904d390904c63376" - integrity sha512-nD3deLvbsApbHAHttzIssYqgb883yU/d9roe4RZymBCDaZryMJDbptVpEpeQuRh4BJ+SYI8le9YGxKvFEvl1Wg== - dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.6" - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helpers" "^7.9.6" - "@babel/parser" "^7.9.6" - "@babel/template" "^7.8.6" - "@babel/traverse" "^7.9.6" - "@babel/types" "^7.9.6" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.1" - json5 "^2.1.2" - lodash "^4.17.13" - resolve "^1.3.2" - semver "^5.4.1" - source-map "^0.5.0" - "@babel/generator@^7.10.5": version "7.10.5" resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.10.5.tgz#1b903554bc8c583ee8d25f1e8969732e6b829a69" @@ -206,6 +180,15 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.12.5": + version "7.12.5" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz#a2c50de5c8b6d708ab95be5e6053936c1884a4de" + integrity sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A== + dependencies: + "@babel/types" "^7.12.5" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" @@ -230,6 +213,15 @@ "@babel/helper-module-imports" "^7.10.4" "@babel/types" "^7.10.5" +"@babel/helper-builder-react-jsx-experimental@^7.12.4": + version "7.12.4" + resolved "https://registry.npmjs.org/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.12.4.tgz#55fc1ead5242caa0ca2875dcb8eed6d311e50f48" + integrity sha512-AjEa0jrQqNk7eDQOo0pTfUOwQBMF+xVqrausQwT9/rTKy0g04ggFNaJpaE09IQMn9yExluigWMJcj0WC7bq+Og== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-module-imports" "^7.12.1" + "@babel/types" "^7.12.1" + "@babel/helper-builder-react-jsx@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.4.tgz#8095cddbff858e6fa9c326daee54a2f2732c1d5d" @@ -238,15 +230,14 @@ "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-compilation-targets@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz#804ae8e3f04376607cc791b9d47d540276332bd2" - integrity sha512-a3rYhlsGV0UHNDvrtOXBg8/OpfV0OKTkxKPzIplS1zpx7CygDcWWxckxZeDd3gzPzC4kUT0A4nVFDK0wGMh4MQ== +"@babel/helper-compilation-targets@^7.12.5": + version "7.12.5" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz#cb470c76198db6a24e9dbc8987275631e5d29831" + integrity sha512-+qH6NrscMolUlzOYngSBMIOQpKUGPPsc61Bu5W10mg84LxZ7cmvnBHzARKbDoFxVvqqAbj6Tg6N7bSrWSPXMyw== dependencies: - "@babel/compat-data" "^7.10.4" - browserslist "^4.12.0" - invariant "^2.2.4" - levenary "^1.1.1" + "@babel/compat-data" "^7.12.5" + "@babel/helper-validator-option" "^7.12.1" + browserslist "^4.14.5" semver "^5.5.0" "@babel/helper-create-class-features-plugin@^7.10.4", "@babel/helper-create-class-features-plugin@^7.10.5": @@ -261,6 +252,17 @@ "@babel/helper-replace-supers" "^7.10.4" "@babel/helper-split-export-declaration" "^7.10.4" +"@babel/helper-create-class-features-plugin@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz#3c45998f431edd4a9214c5f1d3ad1448a6137f6e" + integrity sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-member-expression-to-functions" "^7.12.1" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-replace-supers" "^7.12.1" + "@babel/helper-split-export-declaration" "^7.10.4" + "@babel/helper-create-regexp-features-plugin@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8" @@ -270,6 +272,14 @@ "@babel/helper-regex" "^7.10.4" regexpu-core "^4.7.0" +"@babel/helper-create-regexp-features-plugin@^7.12.1": + version "7.12.7" + resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.7.tgz#2084172e95443fa0a09214ba1bb328f9aea1278f" + integrity sha512-idnutvQPdpbduutvi3JVfEgcVIHooQnhvhx0Nk9isOINOIGYkZea1Pk2JlJRiUnMefrlvr0vkByATBY/mB4vjQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + regexpu-core "^4.7.1" + "@babel/helper-define-map@^7.10.4": version "7.10.5" resolved "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz#b53c10db78a640800152692b13393147acb9bb30" @@ -317,6 +327,13 @@ dependencies: "@babel/types" "^7.10.5" +"@babel/helper-member-expression-to-functions@^7.12.1": + version "7.12.7" + resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz#aa77bd0396ec8114e5e30787efa78599d874a855" + integrity sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw== + dependencies: + "@babel/types" "^7.12.7" + "@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" @@ -324,7 +341,14 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5", "@babel/helper-module-transforms@^7.11.0", "@babel/helper-module-transforms@^7.9.0": +"@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.12.5": + version "7.12.5" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz#1bfc0229f794988f76ed0a4d4e90860850b54dfb" + integrity sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA== + dependencies: + "@babel/types" "^7.12.5" + +"@babel/helper-module-transforms@^7.10.4": version "7.11.0" resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg== @@ -337,6 +361,21 @@ "@babel/types" "^7.11.0" lodash "^4.17.19" +"@babel/helper-module-transforms@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz#7954fec71f5b32c48e4b303b437c34453fd7247c" + integrity sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w== + dependencies: + "@babel/helper-module-imports" "^7.12.1" + "@babel/helper-replace-supers" "^7.12.1" + "@babel/helper-simple-access" "^7.12.1" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/helper-validator-identifier" "^7.10.4" + "@babel/template" "^7.10.4" + "@babel/traverse" "^7.12.1" + "@babel/types" "^7.12.1" + lodash "^4.17.19" + "@babel/helper-optimise-call-expression@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" @@ -356,16 +395,14 @@ dependencies: lodash "^4.17.19" -"@babel/helper-remap-async-to-generator@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.4.tgz#fce8bea4e9690bbe923056ded21e54b4e8b68ed5" - integrity sha512-86Lsr6NNw3qTNl+TBcF1oRZMaVzJtbWTyTko+CQL/tvNvcGYEFKbLXDPxtW0HKk3McNOk4KzY55itGWCAGK5tg== +"@babel/helper-remap-async-to-generator@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz#8c4dbbf916314f6047dc05e6a2217074238347fd" + integrity sha512-9d0KQCRM8clMPcDwo8SevNs+/9a8yWVVmaE80FGJcEP8N1qToREmWEGnBn8BUlJhYRFz6fqxeRL1sl5Ogsed7A== dependencies: "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-wrap-function" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" + "@babel/types" "^7.12.1" "@babel/helper-replace-supers@^7.10.4": version "7.10.4" @@ -377,6 +414,16 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" +"@babel/helper-replace-supers@^7.12.1": + version "7.12.5" + resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz#f009a17543bbbbce16b06206ae73b63d3fca68d9" + integrity sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.12.1" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/traverse" "^7.12.5" + "@babel/types" "^7.12.5" + "@babel/helper-simple-access@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" @@ -385,6 +432,13 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" +"@babel/helper-simple-access@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz#32427e5aa61547d38eb1e6eaf5fd1426fdad9136" + integrity sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA== + dependencies: + "@babel/types" "^7.12.1" + "@babel/helper-skip-transparent-expression-wrappers@^7.11.0": version "7.11.0" resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz#eec162f112c2f58d3af0af125e3bb57665146729" @@ -392,6 +446,13 @@ dependencies: "@babel/types" "^7.11.0" +"@babel/helper-skip-transparent-expression-wrappers@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz#462dc63a7e435ade8468385c63d2b84cce4b3cbf" + integrity sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA== + dependencies: + "@babel/types" "^7.12.1" + "@babel/helper-split-export-declaration@^7.10.4", "@babel/helper-split-export-declaration@^7.11.0", "@babel/helper-split-export-declaration@^7.8.3": version "7.11.0" resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" @@ -404,6 +465,11 @@ resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== +"@babel/helper-validator-option@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.1.tgz#175567380c3e77d60ff98a54bb015fe78f2178d9" + integrity sha512-YpJabsXlJVWP0USHjnC/AQDTLlZERbON577YUVO/wLpqyj6HAtVYnWaQaN0iUN+1/tWn3c+uKKXjRut5115Y2A== + "@babel/helper-wrap-function@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87" @@ -414,14 +480,14 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helpers@^7.10.4", "@babel/helpers@^7.9.6": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044" - integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA== +"@babel/helpers@^7.12.5": + version "7.12.5" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz#1a1ba4a768d9b58310eda516c449913fe647116e" + integrity sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA== dependencies: "@babel/template" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" + "@babel/traverse" "^7.12.5" + "@babel/types" "^7.12.5" "@babel/highlight@^7.0.0", "@babel/highlight@^7.10.4", "@babel/highlight@^7.8.3": version "7.10.4" @@ -437,7 +503,7 @@ resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q== -"@babel/parser@^7.0.0", "@babel/parser@^7.11.0", "@babel/parser@^7.11.1": +"@babel/parser@^7.0.0", "@babel/parser@^7.11.0": version "7.11.3" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.11.3.tgz#9e1eae46738bcd08e23e867bab43e7b95299a8f9" integrity sha512-REo8xv7+sDxkKvoxEywIdsNFiZLybwdI7hcT5uEPyQrSMB4YQ973BfC9OOrD/81MaIjh6UxdulIQXkjmiH3PcA== @@ -452,16 +518,21 @@ resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.10.5.tgz#e7c6bf5a7deff957cec9f04b551e2762909d826b" integrity sha512-wfryxy4bE1UivvQKSQDU4/X6dr+i8bctjUjj8Zyt3DQy7NtPizJXT8M52nqpNKL+nq2PW8lxk4ZqLj0fD4B4hQ== -"@babel/plugin-proposal-async-generator-functions@^7.10.4": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz#3491cabf2f7c179ab820606cec27fed15e0e8558" - integrity sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg== +"@babel/parser@^7.12.7": + version "7.12.7" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz#fee7b39fe809d0e73e5b25eecaf5780ef3d73056" + integrity sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg== + +"@babel/plugin-proposal-async-generator-functions@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.1.tgz#dc6c1170e27d8aca99ff65f4925bd06b1c90550e" + integrity sha512-d+/o30tJxFxrA1lhzJqiUcEJdI6jKlNregCv5bASeGf2Q4MXmnwH7viDo7nhx1/ohf09oaH8j1GVYG/e3Yqk6A== dependencies: "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-remap-async-to-generator" "^7.10.4" + "@babel/helper-remap-async-to-generator" "^7.12.1" "@babel/plugin-syntax-async-generators" "^7.8.0" -"@babel/plugin-proposal-class-properties@^7.0.0", "@babel/plugin-proposal-class-properties@^7.10.4", "@babel/plugin-proposal-class-properties@^7.8.3": +"@babel/plugin-proposal-class-properties@^7.0.0", "@babel/plugin-proposal-class-properties@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807" integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg== @@ -469,6 +540,14 @@ "@babel/helper-create-class-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-proposal-class-properties@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz#a082ff541f2a29a4821065b8add9346c0c16e5de" + integrity sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-proposal-decorators@^7.8.3": version "7.10.5" resolved "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.10.5.tgz#42898bba478bc4b1ae242a703a953a7ad350ffb4" @@ -478,10 +557,10 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-decorators" "^7.10.4" -"@babel/plugin-proposal-dynamic-import@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz#ba57a26cb98b37741e9d5bca1b8b0ddf8291f17e" - integrity sha512-up6oID1LeidOOASNXgv/CFbgBqTuKJ0cJjz6An5tWD+NVBNlp3VNSBxv2ZdU7SYl3NxJC7agAQDApZusV6uFwQ== +"@babel/plugin-proposal-dynamic-import@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz#43eb5c2a3487ecd98c5c8ea8b5fdb69a2749b2dc" + integrity sha512-a4rhUSZFuq5W8/OO8H7BL5zspjnc1FLd9hlOxIK/f7qG4a0qsqk8uvF/ywgBA8/OmjsapjpvaEOYItfGG1qIvQ== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-dynamic-import" "^7.8.0" @@ -494,31 +573,31 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-export-default-from" "^7.10.4" -"@babel/plugin-proposal-export-namespace-from@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz#570d883b91031637b3e2958eea3c438e62c05f54" - integrity sha512-aNdf0LY6/3WXkhh0Fdb6Zk9j1NMD8ovj3F6r0+3j837Pn1S1PdNtcwJ5EG9WkVPNHPxyJDaxMaAOVq4eki0qbg== +"@babel/plugin-proposal-export-namespace-from@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.1.tgz#8b9b8f376b2d88f5dd774e4d24a5cc2e3679b6d4" + integrity sha512-6CThGf0irEkzujYS5LQcjBx8j/4aQGiVv7J9+2f7pGfxqyKh3WnmVJYW3hdrQjyksErMGBPQrCnHfOtna+WLbw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" -"@babel/plugin-proposal-json-strings@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz#593e59c63528160233bd321b1aebe0820c2341db" - integrity sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw== +"@babel/plugin-proposal-json-strings@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.1.tgz#d45423b517714eedd5621a9dfdc03fa9f4eb241c" + integrity sha512-GoLDUi6U9ZLzlSda2Df++VSqDJg3CG+dR0+iWsv6XRw1rEq+zwt4DirM9yrxW6XWaTpmai1cWJLMfM8qQJf+yw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.0" -"@babel/plugin-proposal-logical-assignment-operators@^7.11.0": - version "7.11.0" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz#9f80e482c03083c87125dee10026b58527ea20c8" - integrity sha512-/f8p4z+Auz0Uaf+i8Ekf1iM7wUNLcViFUGiPxKeXvxTSl63B875YPiVdUDdem7hREcI0E0kSpEhS8tF5RphK7Q== +"@babel/plugin-proposal-logical-assignment-operators@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.1.tgz#f2c490d36e1b3c9659241034a5d2cd50263a2751" + integrity sha512-k8ZmVv0JU+4gcUGeCDZOGd0lCIamU/sMtIiX3UWnUc5yzgq6YUGyEolNYD+MLYKfSzgECPcqetVcJP9Afe/aCA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.1", "@babel/plugin-proposal-nullish-coalescing-operator@^7.10.4": +"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.1": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.4.tgz#02a7e961fc32e6d5b2db0649e01bf80ddee7e04a" integrity sha512-wq5n1M3ZUlHl9sqT2ok1T2/MTt6AXE0e1Lz4WzWBr95LsAZ5qDXe4KnFuauYyEyLiohvXFMdbsOTMyLZs91Zlw== @@ -526,15 +605,23 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" -"@babel/plugin-proposal-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz#ce1590ff0a65ad12970a609d78855e9a4c1aef06" - integrity sha512-73/G7QoRoeNkLZFxsoCCvlg4ezE4eM+57PnOqgaPOozd5myfj7p0muD1mRVJvbUWbOzD+q3No2bWbaKy+DJ8DA== +"@babel/plugin-proposal-nullish-coalescing-operator@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.1.tgz#3ed4fff31c015e7f3f1467f190dbe545cd7b046c" + integrity sha512-nZY0ESiaQDI1y96+jk6VxMOaL4LPo/QDHBqL+SF3/vl6dHkTwHlOI8L4ZwuRBHgakRBw5zsVylel7QPbbGuYgg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" + +"@babel/plugin-proposal-numeric-separator@^7.12.7": + version "7.12.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.7.tgz#8bf253de8139099fea193b297d23a9d406ef056b" + integrity sha512-8c+uy0qmnRTeukiGsjLGy6uVs/TFjJchGXUeBqlG4VWYOdJWkhhVPdQ3uHwbmalfJwv2JsV0qffXP4asRfL2SQ== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-proposal-object-rest-spread@^7.0.0", "@babel/plugin-proposal-object-rest-spread@^7.11.0", "@babel/plugin-proposal-object-rest-spread@^7.9.6": +"@babel/plugin-proposal-object-rest-spread@^7.0.0", "@babel/plugin-proposal-object-rest-spread@^7.9.6": version "7.11.0" resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz#bd81f95a1f746760ea43b6c2d3d62b11790ad0af" integrity sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA== @@ -543,15 +630,24 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.0" "@babel/plugin-transform-parameters" "^7.10.4" -"@babel/plugin-proposal-optional-catch-binding@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz#31c938309d24a78a49d68fdabffaa863758554dd" - integrity sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g== +"@babel/plugin-proposal-object-rest-spread@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz#def9bd03cea0f9b72283dac0ec22d289c7691069" + integrity sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.0" + "@babel/plugin-transform-parameters" "^7.12.1" + +"@babel/plugin-proposal-optional-catch-binding@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.1.tgz#ccc2421af64d3aae50b558a71cede929a5ab2942" + integrity sha512-hFvIjgprh9mMw5v42sJWLI1lzU5L2sznP805zeT6rySVRA0Y18StRhDqhSxlap0oVgItRsB6WSROp4YnJTJz0g== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.10.1", "@babel/plugin-proposal-optional-chaining@^7.11.0": +"@babel/plugin-proposal-optional-chaining@^7.10.1": version "7.11.0" resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076" integrity sha512-v9fZIu3Y8562RRwhm1BbMRxtqZNFmFA2EG+pT2diuU8PT3H6T/KXoZ54KgYisfOFZHV6PfvAiBIZ9Rcz+/JCxA== @@ -560,7 +656,24 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" "@babel/plugin-syntax-optional-chaining" "^7.8.0" -"@babel/plugin-proposal-private-methods@^7.10.4", "@babel/plugin-proposal-private-methods@^7.8.3": +"@babel/plugin-proposal-optional-chaining@^7.12.7": + version "7.12.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.7.tgz#e02f0ea1b5dc59d401ec16fb824679f683d3303c" + integrity sha512-4ovylXZ0PWmwoOvhU2vhnzVNnm88/Sm9nx7V8BPgMvAzn5zDou3/Awy0EjglyubVHasJj+XCEkr/r1X3P5elCA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" + "@babel/plugin-syntax-optional-chaining" "^7.8.0" + +"@babel/plugin-proposal-private-methods@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz#86814f6e7a21374c980c10d38b4493e703f4a389" + integrity sha512-mwZ1phvH7/NHK6Kf8LP7MYDogGV+DKB1mryFOEwx5EBNQrosvIczzZFTUmWaeujd5xT6G1ELYWUz3CutMhjE1w== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-proposal-private-methods@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz#b160d972b8fdba5c7d111a145fc8c421fc2a6909" integrity sha512-wh5GJleuI8k3emgTg5KkJK6kHNsGEr0uBTDBuQUBJwckk9xs1ez79ioheEVVxMLyPscB0LfkbVHslQqIzWV6Bw== @@ -568,7 +681,15 @@ "@babel/helper-create-class-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-proposal-unicode-property-regex@^7.10.4", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": +"@babel/plugin-proposal-unicode-property-regex@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.1.tgz#2a183958d417765b9eae334f47758e5d6a82e072" + integrity sha512-MYq+l+PvHuw/rKUz1at/vb6nCnQ2gmJBNaM62z0OgH7B2W1D9pvkpYtlti9bGtizNIU1K3zm4bZF9F91efVY0w== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-proposal-unicode-property-regex@^7.4.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz#4483cda53041ce3413b7fe2f00022665ddfaa75d" integrity sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA== @@ -590,13 +711,20 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-class-properties@^7.0.0", "@babel/plugin-syntax-class-properties@^7.10.4", "@babel/plugin-syntax-class-properties@^7.8.3": +"@babel/plugin-syntax-class-properties@^7.0.0", "@babel/plugin-syntax-class-properties@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz#6644e6a0baa55a61f9e3231f6c9eeb6ee46c124c" integrity sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-syntax-class-properties@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz#bcb297c5366e79bebadef509549cd93b04f19978" + integrity sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-decorators@^7.10.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.10.4.tgz#6853085b2c429f9d322d02f5a635018cdeb2360c" @@ -660,6 +788,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-syntax-jsx@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz#9d9d357cc818aa7ae7935917c1257f67677a0926" + integrity sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" @@ -709,10 +844,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-top-level-await@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.4.tgz#4bbeb8917b54fcf768364e0a81f560e33a3ef57d" - integrity sha512-ni1brg4lXEmWyafKr0ccFWkJG0CeMt4WV1oyeBW6EFObF4oOHclbkj5cARxAPQyAQ2UTuplJyK4nfkXIMMFvsQ== +"@babel/plugin-syntax-top-level-await@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz#dd6c0b357ac1bb142d98537450a319625d13d2a0" + integrity sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A== dependencies: "@babel/helper-plugin-utils" "^7.10.4" @@ -730,29 +865,43 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-arrow-functions@^7.0.0", "@babel/plugin-transform-arrow-functions@^7.10.4", "@babel/plugin-transform-arrow-functions@^7.8.3": +"@babel/plugin-transform-arrow-functions@^7.0.0", "@babel/plugin-transform-arrow-functions@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz#e22960d77e697c74f41c501d44d73dbf8a6a64cd" integrity sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-async-to-generator@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz#41a5017e49eb6f3cda9392a51eef29405b245a37" - integrity sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ== +"@babel/plugin-transform-arrow-functions@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz#8083ffc86ac8e777fbe24b5967c4b2521f3cb2b3" + integrity sha512-5QB50qyN44fzzz4/qxDPQMBCTHgxg3n0xRBLJUmBlLoU/sFvxVWGZF/ZUfMVDQuJUKXaBhbupxIzIfZ6Fwk/0A== dependencies: - "@babel/helper-module-imports" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-remap-async-to-generator" "^7.10.4" -"@babel/plugin-transform-block-scoped-functions@^7.0.0", "@babel/plugin-transform-block-scoped-functions@^7.10.4": +"@babel/plugin-transform-async-to-generator@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz#3849a49cc2a22e9743cbd6b52926d30337229af1" + integrity sha512-SDtqoEcarK1DFlRJ1hHRY5HvJUj5kX4qmtpMAm2QnhOlyuMC4TMdCRgW6WXpv93rZeYNeLP22y8Aq2dbcDRM1A== + dependencies: + "@babel/helper-module-imports" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-remap-async-to-generator" "^7.12.1" + +"@babel/plugin-transform-block-scoped-functions@^7.0.0": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz#1afa595744f75e43a91af73b0d998ecfe4ebc2e8" integrity sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-block-scoped-functions@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.1.tgz#f2a1a365bde2b7112e0a6ded9067fdd7c07905d9" + integrity sha512-5OpxfuYnSgPalRpo8EWGPzIYf0lHBWORCkj5M0oLBwHdlux9Ri36QqGW3/LR13RSVOAoUUMzoPI/jpE4ABcHoA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-block-scoping@^7.0.0", "@babel/plugin-transform-block-scoping@^7.8.3": version "7.11.1" resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz#5b7efe98852bef8d652c0b28144cd93a9e4b5215" @@ -760,14 +909,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-block-scoping@^7.10.4": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.5.tgz#b81b8aafefbfe68f0f65f7ef397b9ece68a6037d" - integrity sha512-6Ycw3hjpQti0qssQcA6AMSFDHeNJ++R6dIMnpRqUjFeBBTmTDPa8zgF90OVfTvAo11mXZTlVUViY1g8ffrURLg== +"@babel/plugin-transform-block-scoping@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.1.tgz#f0ee727874b42a208a48a586b84c3d222c2bbef1" + integrity sha512-zJyAC9sZdE60r1nVQHblcfCj29Dh2Y0DOvlMkcqSo0ckqjiCwNiUezUKw+RjOCwGfpLRwnAeQ2XlLpsnGkvv9w== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-classes@^7.0.0", "@babel/plugin-transform-classes@^7.10.4", "@babel/plugin-transform-classes@^7.9.5": +"@babel/plugin-transform-classes@^7.0.0", "@babel/plugin-transform-classes@^7.9.5": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz#405136af2b3e218bc4a1926228bc917ab1a0adc7" integrity sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA== @@ -781,21 +930,57 @@ "@babel/helper-split-export-declaration" "^7.10.4" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.0.0", "@babel/plugin-transform-computed-properties@^7.10.4": +"@babel/plugin-transform-classes@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz#65e650fcaddd3d88ddce67c0f834a3d436a32db6" + integrity sha512-/74xkA7bVdzQTBeSUhLLJgYIcxw/dpEpCdRDiHgPJ3Mv6uC11UhjpOhl72CgqbBCmt1qtssCyB2xnJm1+PFjog== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.4" + "@babel/helper-define-map" "^7.10.4" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-optimise-call-expression" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.12.1" + "@babel/helper-split-export-declaration" "^7.10.4" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.0.0": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz#9ded83a816e82ded28d52d4b4ecbdd810cdfc0eb" integrity sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-destructuring@^7.0.0", "@babel/plugin-transform-destructuring@^7.10.4", "@babel/plugin-transform-destructuring@^7.9.5": +"@babel/plugin-transform-computed-properties@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.12.1.tgz#d68cf6c9b7f838a8a4144badbe97541ea0904852" + integrity sha512-vVUOYpPWB7BkgUWPo4C44mUQHpTZXakEqFjbv8rQMg7TC6S6ZhGZ3otQcRH6u7+adSlE5i0sp63eMC/XGffrzg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-destructuring@^7.0.0", "@babel/plugin-transform-destructuring@^7.9.5": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz#70ddd2b3d1bea83d01509e9bb25ddb3a74fc85e5" integrity sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-dotall-regex@^7.10.4", "@babel/plugin-transform-dotall-regex@^7.4.4": +"@babel/plugin-transform-destructuring@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.1.tgz#b9a570fe0d0a8d460116413cb4f97e8e08b2f847" + integrity sha512-fRMYFKuzi/rSiYb2uRLiUENJOKq4Gnl+6qOv5f8z0TZXg3llUwUhsNNwrwaT/6dUhJTzNpBr+CUvEWBtfNY1cw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-dotall-regex@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.1.tgz#a1d16c14862817b6409c0a678d6f9373ca9cd975" + integrity sha512-B2pXeRKoLszfEW7J4Hg9LoFaWEbr/kzo3teWHmtFCszjRNa/b40f9mfeqZsIDLLt/FjwQ6pz/Gdlwy85xNckBA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-dotall-regex@^7.4.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz#469c2062105c1eb6a040eaf4fac4b488078395ee" integrity sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA== @@ -803,17 +988,17 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-duplicate-keys@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz#697e50c9fee14380fe843d1f306b295617431e47" - integrity sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA== +"@babel/plugin-transform-duplicate-keys@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.1.tgz#745661baba295ac06e686822797a69fbaa2ca228" + integrity sha512-iRght0T0HztAb/CazveUpUQrZY+aGKKaWXMJ4uf9YJtqxSUe09j3wteztCUDRHs+SRAL7yMuFqUsLoAKKzgXjw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-exponentiation-operator@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz#5ae338c57f8cf4001bdb35607ae66b92d665af2e" - integrity sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw== +"@babel/plugin-transform-exponentiation-operator@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.1.tgz#b0f2ed356ba1be1428ecaf128ff8a24f02830ae0" + integrity sha512-7tqwy2bv48q+c1EHbXK0Zx3KXd2RVQp6OC7PbwFNt/dPTAV3Lu5sWtWuAj8owr5wqtWnqHfl2/mJlUmqkChKug== dependencies: "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" @@ -834,14 +1019,21 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-flow" "^7.8.3" -"@babel/plugin-transform-for-of@^7.0.0", "@babel/plugin-transform-for-of@^7.10.4", "@babel/plugin-transform-for-of@^7.9.0": +"@babel/plugin-transform-for-of@^7.0.0", "@babel/plugin-transform-for-of@^7.9.0": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz#c08892e8819d3a5db29031b115af511dbbfebae9" integrity sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-function-name@^7.0.0", "@babel/plugin-transform-function-name@^7.10.4": +"@babel/plugin-transform-for-of@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.12.1.tgz#07640f28867ed16f9511c99c888291f560921cfa" + integrity sha512-Zaeq10naAsuHo7heQvyV0ptj4dlZJwZgNAtBYBnu5nNKJoW62m0zKcIEyVECrUKErkUkg6ajMy4ZfnVZciSBhg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-function-name@^7.0.0": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz#6a467880e0fc9638514ba369111811ddbe2644b7" integrity sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg== @@ -849,30 +1041,52 @@ "@babel/helper-function-name" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-literals@^7.0.0", "@babel/plugin-transform-literals@^7.10.4": +"@babel/plugin-transform-function-name@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.1.tgz#2ec76258c70fe08c6d7da154003a480620eba667" + integrity sha512-JF3UgJUILoFrFMEnOJLJkRHSk6LUSXLmEFsA23aR2O5CSLUxbeUX1IZ1YQ7Sn0aXb601Ncwjx73a+FVqgcljVw== + dependencies: + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-literals@^7.0.0": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz#9f42ba0841100a135f22712d0e391c462f571f3c" integrity sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-member-expression-literals@^7.0.0", "@babel/plugin-transform-member-expression-literals@^7.10.4": +"@babel/plugin-transform-literals@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.1.tgz#d73b803a26b37017ddf9d3bb8f4dc58bfb806f57" + integrity sha512-+PxVGA+2Ag6uGgL0A5f+9rklOnnMccwEBzwYFL3EUaKuiyVnUipyXncFcfjSkbimLrODoqki1U9XxZzTvfN7IQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-member-expression-literals@^7.0.0": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz#b1ec44fcf195afcb8db2c62cd8e551c881baf8b7" integrity sha512-0bFOvPyAoTBhtcJLr9VcwZqKmSjFml1iVxvPL0ReomGU53CX53HsM4h2SzckNdkQcHox1bpAqzxBI1Y09LlBSw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-modules-amd@^7.10.4": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz#1b9cddaf05d9e88b3aad339cb3e445c4f020a9b1" - integrity sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw== +"@babel/plugin-transform-member-expression-literals@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.1.tgz#496038602daf1514a64d43d8e17cbb2755e0c3ad" + integrity sha512-1sxePl6z9ad0gFMB9KqmYofk34flq62aqMt9NqliS/7hPEpURUCMbyHXrMPlo282iY7nAvUB1aQd5mg79UD9Jg== dependencies: - "@babel/helper-module-transforms" "^7.10.5" + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-modules-amd@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.12.1.tgz#3154300b026185666eebb0c0ed7f8415fefcf6f9" + integrity sha512-tDW8hMkzad5oDtzsB70HIQQRBiTKrhfgwC/KkJeGsaNFTdWhKNt/BiE8c5yj19XiGyrxpbkOfH87qkNg1YGlOQ== + dependencies: + "@babel/helper-module-transforms" "^7.12.1" "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.0.0", "@babel/plugin-transform-modules-commonjs@^7.10.4", "@babel/plugin-transform-modules-commonjs@^7.4.4": +"@babel/plugin-transform-modules-commonjs@^7.0.0", "@babel/plugin-transform-modules-commonjs@^7.4.4": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz#66667c3eeda1ebf7896d41f1f16b17105a2fbca0" integrity sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w== @@ -882,39 +1096,50 @@ "@babel/helper-simple-access" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-systemjs@^7.10.4": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85" - integrity sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw== +"@babel/plugin-transform-modules-commonjs@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.1.tgz#fa403124542636c786cf9b460a0ffbb48a86e648" + integrity sha512-dY789wq6l0uLY8py9c1B48V8mVL5gZh/+PQ5ZPrylPYsnAvnEMjqsUXkuoDVPeVK+0VyGar+D08107LzDQ6pag== dependencies: - "@babel/helper-hoist-variables" "^7.10.4" - "@babel/helper-module-transforms" "^7.10.5" + "@babel/helper-module-transforms" "^7.12.1" "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-simple-access" "^7.12.1" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-umd@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz#9a8481fe81b824654b3a0b65da3df89f3d21839e" - integrity sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA== +"@babel/plugin-transform-modules-systemjs@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.1.tgz#663fea620d593c93f214a464cd399bf6dc683086" + integrity sha512-Hn7cVvOavVh8yvW6fLwveFqSnd7rbQN3zJvoPNyNaQSvgfKmDBO9U1YL9+PCXGRlZD9tNdWTy5ACKqMuzyn32Q== dependencies: - "@babel/helper-module-transforms" "^7.10.4" + "@babel/helper-hoist-variables" "^7.10.4" + "@babel/helper-module-transforms" "^7.12.1" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-validator-identifier" "^7.10.4" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-umd@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.12.1.tgz#eb5a218d6b1c68f3d6217b8fa2cc82fec6547902" + integrity sha512-aEIubCS0KHKM0zUos5fIoQm+AZUMt1ZvMpqz0/H5qAQ7vWylr9+PLYurT+Ic7ID/bKLd4q8hDovaG3Zch2uz5Q== + dependencies: + "@babel/helper-module-transforms" "^7.12.1" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-named-capturing-groups-regex@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz#78b4d978810b6f3bcf03f9e318f2fc0ed41aecb6" - integrity sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA== +"@babel/plugin-transform-named-capturing-groups-regex@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.1.tgz#b407f5c96be0d9f5f88467497fa82b30ac3e8753" + integrity sha512-tB43uQ62RHcoDp9v2Nsf+dSM8sbNodbEicbQNA53zHz8pWUhsgHSJCGpt7daXxRydjb0KnfmB+ChXOv3oADp1Q== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/helper-create-regexp-features-plugin" "^7.12.1" -"@babel/plugin-transform-new-target@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz#9097d753cb7b024cb7381a3b2e52e9513a9c6888" - integrity sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw== +"@babel/plugin-transform-new-target@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.1.tgz#80073f02ee1bb2d365c3416490e085c95759dec0" + integrity sha512-+eW/VLcUL5L9IvJH7rT1sT0CzkdUTvPrXC2PXTn/7z7tXLBuKvezYbGdxD5WMRoyvyaujOq2fWoKl869heKjhw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-object-super@^7.0.0", "@babel/plugin-transform-object-super@^7.10.4": +"@babel/plugin-transform-object-super@^7.0.0": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz#d7146c4d139433e7a6526f888c667e314a093894" integrity sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ== @@ -922,6 +1147,14 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-replace-supers" "^7.10.4" +"@babel/plugin-transform-object-super@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.1.tgz#4ea08696b8d2e65841d0c7706482b048bed1066e" + integrity sha512-AvypiGJH9hsquNUn+RXVcBdeE3KHPZexWRdimhuV59cSoOt5kFBmqlByorAeUlGG2CJWd0U+4ZtNKga/TB0cAw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-replace-supers" "^7.12.1" + "@babel/plugin-transform-parameters@^7.0.0", "@babel/plugin-transform-parameters@^7.10.4", "@babel/plugin-transform-parameters@^7.9.5": version "7.10.5" resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz#59d339d58d0b1950435f4043e74e2510005e2c4a" @@ -930,53 +1163,72 @@ "@babel/helper-get-function-arity" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-property-literals@^7.0.0", "@babel/plugin-transform-property-literals@^7.10.4": +"@babel/plugin-transform-parameters@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.1.tgz#d2e963b038771650c922eff593799c96d853255d" + integrity sha512-xq9C5EQhdPK23ZeCdMxl8bbRnAgHFrw5EOC3KJUsSylZqdkCaFEXxGSBuTSObOpiiHHNyb82es8M1QYgfQGfNg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-property-literals@^7.0.0": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz#f6fe54b6590352298785b83edd815d214c42e3c0" integrity sha512-ofsAcKiUxQ8TY4sScgsGeR2vJIsfrzqvFb9GvJ5UdXDzl+MyYCaBj/FGzXuv7qE0aJcjWMILny1epqelnFlz8g== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-react-constant-elements@^7.7.4", "@babel/plugin-transform-react-constant-elements@^7.9.0": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.10.4.tgz#0f485260bf1c29012bb973e7e404749eaac12c9e" - integrity sha512-cYmQBW1pXrqBte1raMkAulXmi7rjg3VI6ZLg9QIic8Hq7BtYXaWuZSxsr2siOMI6SWwpxjWfnwhTUrd7JlAV7g== +"@babel/plugin-transform-property-literals@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.1.tgz#41bc81200d730abb4456ab8b3fbd5537b59adecd" + integrity sha512-6MTCR/mZ1MQS+AwZLplX4cEySjCpnIF26ToWo942nqn8hXSm7McaHQNeGx/pt7suI1TWOWMfa/NgBhiqSnX0cQ== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-react-display-name@^7.0.0", "@babel/plugin-transform-react-display-name@^7.10.4": +"@babel/plugin-transform-react-constant-elements@^7.12.1", "@babel/plugin-transform-react-constant-elements@^7.9.0": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.12.1.tgz#4471f0851feec3231cc9aaa0dccde39947c1ac1e" + integrity sha512-KOHd0tIRLoER+J+8f9DblZDa1fLGPwaaN1DI1TVHuQFOpjHV22C3CUB3obeC4fexHY9nx+fH0hQNvLFFfA1mxA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-react-display-name@^7.0.0": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.10.4.tgz#b5795f4e3e3140419c3611b7a2a3832b9aef328d" integrity sha512-Zd4X54Mu9SBfPGnEcaGcOrVAYOtjT2on8QZkLKEq1S/tHexG39d9XXGZv19VfRrDjPJzFmPfTAqOQS1pfFOujw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-react-jsx-development@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.10.4.tgz#6ec90f244394604623880e15ebc3c34c356258ba" - integrity sha512-RM3ZAd1sU1iQ7rI2dhrZRZGv0aqzNQMbkIUCS1txYpi9wHQ2ZHNjo5TwX+UD6pvFW4AbWqLVYvKy5qJSAyRGjQ== - dependencies: - "@babel/helper-builder-react-jsx-experimental" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-jsx" "^7.10.4" - -"@babel/plugin-transform-react-jsx-self@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.4.tgz#cd301a5fed8988c182ed0b9d55e9bd6db0bd9369" - integrity sha512-yOvxY2pDiVJi0axdTWHSMi5T0DILN+H+SaeJeACHKjQLezEzhLx9nEF9xgpBLPtkZsks9cnb5P9iBEi21En3gg== +"@babel/plugin-transform-react-display-name@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.12.1.tgz#1cbcd0c3b1d6648c55374a22fc9b6b7e5341c00d" + integrity sha512-cAzB+UzBIrekfYxyLlFqf/OagTvHLcVBb5vpouzkYkBclRPraiygVnafvAoipErZLI8ANv8Ecn6E/m5qPXD26w== dependencies: "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-jsx" "^7.10.4" -"@babel/plugin-transform-react-jsx-source@^7.10.4": - version "7.10.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.10.5.tgz#34f1779117520a779c054f2cdd9680435b9222b4" - integrity sha512-wTeqHVkN1lfPLubRiZH3o73f4rfon42HpgxUSs86Nc+8QIcm/B9s8NNVXu/gwGcOyd7yDib9ikxoDLxJP0UiDA== +"@babel/plugin-transform-react-jsx-development@^7.12.7": + version "7.12.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.7.tgz#4c2a647de79c7e2b16bfe4540677ba3121e82a08" + integrity sha512-Rs3ETtMtR3VLXFeYRChle5SsP/P9Jp/6dsewBQfokDSzKJThlsuFcnzLTDRALiUmTC48ej19YD9uN1mupEeEDg== + dependencies: + "@babel/helper-builder-react-jsx-experimental" "^7.12.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-jsx" "^7.12.1" + +"@babel/plugin-transform-react-jsx-self@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.12.1.tgz#ef43cbca2a14f1bd17807dbe4376ff89d714cf28" + integrity sha512-FbpL0ieNWiiBB5tCldX17EtXgmzeEZjFrix72rQYeq9X6nUK38HCaxexzVQrZWXanxKJPKVVIU37gFjEQYkPkA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-jsx" "^7.10.4" -"@babel/plugin-transform-react-jsx@^7.0.0", "@babel/plugin-transform-react-jsx@^7.10.4": +"@babel/plugin-transform-react-jsx-source@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.12.1.tgz#d07de6863f468da0809edcf79a1aa8ce2a82a26b" + integrity sha512-keQ5kBfjJNRc6zZN1/nVHCd6LLIHq4aUKcVnvE/2l+ZZROSbqoiGFRtT5t3Is89XJxBQaP7NLZX2jgGHdZvvFQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-react-jsx@^7.0.0": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.10.4.tgz#673c9f913948764a4421683b2bef2936968fddf2" integrity sha512-L+MfRhWjX0eI7Js093MM6MacKU4M6dnCRa/QPDwYMxjljzSCzzlzKzj9Pk4P3OtrPcxr2N3znR419nr3Xw+65A== @@ -986,36 +1238,53 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-jsx" "^7.10.4" -"@babel/plugin-transform-react-pure-annotations@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.10.4.tgz#3eefbb73db94afbc075f097523e445354a1c6501" - integrity sha512-+njZkqcOuS8RaPakrnR9KvxjoG1ASJWpoIv/doyWngId88JoFlPlISenGXjrVacZUIALGUr6eodRs1vmPnF23A== +"@babel/plugin-transform-react-jsx@^7.12.7": + version "7.12.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.7.tgz#8b14d45f6eccd41b7f924bcb65c021e9f0a06f7f" + integrity sha512-YFlTi6MEsclFAPIDNZYiCRbneg1MFGao9pPG9uD5htwE0vDbPaMUMeYd6itWjw7K4kro4UbdQf3ljmFl9y48dQ== + dependencies: + "@babel/helper-builder-react-jsx" "^7.10.4" + "@babel/helper-builder-react-jsx-experimental" "^7.12.4" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-jsx" "^7.12.1" + +"@babel/plugin-transform-react-pure-annotations@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.12.1.tgz#05d46f0ab4d1339ac59adf20a1462c91b37a1a42" + integrity sha512-RqeaHiwZtphSIUZ5I85PEH19LOSzxfuEazoY7/pWASCAIBuATQzpSVD+eT6MebeeZT2F4eSL0u4vw6n4Nm0Mjg== dependencies: "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-regenerator@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz#2015e59d839074e76838de2159db421966fd8b63" - integrity sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw== +"@babel/plugin-transform-regenerator@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz#5f0a28d842f6462281f06a964e88ba8d7ab49753" + integrity sha512-gYrHqs5itw6i4PflFX3OdBPMQdPbF4bj2REIUxlMRUFk0/ZOAIpDFuViuxPjUL7YC8UPnf+XG7/utJvqXdPKng== dependencies: regenerator-transform "^0.14.2" -"@babel/plugin-transform-reserved-words@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz#8f2682bcdcef9ed327e1b0861585d7013f8a54dd" - integrity sha512-hGsw1O6Rew1fkFbDImZIEqA8GoidwTAilwCyWqLBM9f+e/u/sQMQu7uX6dyokfOayRuuVfKOW4O7HvaBWM+JlQ== +"@babel/plugin-transform-reserved-words@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.1.tgz#6fdfc8cc7edcc42b36a7c12188c6787c873adcd8" + integrity sha512-pOnUfhyPKvZpVyBHhSBoX8vfA09b7r00Pmm1sH+29ae2hMTKVmSp4Ztsr8KBKjLjx17H0eJqaRC3bR2iThM54A== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-shorthand-properties@^7.0.0", "@babel/plugin-transform-shorthand-properties@^7.10.4", "@babel/plugin-transform-shorthand-properties@^7.8.3": +"@babel/plugin-transform-shorthand-properties@^7.0.0", "@babel/plugin-transform-shorthand-properties@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz#9fd25ec5cdd555bb7f473e5e6ee1c971eede4dd6" integrity sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-spread@^7.0.0", "@babel/plugin-transform-spread@^7.11.0", "@babel/plugin-transform-spread@^7.8.3": +"@babel/plugin-transform-shorthand-properties@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz#0bf9cac5550fce0cfdf043420f661d645fdc75e3" + integrity sha512-GFZS3c/MhX1OusqB1MZ1ct2xRzX5ppQh2JU1h2Pnfk88HtFTM+TWQqJNfwkmxtPQtb/s1tk87oENfXJlx7rSDw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-spread@^7.0.0", "@babel/plugin-transform-spread@^7.8.3": version "7.11.0" resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz#fa84d300f5e4f57752fe41a6d1b3c554f13f17cc" integrity sha512-UwQYGOqIdQJe4aWNyS7noqAnN2VbaczPLiEtln+zPowRNlD+79w3oi2TWfYe0eZgd+gjZCbsydN7lzWysDt+gw== @@ -1023,15 +1292,22 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" -"@babel/plugin-transform-sticky-regex@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz#8f3889ee8657581130a29d9cc91d7c73b7c4a28d" - integrity sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ== +"@babel/plugin-transform-spread@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.12.1.tgz#527f9f311be4ec7fdc2b79bb89f7bf884b3e1e1e" + integrity sha512-vuLp8CP0BE18zVYjsEBZ5xoCecMK6LBMMxYzJnh01rxQRvhNhH1csMMmBfNo5tGpGO+NhdSNW2mzIvBu3K1fng== dependencies: "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-regex" "^7.10.4" + "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" -"@babel/plugin-transform-template-literals@^7.0.0", "@babel/plugin-transform-template-literals@^7.10.4", "@babel/plugin-transform-template-literals@^7.8.3": +"@babel/plugin-transform-sticky-regex@^7.12.7": + version "7.12.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.7.tgz#560224613ab23987453948ed21d0b0b193fa7fad" + integrity sha512-VEiqZL5N/QvDbdjfYQBhruN0HYjSPjC4XkeqW4ny/jNtH9gcbgaqBIXYEZCNnESMAGs0/K/R7oFGMhOyu/eIxg== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-template-literals@^7.0.0", "@babel/plugin-transform-template-literals@^7.8.3": version "7.10.5" resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz#78bc5d626a6642db3312d9d0f001f5e7639fde8c" integrity sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw== @@ -1039,10 +1315,17 @@ "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-typeof-symbol@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz#9509f1a7eec31c4edbffe137c16cc33ff0bc5bfc" - integrity sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA== +"@babel/plugin-transform-template-literals@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.12.1.tgz#b43ece6ed9a79c0c71119f576d299ef09d942843" + integrity sha512-b4Zx3KHi+taXB1dVRBhVJtEPi9h1THCeKmae2qP0YdUHIFhVjtpqqNfxeVAa1xeHVhAy4SbHxEwx5cltAu5apw== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-transform-typeof-symbol@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.1.tgz#9ca6be343d42512fbc2e68236a82ae64bc7af78a" + integrity sha512-EPGgpGy+O5Kg5pJFNDKuxt9RdmTgj5sgrus2XVeMp/ZIbOESadgILUbm50SNpghOh3/6yrbsH+NB5+WJTmsA7Q== dependencies: "@babel/helper-plugin-utils" "^7.10.4" @@ -1055,45 +1338,46 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-typescript" "^7.10.4" -"@babel/plugin-transform-unicode-escapes@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.4.tgz#feae523391c7651ddac115dae0a9d06857892007" - integrity sha512-y5XJ9waMti2J+e7ij20e+aH+fho7Wb7W8rNuu72aKRwCHFqQdhkdU2lo3uZ9tQuboEJcUFayXdARhcxLQ3+6Fg== +"@babel/plugin-transform-unicode-escapes@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz#5232b9f81ccb07070b7c3c36c67a1b78f1845709" + integrity sha512-I8gNHJLIc7GdApm7wkVnStWssPNbSRMPtgHdmH3sRM1zopz09UWPS4x5V4n1yz/MIWTVnJ9sp6IkuXdWM4w+2Q== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-unicode-regex@^7.10.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz#e56d71f9282fac6db09c82742055576d5e6d80a8" - integrity sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A== +"@babel/plugin-transform-unicode-regex@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.1.tgz#cc9661f61390db5c65e3febaccefd5c6ac3faecb" + integrity sha512-SqH4ClNngh/zGwHZOOQMTD+e8FGWexILV+ePMyiDJttAWRh5dhDL8rcl5lSgU3Huiq6Zn6pWTMvdPAb21Dwdyg== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.10.4" + "@babel/helper-create-regexp-features-plugin" "^7.12.1" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/preset-env@^7.9.5", "@babel/preset-env@^7.9.6": - version "7.11.5" - resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.11.5.tgz#18cb4b9379e3e92ffea92c07471a99a2914e4272" - integrity sha512-kXqmW1jVcnB2cdueV+fyBM8estd5mlNfaQi6lwLgRwCby4edpavgbFhiBNjmWA3JpB/yZGSISa7Srf+TwxDQoA== +"@babel/preset-env@^7.12.1", "@babel/preset-env@^7.9.5", "@babel/preset-env@^7.9.6": + version "7.12.7" + resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.7.tgz#54ea21dbe92caf6f10cb1a0a576adc4ebf094b55" + integrity sha512-OnNdfAr1FUQg7ksb7bmbKoby4qFOHw6DKWWUNB9KqnnCldxhxJlP+21dpyaWFmf2h0rTbOkXJtAGevY3XW1eew== dependencies: - "@babel/compat-data" "^7.11.0" - "@babel/helper-compilation-targets" "^7.10.4" - "@babel/helper-module-imports" "^7.10.4" + "@babel/compat-data" "^7.12.7" + "@babel/helper-compilation-targets" "^7.12.5" + "@babel/helper-module-imports" "^7.12.5" "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-proposal-async-generator-functions" "^7.10.4" - "@babel/plugin-proposal-class-properties" "^7.10.4" - "@babel/plugin-proposal-dynamic-import" "^7.10.4" - "@babel/plugin-proposal-export-namespace-from" "^7.10.4" - "@babel/plugin-proposal-json-strings" "^7.10.4" - "@babel/plugin-proposal-logical-assignment-operators" "^7.11.0" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.4" - "@babel/plugin-proposal-numeric-separator" "^7.10.4" - "@babel/plugin-proposal-object-rest-spread" "^7.11.0" - "@babel/plugin-proposal-optional-catch-binding" "^7.10.4" - "@babel/plugin-proposal-optional-chaining" "^7.11.0" - "@babel/plugin-proposal-private-methods" "^7.10.4" - "@babel/plugin-proposal-unicode-property-regex" "^7.10.4" + "@babel/helper-validator-option" "^7.12.1" + "@babel/plugin-proposal-async-generator-functions" "^7.12.1" + "@babel/plugin-proposal-class-properties" "^7.12.1" + "@babel/plugin-proposal-dynamic-import" "^7.12.1" + "@babel/plugin-proposal-export-namespace-from" "^7.12.1" + "@babel/plugin-proposal-json-strings" "^7.12.1" + "@babel/plugin-proposal-logical-assignment-operators" "^7.12.1" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.12.1" + "@babel/plugin-proposal-numeric-separator" "^7.12.7" + "@babel/plugin-proposal-object-rest-spread" "^7.12.1" + "@babel/plugin-proposal-optional-catch-binding" "^7.12.1" + "@babel/plugin-proposal-optional-chaining" "^7.12.7" + "@babel/plugin-proposal-private-methods" "^7.12.1" + "@babel/plugin-proposal-unicode-property-regex" "^7.12.1" "@babel/plugin-syntax-async-generators" "^7.8.0" - "@babel/plugin-syntax-class-properties" "^7.10.4" + "@babel/plugin-syntax-class-properties" "^7.12.1" "@babel/plugin-syntax-dynamic-import" "^7.8.0" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" "@babel/plugin-syntax-json-strings" "^7.8.0" @@ -1103,45 +1387,42 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.0" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" "@babel/plugin-syntax-optional-chaining" "^7.8.0" - "@babel/plugin-syntax-top-level-await" "^7.10.4" - "@babel/plugin-transform-arrow-functions" "^7.10.4" - "@babel/plugin-transform-async-to-generator" "^7.10.4" - "@babel/plugin-transform-block-scoped-functions" "^7.10.4" - "@babel/plugin-transform-block-scoping" "^7.10.4" - "@babel/plugin-transform-classes" "^7.10.4" - "@babel/plugin-transform-computed-properties" "^7.10.4" - "@babel/plugin-transform-destructuring" "^7.10.4" - "@babel/plugin-transform-dotall-regex" "^7.10.4" - "@babel/plugin-transform-duplicate-keys" "^7.10.4" - "@babel/plugin-transform-exponentiation-operator" "^7.10.4" - "@babel/plugin-transform-for-of" "^7.10.4" - "@babel/plugin-transform-function-name" "^7.10.4" - "@babel/plugin-transform-literals" "^7.10.4" - "@babel/plugin-transform-member-expression-literals" "^7.10.4" - "@babel/plugin-transform-modules-amd" "^7.10.4" - "@babel/plugin-transform-modules-commonjs" "^7.10.4" - "@babel/plugin-transform-modules-systemjs" "^7.10.4" - "@babel/plugin-transform-modules-umd" "^7.10.4" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.10.4" - "@babel/plugin-transform-new-target" "^7.10.4" - "@babel/plugin-transform-object-super" "^7.10.4" - "@babel/plugin-transform-parameters" "^7.10.4" - "@babel/plugin-transform-property-literals" "^7.10.4" - "@babel/plugin-transform-regenerator" "^7.10.4" - "@babel/plugin-transform-reserved-words" "^7.10.4" - "@babel/plugin-transform-shorthand-properties" "^7.10.4" - "@babel/plugin-transform-spread" "^7.11.0" - "@babel/plugin-transform-sticky-regex" "^7.10.4" - "@babel/plugin-transform-template-literals" "^7.10.4" - "@babel/plugin-transform-typeof-symbol" "^7.10.4" - "@babel/plugin-transform-unicode-escapes" "^7.10.4" - "@babel/plugin-transform-unicode-regex" "^7.10.4" + "@babel/plugin-syntax-top-level-await" "^7.12.1" + "@babel/plugin-transform-arrow-functions" "^7.12.1" + "@babel/plugin-transform-async-to-generator" "^7.12.1" + "@babel/plugin-transform-block-scoped-functions" "^7.12.1" + "@babel/plugin-transform-block-scoping" "^7.12.1" + "@babel/plugin-transform-classes" "^7.12.1" + "@babel/plugin-transform-computed-properties" "^7.12.1" + "@babel/plugin-transform-destructuring" "^7.12.1" + "@babel/plugin-transform-dotall-regex" "^7.12.1" + "@babel/plugin-transform-duplicate-keys" "^7.12.1" + "@babel/plugin-transform-exponentiation-operator" "^7.12.1" + "@babel/plugin-transform-for-of" "^7.12.1" + "@babel/plugin-transform-function-name" "^7.12.1" + "@babel/plugin-transform-literals" "^7.12.1" + "@babel/plugin-transform-member-expression-literals" "^7.12.1" + "@babel/plugin-transform-modules-amd" "^7.12.1" + "@babel/plugin-transform-modules-commonjs" "^7.12.1" + "@babel/plugin-transform-modules-systemjs" "^7.12.1" + "@babel/plugin-transform-modules-umd" "^7.12.1" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.12.1" + "@babel/plugin-transform-new-target" "^7.12.1" + "@babel/plugin-transform-object-super" "^7.12.1" + "@babel/plugin-transform-parameters" "^7.12.1" + "@babel/plugin-transform-property-literals" "^7.12.1" + "@babel/plugin-transform-regenerator" "^7.12.1" + "@babel/plugin-transform-reserved-words" "^7.12.1" + "@babel/plugin-transform-shorthand-properties" "^7.12.1" + "@babel/plugin-transform-spread" "^7.12.1" + "@babel/plugin-transform-sticky-regex" "^7.12.7" + "@babel/plugin-transform-template-literals" "^7.12.1" + "@babel/plugin-transform-typeof-symbol" "^7.12.1" + "@babel/plugin-transform-unicode-escapes" "^7.12.1" + "@babel/plugin-transform-unicode-regex" "^7.12.1" "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.11.5" - browserslist "^4.12.0" - core-js-compat "^3.6.2" - invariant "^2.2.2" - levenary "^1.1.1" + "@babel/types" "^7.12.7" + core-js-compat "^3.7.0" semver "^5.5.0" "@babel/preset-flow@^7.0.0": @@ -1163,18 +1444,18 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/preset-react@^7.0.0", "@babel/preset-react@^7.8.3", "@babel/preset-react@^7.9.4": - version "7.10.4" - resolved "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.10.4.tgz#92e8a66d816f9911d11d4cc935be67adfc82dbcf" - integrity sha512-BrHp4TgOIy4M19JAfO1LhycVXOPWdDbTRep7eVyatf174Hff+6Uk53sDyajqZPu8W1qXRBiYOfIamek6jA7YVw== +"@babel/preset-react@^7.0.0", "@babel/preset-react@^7.12.5", "@babel/preset-react@^7.8.3", "@babel/preset-react@^7.9.4": + version "7.12.7" + resolved "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.12.7.tgz#36d61d83223b07b6ac4ec55cf016abb0f70be83b" + integrity sha512-wKeTdnGUP5AEYCYQIMeXMMwU7j+2opxrG0WzuZfxuuW9nhKvvALBjl67653CWamZJVefuJGI219G591RSldrqQ== dependencies: "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-transform-react-display-name" "^7.10.4" - "@babel/plugin-transform-react-jsx" "^7.10.4" - "@babel/plugin-transform-react-jsx-development" "^7.10.4" - "@babel/plugin-transform-react-jsx-self" "^7.10.4" - "@babel/plugin-transform-react-jsx-source" "^7.10.4" - "@babel/plugin-transform-react-pure-annotations" "^7.10.4" + "@babel/plugin-transform-react-display-name" "^7.12.1" + "@babel/plugin-transform-react-jsx" "^7.12.7" + "@babel/plugin-transform-react-jsx-development" "^7.12.7" + "@babel/plugin-transform-react-jsx-self" "^7.12.1" + "@babel/plugin-transform-react-jsx-source" "^7.12.1" + "@babel/plugin-transform-react-pure-annotations" "^7.12.1" "@babel/preset-typescript@^7.9.0": version "7.10.4" @@ -1235,7 +1516,16 @@ "@babel/parser" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/template@^7.3.3", "@babel/template@^7.8.6": +"@babel/template@^7.12.7": + version "7.12.7" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc" + integrity sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.12.7" + "@babel/types" "^7.12.7" + +"@babel/template@^7.3.3": version "7.8.6" resolved "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg== @@ -1259,7 +1549,7 @@ globals "^11.1.0" lodash "^4.17.19" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.11.0": +"@babel/traverse@^7.0.0": version "7.11.0" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.0.tgz#9b996ce1b98f53f7c3e4175115605d56ed07dd24" integrity sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg== @@ -1274,7 +1564,7 @@ globals "^11.1.0" lodash "^4.17.19" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.9.6": +"@babel/traverse@^7.1.0": version "7.9.6" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.6.tgz#5540d7577697bf619cc57b92aa0f1c231a94f442" integrity sha512-b3rAHSjbxy6VEAvlxM8OV/0X4XrG72zoxme6q1MOoe2vd0bEc+TwayhuC1+Dfgqh1QEG+pj7atQqvUprHIccsg== @@ -1304,6 +1594,21 @@ globals "^11.1.0" lodash "^4.17.19" +"@babel/traverse@^7.12.1", "@babel/traverse@^7.12.5", "@babel/traverse@^7.12.9": + version "7.12.9" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.9.tgz#fad26c972eabbc11350e0b695978de6cc8e8596f" + integrity sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.12.5" + "@babel/helper-function-name" "^7.10.4" + "@babel/helper-split-export-declaration" "^7.11.0" + "@babel/parser" "^7.12.7" + "@babel/types" "^7.12.7" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + "@babel/types@7.11.5", "@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.8.6", "@babel/types@^7.9.5", "@babel/types@^7.9.6": version "7.11.5" resolved "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d" @@ -1322,6 +1627,15 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@babel/types@^7.12.1", "@babel/types@^7.12.5", "@babel/types@^7.12.6", "@babel/types@^7.12.7": + version "7.12.7" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz#6039ff1e242640a29452c9ae572162ec9a8f5d13" + integrity sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + "@backstage/catalog-model@^0.2.0": version "0.4.0" dependencies: @@ -4687,6 +5001,11 @@ resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.4.0.tgz#a2212b4d018e6075a058bb7e220a66959ef7a03c" integrity sha512-zLl4Fl3NvKxxjWNkqEcpdSOpQ3LGVH2BNFQ6vjaK6sFo2IrSznrhURIPI0HAphKiiIwNYjAfE0TNoQDSZv0U9A== +"@svgr/babel-plugin-transform-svg-component@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz#583a5e2a193e214da2f3afeb0b9e8d3250126b4a" + integrity sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ== + "@svgr/babel-preset@^5.4.0": version "5.4.0" resolved "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.4.0.tgz#da21854643e1c4ad2279239baa7d5a8b128c1f15" @@ -4701,14 +5020,28 @@ "@svgr/babel-plugin-transform-react-native-svg" "^5.4.0" "@svgr/babel-plugin-transform-svg-component" "^5.4.0" -"@svgr/core@^5.4.0": - version "5.4.0" - resolved "https://registry.npmjs.org/@svgr/core/-/core-5.4.0.tgz#655378ee43679eb94fee3d4e1976e38252dff8e7" - integrity sha512-hWGm1DCCvd4IEn7VgDUHYiC597lUYhFau2lwJBYpQWDirYLkX4OsXu9IslPgJ9UpP7wsw3n2Ffv9sW7SXJVfqQ== +"@svgr/babel-preset@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz#8af54f3e0a8add7b1e2b0fcd5a882c55393df327" + integrity sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig== dependencies: - "@svgr/plugin-jsx" "^5.4.0" - camelcase "^6.0.0" - cosmiconfig "^6.0.0" + "@svgr/babel-plugin-add-jsx-attribute" "^5.4.0" + "@svgr/babel-plugin-remove-jsx-attribute" "^5.4.0" + "@svgr/babel-plugin-remove-jsx-empty-expression" "^5.0.1" + "@svgr/babel-plugin-replace-jsx-attribute-value" "^5.0.1" + "@svgr/babel-plugin-svg-dynamic-title" "^5.4.0" + "@svgr/babel-plugin-svg-em-dimensions" "^5.4.0" + "@svgr/babel-plugin-transform-react-native-svg" "^5.4.0" + "@svgr/babel-plugin-transform-svg-component" "^5.5.0" + +"@svgr/core@^5.4.0", "@svgr/core@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz#82e826b8715d71083120fe8f2492ec7d7874a579" + integrity sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ== + dependencies: + "@svgr/plugin-jsx" "^5.5.0" + camelcase "^6.2.0" + cosmiconfig "^7.0.0" "@svgr/hast-util-to-babel-ast@^5.4.0": version "5.4.0" @@ -4717,7 +5050,14 @@ dependencies: "@babel/types" "^7.9.5" -"@svgr/plugin-jsx@5.4.x", "@svgr/plugin-jsx@^5.4.0": +"@svgr/hast-util-to-babel-ast@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz#5ee52a9c2533f73e63f8f22b779f93cd432a5461" + integrity sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ== + dependencies: + "@babel/types" "^7.12.6" + +"@svgr/plugin-jsx@5.4.x": version "5.4.0" resolved "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.4.0.tgz#ab47504c55615833c6db70fca2d7e489f509787c" integrity sha512-SGzO4JZQ2HvGRKDzRga9YFSqOqaNrgLlQVaGvpZ2Iht2gwRp/tq+18Pvv9kS9ZqOMYgyix2LLxZMY1LOe9NPqw== @@ -4727,7 +5067,17 @@ "@svgr/hast-util-to-babel-ast" "^5.4.0" svg-parser "^2.0.2" -"@svgr/plugin-svgo@5.4.x", "@svgr/plugin-svgo@^5.4.0": +"@svgr/plugin-jsx@^5.4.0", "@svgr/plugin-jsx@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz#1aa8cd798a1db7173ac043466d7b52236b369000" + integrity sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA== + dependencies: + "@babel/core" "^7.12.3" + "@svgr/babel-preset" "^5.5.0" + "@svgr/hast-util-to-babel-ast" "^5.5.0" + svg-parser "^2.0.2" + +"@svgr/plugin-svgo@5.4.x": version "5.4.0" resolved "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.4.0.tgz#45d9800b7099a6f7b4d85ebac89ab9abe8592f64" integrity sha512-3Cgv3aYi1l6SHyzArV9C36yo4kgwVdF3zPQUC6/aCDUeXAofDYwE5kk3e3oT5ZO2a0N3lB+lLGvipBG6lnG8EA== @@ -4736,18 +5086,27 @@ merge-deep "^3.0.2" svgo "^1.2.2" -"@svgr/rollup@5.4.x": - version "5.4.0" - resolved "https://registry.npmjs.org/@svgr/rollup/-/rollup-5.4.0.tgz#b1946c8d2c13870397af014a105d40945b7371e5" - integrity sha512-hwYjrTddW6mFU9vwqRr1TULNvxiIxGdIbqrD5J7vtoATSfWazq/2JSnT4BmiH+/4kFXLEtjKuSKoDUotkOIAkg== +"@svgr/plugin-svgo@^5.4.0", "@svgr/plugin-svgo@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz#02da55d85320549324e201c7b2e53bf431fcc246" + integrity sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ== dependencies: - "@babel/core" "^7.7.5" - "@babel/plugin-transform-react-constant-elements" "^7.7.4" - "@babel/preset-env" "^7.9.5" - "@babel/preset-react" "^7.9.4" - "@svgr/core" "^5.4.0" - "@svgr/plugin-jsx" "^5.4.0" - "@svgr/plugin-svgo" "^5.4.0" + cosmiconfig "^7.0.0" + deepmerge "^4.2.2" + svgo "^1.2.2" + +"@svgr/rollup@5.5.x": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/rollup/-/rollup-5.5.0.tgz#9ceaaa6d463916e69aff8a9e10b3bb8fbb94688a" + integrity sha512-EiZmH2VTr+Xzyb6Ga8XtGa9MEbiU3WQnB5vHmqhwAUqibU3uwuwr7MN+QwIh/gtBk1ucMim8BCfcRTlLVREM8A== + dependencies: + "@babel/core" "^7.12.3" + "@babel/plugin-transform-react-constant-elements" "^7.12.1" + "@babel/preset-env" "^7.12.1" + "@babel/preset-react" "^7.12.5" + "@svgr/core" "^5.5.0" + "@svgr/plugin-jsx" "^5.5.0" + "@svgr/plugin-svgo" "^5.5.0" rollup-pluginutils "^2.8.2" "@svgr/webpack@5.4.x", "@svgr/webpack@^5.4.0": @@ -8129,7 +8488,7 @@ browserslist@4.10.0: node-releases "^1.1.52" pkg-up "^3.1.0" -browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.8.3: +browserslist@^4.0.0, browserslist@^4.12.0: version "4.13.0" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.13.0.tgz#42556cba011e1b0a2775b611cba6a8eca18e940d" integrity sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ== @@ -8139,6 +8498,17 @@ browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.8.3: escalade "^3.0.1" node-releases "^1.1.58" +browserslist@^4.14.5, browserslist@^4.15.0: + version "4.15.0" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.15.0.tgz#3d48bbca6a3f378e86102ffd017d9a03f122bdb0" + integrity sha512-IJ1iysdMkGmjjYeRlDU8PQejVwxvVO5QOfXH7ylW31GO6LwNRSmm/SgRXtNsEXqMLl2e+2H5eEJ7sfynF8TCaQ== + dependencies: + caniuse-lite "^1.0.30001164" + colorette "^1.2.1" + electron-to-chromium "^1.3.612" + escalade "^3.1.1" + node-releases "^1.1.67" + bs-logger@0.x: version "0.2.6" resolved "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" @@ -8507,6 +8877,11 @@ camelcase@^6.0.0: resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e" integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w== +camelcase@^6.2.0: + version "6.2.0" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" + integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== + caniuse-api@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" @@ -8522,6 +8897,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001093, can resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001113.tgz#22016ab55b5a8b04fa00ca342d9ee1b98df48065" integrity sha512-qMvjHiKH21zzM/VDZr6oosO6Ri3U0V2tC015jRXjOecwQCJtsU5zklTNTk31jQbIOP8gha0h1ccM/g0ECP+4BA== +caniuse-lite@^1.0.30001164: + version "1.0.30001165" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001165.tgz#32955490d2f60290bb186bb754f2981917fa744f" + integrity sha512-8cEsSMwXfx7lWSUMA2s08z9dIgsnR5NAqjXP23stdsU3AUWkCr/rr4s4OFtHXn5XXr6+7kam3QFVoYyXNPdJPA== + canvas@^2.6.1: version "2.6.1" resolved "https://registry.npmjs.org/canvas/-/canvas-2.6.1.tgz#0d087dd4d60f5a5a9efa202757270abea8bef89e" @@ -9423,12 +9803,12 @@ copy-to-clipboard@^3, copy-to-clipboard@^3.0.8, copy-to-clipboard@^3.1.0, copy-t dependencies: toggle-selection "^1.0.6" -core-js-compat@^3.6.2: - version "3.6.4" - resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.4.tgz#938476569ebb6cda80d339bcf199fae4f16fff17" - integrity sha512-zAa3IZPvsJ0slViBQ2z+vgyyTuhd3MFn1rBQjZSKVEgB0UMYhUkCj9jJUVPgGTGqWvsBVmfnruXgTcNyTlEiSA== +core-js-compat@^3.7.0: + version "3.8.1" + resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.1.tgz#8d1ddd341d660ba6194cbe0ce60f4c794c87a36e" + integrity sha512-a16TLmy9NVD1rkjUGbwuyWkiDoN0FDpAwrfLONvHFQx0D9k7J9y0srwMT8QP/Z6HE3MIFaVynEeYwZwPX1o5RQ== dependencies: - browserslist "^4.8.3" + browserslist "^4.15.0" semver "7.0.0" core-js-pure@^3.0.0, core-js-pure@^3.0.1: @@ -10896,6 +11276,11 @@ electron-to-chromium@^1.3.378, electron-to-chromium@^1.3.488: resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.509.tgz#830fcb89cd66dc2984d18d794973b99e3f00584c" integrity sha512-cN4lkjNRuTG8rtAqTOVgwpecEC2kbKA04PG6YijcKGHK/kD0xLjiqExcAOmLUwtXZRF8cBeam2I0VZcih919Ug== +electron-to-chromium@^1.3.612: + version "1.3.620" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.620.tgz#c6f36a7e398acc9d7d12743a6f58d536fbc58700" + integrity sha512-YbgWXUR2Mu+Fp6rm3GZ5YJdNo8SgZKLUTNSl2PNvdOcM8OIz07jRJnRkIaV9vdszFv9UUuGChh19w9qSuoLJgw== + elegant-spinner@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" @@ -14033,7 +14418,7 @@ interpret@^2.0.0, interpret@^2.2.0: resolved "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== -invariant@^2.0.0, invariant@^2.2.2, invariant@^2.2.3, invariant@^2.2.4: +invariant@^2.0.0, invariant@^2.2.3, invariant@^2.2.4: version "2.2.4" resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -15781,13 +16166,6 @@ leven@^3.1.0: resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== -levenary@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/levenary/-/levenary-1.1.1.tgz#842a9ee98d2075aa7faeedbe32679e9205f46f77" - integrity sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ== - dependencies: - leven "^3.1.0" - levn@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -17513,6 +17891,11 @@ node-releases@^1.1.52, node-releases@^1.1.58: resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084" integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA== +node-releases@^1.1.67: + version "1.1.67" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12" + integrity sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg== + node-request-interceptor@^0.3.5: version "0.3.5" resolved "https://registry.npmjs.org/node-request-interceptor/-/node-request-interceptor-0.3.5.tgz#4b26159617829c9a70643012c0fdc3ae4c78ae43" @@ -20832,6 +21215,18 @@ regexpu-core@^4.6.0, regexpu-core@^4.7.0: unicode-match-property-ecmascript "^1.0.4" unicode-match-property-value-ecmascript "^1.2.0" +regexpu-core@^4.7.1: + version "4.7.1" + resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" + integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== + dependencies: + regenerate "^1.4.0" + regenerate-unicode-properties "^8.2.0" + regjsgen "^0.5.1" + regjsparser "^0.6.4" + unicode-match-property-ecmascript "^1.0.4" + unicode-match-property-value-ecmascript "^1.2.0" + registry-auth-token@^4.0.0: version "4.1.1" resolved "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.1.1.tgz#40a33be1e82539460f94328b0f7f0f84c16d9479" From 1c69d4716ce228689145868351d93aa477c3f494 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Wed, 9 Dec 2020 01:47:07 -0500 Subject: [PATCH 85/86] fix(core): React descendant P tag warning (#3641) * Fix descendant P tag warning * Add changeset * Remove whitespace on end of example --- .changeset/rare-cooks-tan.md | 5 +++++ .../EmptyState/MissingAnnotationEmptyState.tsx | 12 +++++------- 2 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 .changeset/rare-cooks-tan.md diff --git a/.changeset/rare-cooks-tan.md b/.changeset/rare-cooks-tan.md new file mode 100644 index 0000000000..790ab3503e --- /dev/null +++ b/.changeset/rare-cooks-tan.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Fix React warning of descendant paragraph tag diff --git a/packages/core/src/components/EmptyState/MissingAnnotationEmptyState.tsx b/packages/core/src/components/EmptyState/MissingAnnotationEmptyState.tsx index 1c27ba27d3..377373a06d 100644 --- a/packages/core/src/components/EmptyState/MissingAnnotationEmptyState.tsx +++ b/packages/core/src/components/EmptyState/MissingAnnotationEmptyState.tsx @@ -20,8 +20,7 @@ import { BackstageTheme } from '@backstage/theme'; import { EmptyState } from './EmptyState'; import { CodeSnippet } from '../CodeSnippet'; -const COMPONENT_YAML = `# Example -apiVersion: backstage.io/v1alpha1 +const COMPONENT_YAML = `apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: example @@ -31,8 +30,7 @@ metadata: spec: type: website lifecycle: production - owner: guest -`; + owner: guest`; type Props = { annotation: string; @@ -49,10 +47,10 @@ const useStyles = makeStyles(theme => ({ export const MissingAnnotationEmptyState = ({ annotation }: Props) => { const classes = useStyles(); const description = ( - + <> The {annotation} annotation is missing. You need to add the annotation to your component if you want to enable this tool. - + ); return ( { text={COMPONENT_YAML.replace('ANNOTATION', annotation)} language="yaml" showLineNumbers - highlightedNumbers={[7, 8]} + highlightedNumbers={[6, 7]} customStyle={{ background: 'inherit', fontSize: '115%' }} /> From cbd3a44c09af69643b985d6cc3c98b2d3f364d08 Mon Sep 17 00:00:00 2001 From: Frieder Bluemle Date: Tue, 8 Dec 2020 22:50:41 -0800 Subject: [PATCH 86/86] Fix typos (#3646) --- .github/styles/vocab.txt | 4 ++-- .github/workflows/nightly.yml | 2 +- .github/workflows/techdocs-project-board.yml | 2 +- docs/FAQ.md | 2 +- .../adr005-catalog-core-entities.md | 2 +- docs/architecture-decisions/adr006-avoid-react-fc.md | 2 +- docs/features/software-catalog/descriptor-format.md | 4 ++-- docs/features/techdocs/architecture.md | 4 ++-- docs/getting-started/configure-app-with-plugins.md | 2 +- docs/getting-started/create-an-app.md | 2 +- docs/getting-started/index.md | 2 +- docs/plugins/publishing.md | 8 ++++---- docs/plugins/testing.md | 4 ++-- docs/support/project-structure.md | 2 +- docs/tutorials/journey.md | 2 +- mkdocs.yml | 2 +- packages/cli/README.md | 2 +- packages/cli/src/commands/index.ts | 6 +++--- packages/cli/src/lib/packager/index.ts | 2 +- packages/cli/src/lib/versioning/Lockfile.ts | 2 +- packages/core-api/CHANGELOG.md | 2 +- .../src/apis/implementations/auth/github/GithubAuth.ts | 2 +- .../src/apis/implementations/auth/gitlab/GitlabAuth.ts | 2 +- packages/core/CHANGELOG.md | 4 ++-- packages/core/README.md | 2 +- packages/create-app/CHANGELOG.md | 2 +- packages/dev-utils/README.md | 2 +- packages/docgen/src/docgen/GitHubMarkdownPrinter.ts | 2 +- packages/docgen/src/docgen/types.ts | 2 +- packages/integration/src/gitlab/core.ts | 2 +- packages/test-utils/README.md | 2 +- packages/theme/README.md | 2 +- plugins/auth-backend/README.md | 8 ++++---- .../src/components/Cards/RecentWorkflowRunsCard.tsx | 2 +- .../components/WorkflowRunsTable/WorkflowRunsTable.tsx | 2 +- plugins/scaffolder-backend/CHANGELOG.md | 2 +- .../sample-templates/create-react-app/template.yaml | 4 ++-- plugins/scaffolder-backend/src/service/router.test.ts | 4 ++-- plugins/scaffolder/CHANGELOG.md | 2 +- plugins/techdocs-backend/CHANGELOG.md | 4 ++-- .../components/AuthProviders/DefaultProviderSettings.tsx | 4 ++-- 41 files changed, 58 insertions(+), 58 deletions(-) diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 35251ef502..b595c4ba2b 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -67,9 +67,9 @@ Firekube freben Fredrik github -Github +GitHub gitlab -Gitlab +GitLab Grafana graphql graphviz diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 3f59b815b3..0dc40d2c40 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -62,7 +62,7 @@ jobs: - name: prepare nightly release run: yarn changeset version --snapshot nightly - # Publishes the nightly release to NPM, by using tag we make sure the release is + # Publishes the nightly release to npm, by using tag we make sure the release is # not flagged as the latest release, which means that people will not get this # version of the package unless requested explicitly - name: publish nightly release diff --git a/.github/workflows/techdocs-project-board.yml b/.github/workflows/techdocs-project-board.yml index a8d476f713..679abe6536 100644 --- a/.github/workflows/techdocs-project-board.yml +++ b/.github/workflows/techdocs-project-board.yml @@ -1,7 +1,7 @@ name: Automatically add new TechDocs Issues and PRs to the GitHub project board # Development of TechDocs in Backstage is managed by this Kanban board - https://github.com/orgs/backstage/projects/1 # New issues and PRs with TechDocs in their title or docs-like-code label will be added to the board. -# Caveat: New PRs created from forks will not be added since GitHub actions don't share credentials with forks. +# Caveat: New PRs created from forks will not be added since GitHub Actions don't share credentials with forks. on: issues: diff --git a/docs/FAQ.md b/docs/FAQ.md index 90f642e591..d8be258141 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -124,7 +124,7 @@ service. The `@backstage/create-app` tool that is used to create your own Backstage app is similar to [`create-react-app`](https://github.com/facebook/create-react-app) in that it gives you a starting point. The code you get is meant to be evolved, and most of -the functionality you get out of the box is brought in via NPM dependencies. +the functionality you get out of the box is brought in via npm dependencies. Keeping your app up to date generally means keeping your dependencies up to date. The Backstage CLI provides a command to help you with that. Simply run `yarn backstage-cli versions:bump` at the root of your repo, and the latest diff --git a/docs/architecture-decisions/adr005-catalog-core-entities.md b/docs/architecture-decisions/adr005-catalog-core-entities.md index f91698c5ff..34d6449c6b 100644 --- a/docs/architecture-decisions/adr005-catalog-core-entities.md +++ b/docs/architecture-decisions/adr005-catalog-core-entities.md @@ -58,7 +58,7 @@ discover existing functionality in the ecosystem. APIs are implemented by components and make their boundaries explicit. They might be defined using an RPC IDL (e.g. in Protobuf, GraphQL or similar), a data schema (e.g. in Avro, TFRecord or similar), or as code interfaces (e.g. -framework APIs in Swift, Kotlin, Java, C++, Typescript etc). In any case, APIs +framework APIs in Swift, Kotlin, Java, C++, TypeScript etc). In any case, APIs exposed by components need to be in a known machine-readable format so we can build further tooling and analysis on top. diff --git a/docs/architecture-decisions/adr006-avoid-react-fc.md b/docs/architecture-decisions/adr006-avoid-react-fc.md index 96f594daf7..51dcf042da 100644 --- a/docs/architecture-decisions/adr006-avoid-react-fc.md +++ b/docs/architecture-decisions/adr006-avoid-react-fc.md @@ -6,7 +6,7 @@ description: Architecture Decision Record (ADR) log on Avoid React.FC and React. ## Context -Facebook has removed `React.FC` from their base template for a Typescript +Facebook has removed `React.FC` from their base template for a TypeScript project. The reason for this was that it was found to be an unnecessary feature with next to no benefits in combination with a few downsides. diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index eb7a6fdee0..72e6388273 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -407,7 +407,7 @@ The current set of well-known and common values for this field is: - `service` - a backend service, typically exposing an API - `website` - a website -- `library` - a software library, such as an NPM module or a Java library +- `library` - a software library, such as an npm module or a Java library ### `spec.lifecycle` [required] @@ -558,7 +558,7 @@ The current set of well-known and common values for this field is: - `service` - a backend service, typically exposing an API - `website` - a website -- `library` - a software library, such as an NPM module or a Java library +- `library` - a software library, such as an npm module or a Java library ### `spec.templater` [required] diff --git a/docs/features/techdocs/architecture.md b/docs/features/techdocs/architecture.md index f502b3836e..9305239b7f 100644 --- a/docs/features/techdocs/architecture.md +++ b/docs/features/techdocs/architecture.md @@ -21,7 +21,7 @@ looking at. In response, it receives the static files (HTML, CSS, JSON, etc.) to render on the page in TechDocs/Backstage. The static files consist of HTML, CSS and Images generated by MkDocs. We remove -all the Javascript before adding them to Backstage for security reasons. And +all the JavaScript before adding them to Backstage for security reasons. And there are some additional techdocs metadata JSON files that TechDocs needs to render a site. @@ -59,7 +59,7 @@ Similar to how it is done in the Basic setup, the TechDocs Reader requests your configured storage solution for the necessary files and returns them to TechDocs Reader. -We will provide instructions, scripts and/or templates (e.g. GitHub actions) to +We will provide instructions, scripts and/or templates (e.g. GitHub Actions) to build docs in your CI/CD system. [Track progress here.](https://github.com/backstage/backstage/issues/3400) You will be able to use `techdocs-cli` to build docs and publish the generated docs diff --git a/docs/getting-started/configure-app-with-plugins.md b/docs/getting-started/configure-app-with-plugins.md index 042d8964ca..64b9e772d1 100644 --- a/docs/getting-started/configure-app-with-plugins.md +++ b/docs/getting-started/configure-app-with-plugins.md @@ -11,7 +11,7 @@ add an existing plugin to it. We are using the [CircleCI](https://github.com/backstage/backstage/blob/master/plugins/circleci/README.md) plugin in this example. -1. Add the plugin's NPM package to the repo: +1. Add the plugin's npm package to the repo: ```bash yarn add @backstage/plugin-circleci diff --git a/docs/getting-started/create-an-app.md b/docs/getting-started/create-an-app.md index 6aef4c48c1..52ea90db5b 100644 --- a/docs/getting-started/create-an-app.md +++ b/docs/getting-started/create-an-app.md @@ -87,7 +87,7 @@ You may encounter the following error message: Couldn't find any versions for "file-saver" that matches "eligrey-FileSaver.js-1.3.8.tar.gz-art-external" ``` -This is likely because you have a globally configured NPM proxy, which breaks +This is likely because you have a globally configured npm proxy, which breaks the installation of the `material-table` dependency. This is a known issue and being worked on in `material-table`, but for now you can work around it using the following: diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 7fc80b625c..16c837d676 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -10,7 +10,7 @@ you're planning to do. Creating a standalone instance makes it simpler to customize the application for your needs whilst staying up to date with the project. You will also depend on -`@backstage` packages from NPM, making the project much smaller. This is the +`@backstage` packages from npm, making the project much smaller. This is the recommended approach if you want to kick the tyres of Backstage or setup your own instance. diff --git a/docs/plugins/publishing.md b/docs/plugins/publishing.md index 7eab61891e..efea02ee23 100644 --- a/docs/plugins/publishing.md +++ b/docs/plugins/publishing.md @@ -1,16 +1,16 @@ --- id: publishing title: Publishing -description: Documentation on Publishing NPM packages +description: Documentation on Publishing npm packages --- -## NPM +## npm -NPM packages are published through CI/CD in the +npm packages are published through CI/CD in the [.github/workflows/master.yml](https://github.com/backstage/backstage/blob/master/.github/workflows/master.yml) workflow. Every commit that is merged to master will be checked for new versions of all public packages, and any new versions will automatically be published to -NPM. +npm. ### Creating a new release diff --git a/docs/plugins/testing.md b/docs/plugins/testing.md index e60b5fa8b7..b564fd732c 100644 --- a/docs/plugins/testing.md +++ b/docs/plugins/testing.md @@ -234,7 +234,7 @@ This is especially true for edge cases! ## Non-React Classes -Testing a Javascript object which is _not_ a React component follows a lot of +Testing a JavaScript object which is _not_ a React component follows a lot of the same principles as testing objects in other languages. ### API Testing Principles @@ -243,7 +243,7 @@ Testing an API involves verifying four things: 1. Invalid inputs are caught before being sent to the server. 2. Valid inputs translate into a valid browser request. -3. Server response is translated into an expected Javascript object. +3. Server response is translated into an expected JavaScript object. 4. Server errors are handled gracefully. ### Mocking API Calls diff --git a/docs/support/project-structure.md b/docs/support/project-structure.md index c25e087e57..190259841c 100644 --- a/docs/support/project-structure.md +++ b/docs/support/project-structure.md @@ -161,7 +161,7 @@ are separated out into their own folder, see further down. - [`docgen/`](https://github.com/backstage/backstage/tree/master/packages/docgen) - Uses the - [Typescript Compiler API](https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API) + [TypeScript Compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API) to read out definitions and generate documentation for it. - [`e2e-test/`](https://github.com/backstage/backstage/tree/master/packages/e2e-test) - diff --git a/docs/tutorials/journey.md b/docs/tutorials/journey.md index 664d4b77d3..adefdfa73f 100644 --- a/docs/tutorials/journey.md +++ b/docs/tutorials/journey.md @@ -22,7 +22,7 @@ music and wants to have a theme tune for every service in Backstage. Sam built a Spotify plugin for Backstage that allows service owners to define a theme tune for their service. The theme tune plays whenever a user visits the -service page in Backstage. The plugin is published to NPM and available for any +service page in Backstage. The plugin is published to npm and available for any organization to easily install and add to their Backstage installation. # 1. A New Plugin diff --git a/mkdocs.yml b/mkdocs.yml index 1086259710..72508c821e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -69,7 +69,7 @@ nav: - Testing: - Overview: 'plugins/testing.md' - Publishing: - - Open source and NPM: 'plugins/publishing.md' + - Open source and npm: 'plugins/publishing.md' - Private/internal (non-open source): 'plugins/publish-private.md' - Configuration: - Overview: 'conf/index.md' diff --git a/packages/cli/README.md b/packages/cli/README.md index 9eac5cdfe4..cd6ad8094e 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -4,7 +4,7 @@ This package provides a CLI for developing Backstage plugins and apps. ## Installation -Install the package via npm or yarn: +Install the package via npm or Yarn: ```sh $ npm install --save @backstage/cli diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index f773f279fc..e090c454f9 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -87,9 +87,9 @@ export function registerCommands(program: CommanderStatic) { 'Create plugin with the backend dependencies as default', ) .description('Creates a new plugin in the current repository') - .option('--scope ', 'NPM scope') - .option('--npm-registry ', 'NPM registry URL') - .option('--no-private', 'Public NPM Package') + .option('--scope ', 'npm scope') + .option('--npm-registry ', 'npm registry URL') + .option('--no-private', 'Public npm package') .action( lazy(() => import('./create-plugin/createPlugin').then(m => m.default)), ); diff --git a/packages/cli/src/lib/packager/index.ts b/packages/cli/src/lib/packager/index.ts index e9388ad9bc..3976998998 100644 --- a/packages/cli/src/lib/packager/index.ts +++ b/packages/cli/src/lib/packager/index.ts @@ -90,7 +90,7 @@ type Options = { * will be suitable for packaging e.g. into a docker image. * * This creates a structure that is functionally similar to if the packages where - * installed from NPM, but uses yarn workspaces to link to them at runtime. + * installed from npm, but uses Yarn workspaces to link to them at runtime. */ export async function createDistWorkspace( packageNames: string[], diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts index fd7c189b40..f567786b7c 100644 --- a/packages/cli/src/lib/versioning/Lockfile.ts +++ b/packages/cli/src/lib/versioning/Lockfile.ts @@ -37,7 +37,7 @@ type LockfileQueryEntry = { version: string; }; -/** Entries that have an invalid version range, for example an NPM tag */ +/** Entries that have an invalid version range, for example an npm tag */ type AnalyzeResultInvalidRange = { name: string; range: string; diff --git a/packages/core-api/CHANGELOG.md b/packages/core-api/CHANGELOG.md index ab333b998e..23f2ba1545 100644 --- a/packages/core-api/CHANGELOG.md +++ b/packages/core-api/CHANGELOG.md @@ -62,7 +62,7 @@ ![](https://user-images.githubusercontent.com/872486/93851658-1a76f200-fce3-11ea-990b-26ca1a327a15.png) -- b79017fd3: Updated the `GithubAuth.create` method to configure the default scope of the Github Auth Api. As a result the +- b79017fd3: Updated the `GithubAuth.create` method to configure the default scope of the GitHub Auth Api. As a result the default scope is configurable when overwriting the Core Api in the app. ``` diff --git a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts index 532bff3480..812032a8a3 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -45,7 +45,7 @@ export type GithubAuthResponse = { const DEFAULT_PROVIDER = { id: 'github', - title: 'Github', + title: 'GitHub', icon: GithubIcon, }; diff --git a/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts b/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts index 8669dff022..3f4bc814e3 100644 --- a/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts @@ -21,7 +21,7 @@ import { OAuthApiCreateOptions } from '../types'; const DEFAULT_PROVIDER = { id: 'gitlab', - title: 'Gitlab', + title: 'GitLab', icon: GitlabIcon, }; diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index d29bc53452..cb403685e1 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -21,7 +21,7 @@ ### Patch Changes - 7b37d65fd: Adds the MarkdownContent component to render and display Markdown content with the default - [GFM](https://github.github.com/gfm/) (Github flavored Markdown) dialect. + [GFM](https://github.github.com/gfm/) (GitHub Flavored Markdown) dialect. ``` @@ -59,7 +59,7 @@ - 482b6313d: Fix dense in Structured Metadata Table - 1c60f716e: Added EmptyState component -- b79017fd3: Updated the `GithubAuth.create` method to configure the default scope of the Github Auth Api. As a result the +- b79017fd3: Updated the `GithubAuth.create` method to configure the default scope of the GitHub Auth Api. As a result the default scope is configurable when overwriting the Core Api in the app. ``` diff --git a/packages/core/README.md b/packages/core/README.md index 0d0063c9fd..6d8519d46f 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -4,7 +4,7 @@ This package provides the core API used by Backstage plugins and apps. ## Installation -Install the package via npm or yarn: +Install the package via npm or Yarn: ```sh $ npm install --save @backstage/core diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index e13e31f406..66050cd50a 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -212,7 +212,7 @@ --config ../../app-config.yaml --config ../../app-config.development.yaml ``` -- 5a920c6e4: Updated naming of environment variables. New pattern [NAME]\_TOKEN for Github, Gitlab, Azure & Github enterprise access tokens. +- 5a920c6e4: Updated naming of environment variables. New pattern [NAME]\_TOKEN for GitHub, GitLab, Azure & GitHub Enterprise access tokens. ### Detail: diff --git a/packages/dev-utils/README.md b/packages/dev-utils/README.md index 1aa86ba51c..4b08a5ca6d 100644 --- a/packages/dev-utils/README.md +++ b/packages/dev-utils/README.md @@ -6,7 +6,7 @@ This package provides utilities that help in developing plugins for Backstage, l ## Installation -Install the package via npm or yarn: +Install the package via npm or Yarn: ```sh $ npm install --save-dev @backstage/dev-utils diff --git a/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts b/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts index 08e493600d..d7955e5566 100644 --- a/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts +++ b/packages/docgen/src/docgen/GitHubMarkdownPrinter.ts @@ -27,7 +27,7 @@ const COMMIT_SHA = execSync('git rev-parse HEAD').toString('utf8').trim(); /** - * The GithubMarkdownPrinter is a MarkdownPrinter for printing Github-flavored markdown documents. + * The GithubMarkdownPrinter is a MarkdownPrinter for printing GitHub Flavored Markdown documents. */ export default class GithubMarkdownPrinter implements MarkdownPrinter { private str: string = ''; diff --git a/packages/docgen/src/docgen/types.ts b/packages/docgen/src/docgen/types.ts index 7aff54d04c..1d1d93a6ae 100644 --- a/packages/docgen/src/docgen/types.ts +++ b/packages/docgen/src/docgen/types.ts @@ -31,7 +31,7 @@ export type TypeLink = { }; /** - * TypeInfo describes a Typescript Type. + * TypeInfo describes a TypeScript Type. */ export type TypeInfo = { id: number; diff --git a/packages/integration/src/gitlab/core.ts b/packages/integration/src/gitlab/core.ts index 63dc3fdb3b..29dcc60bac 100644 --- a/packages/integration/src/gitlab/core.ts +++ b/packages/integration/src/gitlab/core.ts @@ -130,7 +130,7 @@ export async function getProjectId( const url = new URL(target); if (!url.pathname.includes('/-/blob/')) { - throw new Error('Please provide full path to yaml file from Gitlab'); + throw new Error('Please provide full path to yaml file from GitLab'); } try { diff --git a/packages/test-utils/README.md b/packages/test-utils/README.md index 38bf1e83cf..0f95d4ab26 100644 --- a/packages/test-utils/README.md +++ b/packages/test-utils/README.md @@ -4,7 +4,7 @@ This package provides utilities that can be used to test plugins and apps for Ba ## Installation -Install the package via npm or yarn: +Install the package via npm or Yarn: ```sh $ npm install --save-dev @backstage/test-utils diff --git a/packages/theme/README.md b/packages/theme/README.md index 4b29738193..9855d6730d 100644 --- a/packages/theme/README.md +++ b/packages/theme/README.md @@ -4,7 +4,7 @@ This package provides the extended Material UI Theme(s) that power Backstage. ## Installation -Install the package via npm or yarn: +Install the package via npm or Yarn: ```sh $ npm install --save @backstage/theme diff --git a/plugins/auth-backend/README.md b/plugins/auth-backend/README.md index e69bb5b86b..ee6e84cded 100644 --- a/plugins/auth-backend/README.md +++ b/plugins/auth-backend/README.md @@ -25,7 +25,7 @@ export AUTH_GOOGLE_CLIENT_ID=x export AUTH_GOOGLE_CLIENT_SECRET=x ``` -### Github +### GitHub #### Creating a GitHub OAuth application @@ -42,7 +42,7 @@ export AUTH_GITHUB_CLIENT_ID=x export AUTH_GITHUB_CLIENT_SECRET=x ``` -for github enterprise: +For GitHub Enterprise: ```bash export AUTH_GITHUB_CLIENT_ID=x @@ -50,7 +50,7 @@ export AUTH_GITHUB_CLIENT_SECRET=x export AUTH_GITHUB_ENTERPRISE_INSTANCE_URL=https://x ``` -### Gitlab +### GitLab #### Creating a GitLab OAuth application @@ -70,7 +70,7 @@ Follow this link, [Add new application](https://gitlab.com/-/profile/application ```bash export GITLAB_BASE_URL=https://gitlab.com -export AUTH_GITLAB_CLIENT_ID=x # Gitlab calls this the Application ID +export AUTH_GITLAB_CLIENT_ID=x # GitLab calls this the Application ID export AUTH_GITLAB_CLIENT_SECRET=x ``` diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx index 46f60981a0..70348ece0a 100644 --- a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx @@ -60,7 +60,7 @@ export const RecentWorkflowRunsCard = ({ { }, use_typescript: { default: true, - description: 'Include typescript', - title: 'Use Typescript', + description: 'Include TypeScript', + title: 'Use TypeScript', type: 'boolean', }, }, diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 197d54a9a4..b10af626e5 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -99,7 +99,7 @@ ![failed-to-create-component](https://user-images.githubusercontent.com/33940798/94339296-90969400-0016-11eb-9a74-ce16b3dd8d88.gif) - c5ef12926: fix the accordion details design when job stage fail -- 1c8c43756: The new `scaffolder.github.baseUrl` config property allows to specify a custom base url for GitHub enterprise instances +- 1c8c43756: The new `scaffolder.github.baseUrl` config property allows to specify a custom base url for GitHub Enterprise instances - Updated dependencies [28edd7d29] - Updated dependencies [819a70229] - Updated dependencies [3a4236570] diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 6557dfe924..77f8ba7a98 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -4,7 +4,7 @@ ### Patch Changes -- ae95c7ff3: Update URL auth format for Gitlab clone +- ae95c7ff3: Update URL auth format for GitLab clone - Updated dependencies [612368274] - Updated dependencies [08835a61d] - Updated dependencies [a9fd599f7] @@ -67,7 +67,7 @@ Draft until we're happy with the implementation, then I can add more docs and changelog entry. Also didn't go on a thorough hunt for places where discovery can be used, but I don't think there are many since it's been pretty awkward to do service-to-service communication. -- 5a920c6e4: Updated naming of environment variables. New pattern [NAME]\_TOKEN for Github, Gitlab, Azure & Github enterprise access tokens. +- 5a920c6e4: Updated naming of environment variables. New pattern [NAME]\_TOKEN for GitHub, GitLab, Azure & GitHub Enterprise access tokens. ### Detail: diff --git a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx index a1dab1e7d4..415d1eaad6 100644 --- a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx +++ b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx @@ -49,7 +49,7 @@ export const DefaultProviderSettings = ({ configuredProviders }: Props) => ( )} {configuredProviders.includes('github') && ( ( )} {configuredProviders.includes('gitlab') && (