From 53eb5e61e31ed470d5c8acf5c33328aeae1c9198 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Tue, 13 Oct 2020 19:13:05 +0530 Subject: [PATCH 001/198] feat: Implement CatalogEntityClient, mainly to get Template entity from template name --- .../src/lib/catalog/CatalogEntityClient.ts | 70 +++++++++++++++++++ .../src/lib/catalog/index.ts | 16 +++++ 2 files changed, 86 insertions(+) create mode 100644 plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts create mode 100644 plugins/scaffolder-backend/src/lib/catalog/index.ts diff --git a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts new file mode 100644 index 0000000000..48856e7783 --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts @@ -0,0 +1,70 @@ +/* + * 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 fetch from 'node-fetch'; +import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { + ConflictError, + NotFoundError, + PluginEndpointDiscovery, +} from '@backstage/backend-common'; + +/** + * A catalog client tailored for reading out entity data from the catalog. + */ +export class CatalogEntityClient { + private readonly discovery: PluginEndpointDiscovery; + + constructor(options: { discovery: PluginEndpointDiscovery }) { + this.discovery = options.discovery; + } + + /** + * Looks up a single template using a template name. + * + * Throws a NotFoundError or ConflictError if 0 or multiple templates are found. + */ + async findTemplate(templateName: string): Promise { + const params = new URLSearchParams(); + params.append('kind', 'Template'); + + params.append('metadata.name', templateName); + + const baseUrl = await this.discovery.getBaseUrl('catalog'); + const response = await fetch(`${baseUrl}/entities?${params}`); + + if (!response.ok) { + const text = await response.text(); + throw new Error( + `Request failed with ${response.status} ${response.statusText}, ${text}`, + ); + } + + const templates: TemplateEntityV1alpha1[] = await response.json(); + + if (templates.length !== 1) { + if (templates.length > 1) { + throw new ConflictError( + 'Templates lookup resulted in multiple matches', + ); + } else { + throw new NotFoundError('Template not found'); + } + } + + return templates[0]; + } +} diff --git a/plugins/scaffolder-backend/src/lib/catalog/index.ts b/plugins/scaffolder-backend/src/lib/catalog/index.ts new file mode 100644 index 0000000000..a8de546e00 --- /dev/null +++ b/plugins/scaffolder-backend/src/lib/catalog/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 { CatalogEntityClient } from './CatalogEntityClient'; From a24b6577212edb80009ac79b8121de12a86278b5 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Tue, 13 Oct 2020 19:14:49 +0530 Subject: [PATCH 002/198] feat: use CatalogEntityClient --- plugins/scaffolder-backend/package.json | 3 ++- .../scaffolder-backend/src/service/router.ts | 23 +++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 66d6457333..fac30df196 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -44,7 +44,8 @@ "nodegit": "0.27.0", "uuid": "^8.2.0", "winston": "^3.2.1", - "yaml": "^1.10.0" + "yaml": "^1.10.0", + "node-fetch": "^2.6.1" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.25", diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index f61d2566b9..a556916df0 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -14,8 +14,12 @@ * limitations under the License. */ +import { + loadBackendConfig, + SingleHostDiscovery, +} from '@backstage/backend-common'; +import { JsonValue, ConfigReader } from '@backstage/config'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { JsonValue } from '@backstage/config'; import Docker from 'dockerode'; import express from 'express'; import Router from 'express-promise-router'; @@ -28,6 +32,7 @@ import { TemplaterBuilder, PublisherBuilder, } from '../scaffolder'; +import { CatalogEntityClient } from '../lib/catalog'; import { validate, ValidatorResult } from 'jsonschema'; export interface RouterOptions { @@ -56,6 +61,10 @@ export async function createRouter( const logger = parentLogger.child({ plugin: 'scaffolder' }); const jobProcessor = new JobProcessor(); + const config = ConfigReader.fromConfigs(await loadBackendConfig()); + const discovery = SingleHostDiscovery.fromConfig(config); + const entityClient = new CatalogEntityClient({ discovery }); + router .get('/v1/job/:jobId', ({ params }, res) => { const job = jobProcessor.get(params.jobId); @@ -81,14 +90,24 @@ export async function createRouter( }); }) .post('/v1/jobs', async (req, res) => { - const template: TemplateEntityV1alpha1 = req.body.template; + const templateName: string = req.body.templateName; const values: RequiredTemplateValues & Record = req.body.values; + let template: TemplateEntityV1alpha1; + try { + template = await entityClient.findTemplate(templateName); + } catch (e) { + // help me here + res.status(400).json({ errors: e }); + return; + } + const validationResult: ValidatorResult = validate( values, template.spec.schema, ); + if (!validationResult.valid) { res.status(400).json({ errors: validationResult.errors }); return; From 91e425fdabb5a024d81114ab643655422e71fb38 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Tue, 13 Oct 2020 19:16:22 +0530 Subject: [PATCH 003/198] feat: Pass entity unique id to POST body of /v1/jobs instead of entire entity --- plugins/scaffolder/src/api.ts | 10 +++------- .../src/components/TemplatePage/TemplatePage.tsx | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index f58c8e68d1..ca52418e92 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -15,7 +15,6 @@ */ import { createApiRef, DiscoveryApi } from '@backstage/core'; -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; export const scaffolderApiRef = createApiRef({ id: 'plugin.scaffolder.service', @@ -33,20 +32,17 @@ export class ScaffolderApi { * Executes the scaffolding of a component, given a template and its * parameter values. * - * @param template Template entity for the scaffolder to use. New project is going to be created out of this template. + * @param templateName Template name for the scaffolder to use. New project is going to be created out of this template. * @param values Parameters for the template, e.g. name, description */ - async scaffold( - template: TemplateEntityV1alpha1, - values: Record, - ) { + async scaffold(templateName: string, values: Record) { const url = `${await this.discoveryApi.getBaseUrl('scaffolder')}/v1/jobs`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ template, values: { ...values } }), + body: JSON.stringify({ templateName, values: { ...values } }), }); if (response.status !== 201) { diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index e68c795bd6..6c71a2eab1 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -97,7 +97,7 @@ export const TemplatePage = () => { const handleCreate = async () => { try { - const job = await scaffolderApi.scaffold(template!, formState); + const job = await scaffolderApi.scaffold(templateName, formState); setJobId(job); } catch (e) { errorApi.post(e); From 34a94c9a583dbbbd2ec156ac3769c114dedb4415 Mon Sep 17 00:00:00 2001 From: Jesko Steinberg Date: Mon, 12 Oct 2020 15:06:27 +0200 Subject: [PATCH 004/198] feat: make default scopes of Auth APIs configurable --- .../apis/implementations/auth/auth0/Auth0Auth.ts | 4 +++- .../implementations/auth/gitlab/GitlabAuth.ts | 4 +++- .../implementations/auth/google/GoogleAuth.ts | 16 +++++++++------- .../auth/microsoft/MicrosoftAuth.ts | 16 +++++++++------- .../apis/implementations/auth/okta/OktaAuth.ts | 4 +++- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/packages/core-api/src/apis/implementations/auth/auth0/Auth0Auth.ts b/packages/core-api/src/apis/implementations/auth/auth0/Auth0Auth.ts index 40e537169c..793d9f90d3 100644 --- a/packages/core-api/src/apis/implementations/auth/auth0/Auth0Auth.ts +++ b/packages/core-api/src/apis/implementations/auth/auth0/Auth0Auth.ts @@ -27,6 +27,7 @@ type CreateOptions = { discoveryApi: DiscoveryApi; oauthRequestApi: OAuthRequestApi; + defaultScopes?: string[]; environment?: string; provider?: AuthProvider & { id: string }; }; @@ -43,13 +44,14 @@ class Auth0Auth { environment = 'development', provider = DEFAULT_PROVIDER, oauthRequestApi, + defaultScopes = ['openid', `email`, `profile`], }: CreateOptions): typeof auth0AuthApiRef.T { return OAuth2.create({ discoveryApi, oauthRequestApi, provider, environment, - defaultScopes: ['openid', `email`, `profile`], + defaultScopes, }); } } 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 9e2acd4537..b6a2757143 100644 --- a/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts @@ -27,6 +27,7 @@ type CreateOptions = { discoveryApi: DiscoveryApi; oauthRequestApi: OAuthRequestApi; + defaultScopes?: string[]; environment?: string; provider?: AuthProvider & { id: string }; }; @@ -43,13 +44,14 @@ class GitlabAuth { environment = 'development', provider = DEFAULT_PROVIDER, oauthRequestApi, + defaultScopes = ['read_user'], }: CreateOptions): typeof gitlabAuthApiRef.T { return OAuth2.create({ discoveryApi, oauthRequestApi, provider, environment, - defaultScopes: ['read_user'], + defaultScopes, }); } } diff --git a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts index 7e84226508..2d2ff3ede7 100644 --- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -27,6 +27,7 @@ type CreateOptions = { discoveryApi: DiscoveryApi; oauthRequestApi: OAuthRequestApi; + defaultScopes?: string[]; environment?: string; provider?: AuthProvider & { id: string }; }; @@ -37,25 +38,26 @@ const DEFAULT_PROVIDER = { icon: GoogleIcon, }; +const SCOPE_PREFIX = 'https://www.googleapis.com/auth/'; + class GoogleAuth { static create({ discoveryApi, oauthRequestApi, environment = 'development', provider = DEFAULT_PROVIDER, + defaultScopes = [ + 'openid', + `${SCOPE_PREFIX}userinfo.email`, + `${SCOPE_PREFIX}userinfo.profile`, + ], }: CreateOptions): typeof googleAuthApiRef.T { - const SCOPE_PREFIX = 'https://www.googleapis.com/auth/'; - return OAuth2.create({ discoveryApi, oauthRequestApi, provider, environment, - defaultScopes: [ - 'openid', - `${SCOPE_PREFIX}userinfo.email`, - `${SCOPE_PREFIX}userinfo.profile`, - ], + defaultScopes, scopeTransform(scopes: string[]) { return scopes.map(scope => { if (scope === 'openid') { diff --git a/packages/core-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts b/packages/core-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts index 241ac7b802..db9180b31c 100644 --- a/packages/core-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts @@ -28,6 +28,7 @@ type CreateOptions = { discoveryApi: DiscoveryApi; oauthRequestApi: OAuthRequestApi; + defaultScopes?: string[]; environment?: string; provider?: AuthProvider & { id: string }; }; @@ -44,19 +45,20 @@ class MicrosoftAuth { provider = DEFAULT_PROVIDER, oauthRequestApi, discoveryApi, + defaultScopes = [ + 'openid', + 'offline_access', + 'profile', + 'email', + 'User.Read', + ], }: CreateOptions): typeof microsoftAuthApiRef.T { return OAuth2.create({ discoveryApi, oauthRequestApi, provider, environment, - defaultScopes: [ - 'openid', - 'offline_access', - 'profile', - 'email', - 'User.Read', - ], + defaultScopes, }); } } diff --git a/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts b/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts index 7e9ff77678..7382d24d62 100644 --- a/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/okta/OktaAuth.ts @@ -27,6 +27,7 @@ type CreateOptions = { discoveryApi: DiscoveryApi; oauthRequestApi: OAuthRequestApi; + defaultScopes?: string[]; environment?: string; provider?: AuthProvider & { id: string }; }; @@ -55,13 +56,14 @@ class OktaAuth { environment = 'development', provider = DEFAULT_PROVIDER, oauthRequestApi, + defaultScopes = ['openid', 'email', 'profile', 'offline_access'], }: CreateOptions): typeof oktaAuthApiRef.T { return OAuth2.create({ discoveryApi, oauthRequestApi, provider, environment, - defaultScopes: ['openid', 'email', 'profile', 'offline_access'], + defaultScopes, scopeTransform(scopes) { return scopes.map(scope => { if (OKTA_OIDC_SCOPES.has(scope)) { From c5acbbb8c42114338747935d7c044898f6578bd0 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Fri, 23 Oct 2020 23:32:10 +0530 Subject: [PATCH 005/198] chore: replace node-fetch => cross-fetch --- plugins/scaffolder-backend/package.json | 2 +- .../scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts | 2 +- yarn.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index fac30df196..e87f86961e 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -45,7 +45,7 @@ "uuid": "^8.2.0", "winston": "^3.2.1", "yaml": "^1.10.0", - "node-fetch": "^2.6.1" + "cross-fetch": "^3.0.6" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.25", diff --git a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts index 48856e7783..84676b63fe 100644 --- a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts +++ b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import fetch from 'node-fetch'; +import fetch from 'cross-fetch'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { ConflictError, diff --git a/yarn.lock b/yarn.lock index 4832a549b8..2c7c609835 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9021,7 +9021,7 @@ cross-env@^7.0.0: dependencies: cross-spawn "^7.0.1" -cross-fetch@3.0.6, cross-fetch@^3.0.4, cross-fetch@^3.0.5: +cross-fetch@3.0.6, cross-fetch@^3.0.4, cross-fetch@^3.0.5, cross-fetch@^3.0.6: version "3.0.6" resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.6.tgz#3a4040bc8941e653e0e9cf17f29ebcd177d3365c" integrity sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ== From 8440f8f91be240cf96c7447262547adb653ba410 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Tue, 27 Oct 2020 10:02:56 +0530 Subject: [PATCH 006/198] feat: load config directly, #2976 --- plugins/scaffolder-backend/src/service/router.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index a556916df0..bf1eeae8dd 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -17,8 +17,9 @@ import { loadBackendConfig, SingleHostDiscovery, + getRootLogger, } from '@backstage/backend-common'; -import { JsonValue, ConfigReader } from '@backstage/config'; +import { JsonValue } from '@backstage/config'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import Docker from 'dockerode'; import express from 'express'; @@ -61,7 +62,10 @@ export async function createRouter( const logger = parentLogger.child({ plugin: 'scaffolder' }); const jobProcessor = new JobProcessor(); - const config = ConfigReader.fromConfigs(await loadBackendConfig()); + const config = await loadBackendConfig({ + argv: process.argv, + logger: getRootLogger(), + }); const discovery = SingleHostDiscovery.fromConfig(config); const entityClient = new CatalogEntityClient({ discovery }); From f2a9bba5e27ad2e2ec29d3fdccec303d8de1e808 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Tue, 27 Oct 2020 10:53:40 +0530 Subject: [PATCH 007/198] feat: update related to #3066 --- .../src/lib/catalog/CatalogEntityClient.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts index 84676b63fe..4541f03a4a 100644 --- a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts +++ b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts @@ -38,13 +38,15 @@ export class CatalogEntityClient { * Throws a NotFoundError or ConflictError if 0 or multiple templates are found. */ async findTemplate(templateName: string): Promise { - const params = new URLSearchParams(); - params.append('kind', 'Template'); - - params.append('metadata.name', templateName); + const conditions = [ + 'kind=template', + `metadata.name=${encodeURIComponent(templateName)}`, + ]; const baseUrl = await this.discovery.getBaseUrl('catalog'); - const response = await fetch(`${baseUrl}/entities?${params}`); + const response = await fetch( + `${baseUrl}/entities?filter=${conditions.join(',')}`, + ); if (!response.ok) { const text = await response.text(); From e29ffbe4118e858108f419cd1f3cde3415920bda Mon Sep 17 00:00:00 2001 From: Vividh Chandna Date: Tue, 27 Oct 2020 16:53:27 +0530 Subject: [PATCH 008/198] fix(TechDocs): Fix broken page title while fetching metadata --- plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx b/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx index a7db342544..f4c447bae2 100644 --- a/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsPageHeader.tsx @@ -84,6 +84,7 @@ export const TechDocsPageHeader = ({ return (
} + pageTitleOverride={siteName || name} subtitle={ siteDescription && siteDescription !== 'None' ? siteDescription : '' } From 57b54c8ed402674fec0cf3da02509d5e3a18722a Mon Sep 17 00:00:00 2001 From: Vividh Chandna Date: Tue, 27 Oct 2020 17:04:26 +0530 Subject: [PATCH 009/198] Add changeset --- .changeset/ten-bees-wash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ten-bees-wash.md diff --git a/.changeset/ten-bees-wash.md b/.changeset/ten-bees-wash.md new file mode 100644 index 0000000000..179dbf87f4 --- /dev/null +++ b/.changeset/ten-bees-wash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +While techdocs fetches site name and metadata for the component, the page title was displayed as '[object Object] | Backstage'. This has now been fixed to display the component ID if site name is not present or being fetched. From 8306a19af170a5894c7c59ca339a39abf6a10400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Frinnstr=C3=B6m?= Date: Tue, 27 Oct 2020 15:50:15 +0100 Subject: [PATCH 010/198] Update the PreparerBase options type --- .../src/scaffolder/stages/prepare/azure.test.ts | 12 ++++++++---- .../src/scaffolder/stages/prepare/azure.ts | 4 ++-- .../src/scaffolder/stages/prepare/file.test.ts | 2 ++ .../src/scaffolder/stages/prepare/file.ts | 4 ++-- .../src/scaffolder/stages/prepare/github.test.ts | 12 ++++++++---- .../src/scaffolder/stages/prepare/github.ts | 4 ++-- .../src/scaffolder/stages/prepare/gitlab.test.ts | 12 ++++++++---- .../src/scaffolder/stages/prepare/gitlab.ts | 4 ++-- .../src/scaffolder/stages/prepare/types.ts | 7 ++++++- 9 files changed, 40 insertions(+), 21 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts index 97f01e6b7d..76dd842368 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts @@ -30,6 +30,7 @@ import { TemplateEntityV1alpha1, LOCATION_ANNOTATION, } from '@backstage/catalog-model'; +import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; describe('AzurePreparer', () => { @@ -78,7 +79,7 @@ describe('AzurePreparer', () => { it('calls the clone command with the correct arguments for a repository', async () => { const preparer = new AzurePreparer(ConfigReader.fromConfigs([])); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mocks.Clone.clone).toHaveBeenNthCalledWith( 1, 'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo', @@ -104,7 +105,7 @@ describe('AzurePreparer', () => { }, ]), ); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mocks.Clone.clone).toHaveBeenNthCalledWith( 1, 'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo', @@ -122,7 +123,7 @@ describe('AzurePreparer', () => { it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { const preparer = new AzurePreparer(ConfigReader.fromConfigs([])); delete mockEntity.spec.path; - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mocks.Clone.clone).toHaveBeenNthCalledWith( 1, 'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo', @@ -134,7 +135,9 @@ describe('AzurePreparer', () => { it('return the temp directory with the path to the folder if it is specified', async () => { const preparer = new AzurePreparer(ConfigReader.fromConfigs([])); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity); + const response = await preparer.prepare(mockEntity, { + logger: getVoidLogger(), + }); expect(response.split('\\').join('/')).toMatch( /\/template\/test\/1\/2\/3$/, @@ -145,6 +148,7 @@ describe('AzurePreparer', () => { const preparer = new AzurePreparer(ConfigReader.fromConfigs([])); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { + logger: getVoidLogger(), workingDirectory: '/workDir', }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts index bdcc7e07de..333bf9116c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.ts @@ -19,7 +19,7 @@ import path from 'path'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; import { InputError } from '@backstage/backend-common'; -import { PreparerBase } from './types'; +import { PreparerBase, PreparerOptions } from './types'; import GitUriParser from 'git-url-parse'; import { Clone, Cred } from 'nodegit'; import { Config } from '@backstage/config'; @@ -34,7 +34,7 @@ export class AzurePreparer implements PreparerBase { async prepare( template: TemplateEntityV1alpha1, - opts?: { workingDirectory?: string }, + opts: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts index d226736efe..2b328f420b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts @@ -23,6 +23,7 @@ import { TemplateEntityV1alpha1, LOCATION_ANNOTATION, } from '@backstage/catalog-model'; +import { getVoidLogger } from '@backstage/backend-common'; const setupTest = async (fixturePath: string) => { const locationForTemplateYaml = path.resolve( @@ -44,6 +45,7 @@ const setupTest = async (fixturePath: string) => { const filePreparer = new FilePreparer(); const resultDir = await filePreparer.prepare(template, { + logger: getVoidLogger(), workingDirectory: os.tmpdir(), }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts index 683d0c8cfb..08129c7397 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts @@ -19,12 +19,12 @@ import path from 'path'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; import { InputError } from '@backstage/backend-common'; -import { PreparerBase } from './types'; +import { PreparerBase, PreparerOptions } from './types'; export class FilePreparer implements PreparerBase { async prepare( template: TemplateEntityV1alpha1, - opts?: { workingDirectory?: string }, + opts: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index f251a39d28..af3932d71a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -30,6 +30,7 @@ import { TemplateEntityV1alpha1, LOCATION_ANNOTATION, } from '@backstage/catalog-model'; +import { getVoidLogger } from '@backstage/backend-common'; describe('GitHubPreparer', () => { let mockEntity: TemplateEntityV1alpha1; @@ -76,7 +77,7 @@ describe('GitHubPreparer', () => { }); it('calls the clone command with the correct arguments for a repository', async () => { const preparer = new GithubPreparer(); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mocks.Clone.clone).toHaveBeenNthCalledWith( 1, 'https://github.com/benjdlambert/backstage-graphql-template', @@ -89,7 +90,7 @@ describe('GitHubPreparer', () => { it('calls the clone command with the correct arguments for a repository when no path is provided', async () => { const preparer = new GithubPreparer(); delete mockEntity.spec.path; - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mocks.Clone.clone).toHaveBeenNthCalledWith( 1, 'https://github.com/benjdlambert/backstage-graphql-template', @@ -103,7 +104,9 @@ describe('GitHubPreparer', () => { it('return the temp directory with the path to the folder if it is specified', async () => { const preparer = new GithubPreparer(); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity); + const response = await preparer.prepare(mockEntity, { + logger: getVoidLogger(), + }); expect(response.split('\\').join('/')).toMatch( /\/template\/test\/1\/2\/3$/, @@ -114,6 +117,7 @@ describe('GitHubPreparer', () => { const preparer = new GithubPreparer(); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { + logger: getVoidLogger(), workingDirectory: '/workDir', }); @@ -124,7 +128,7 @@ describe('GitHubPreparer', () => { it('calls the clone command with the token when provided', async () => { const preparer = new GithubPreparer({ token: 'abc' }); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mocks.Clone.clone).toHaveBeenNthCalledWith( 1, 'https://github.com/benjdlambert/backstage-graphql-template', diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index 04b0bbcce1..e6b41907be 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -19,7 +19,7 @@ import path from 'path'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; import { InputError } from '@backstage/backend-common'; -import { PreparerBase } from './types'; +import { PreparerBase, PreparerOptions } from './types'; import GitUriParser from 'git-url-parse'; import { Clone, CloneOptions, Cred } from 'nodegit'; @@ -32,7 +32,7 @@ export class GithubPreparer implements PreparerBase { async prepare( template: TemplateEntityV1alpha1, - opts?: { workingDirectory?: string }, + opts: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts index a229c4f394..6be3f790f5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -30,6 +30,7 @@ import { LOCATION_ANNOTATION, } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '@backstage/backend-common'; const mockEntityWithProtocol = (protocol: string): TemplateEntityV1alpha1 => ({ apiVersion: 'backstage.io/v1alpha1', @@ -79,7 +80,7 @@ describe('GitLabPreparer', () => { it(`calls the clone command with the correct arguments for a repository using the ${protocol} protocol`, async () => { const preparer = new GitlabPreparer(ConfigReader.fromConfigs([])); mockEntity = mockEntityWithProtocol(protocol); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mocks.Clone.clone).toHaveBeenNthCalledWith( 1, 'https://gitlab.com/benjdlambert/backstage-graphql-template', @@ -106,7 +107,7 @@ describe('GitLabPreparer', () => { ]), ); mockEntity = mockEntityWithProtocol(protocol); - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mocks.Clone.clone).toHaveBeenNthCalledWith( 1, 'https://gitlab.com/benjdlambert/backstage-graphql-template', @@ -125,7 +126,7 @@ describe('GitLabPreparer', () => { const preparer = new GitlabPreparer(ConfigReader.fromConfigs([])); mockEntity = mockEntityWithProtocol(protocol); delete mockEntity.spec.path; - await preparer.prepare(mockEntity); + await preparer.prepare(mockEntity, { logger: getVoidLogger() }); expect(mocks.Clone.clone).toHaveBeenNthCalledWith( 1, 'https://gitlab.com/benjdlambert/backstage-graphql-template', @@ -138,7 +139,9 @@ describe('GitLabPreparer', () => { const preparer = new GitlabPreparer(ConfigReader.fromConfigs([])); mockEntity = mockEntityWithProtocol(protocol); mockEntity.spec.path = './template/test/1/2/3'; - const response = await preparer.prepare(mockEntity); + const response = await preparer.prepare(mockEntity, { + logger: getVoidLogger(), + }); expect(response.split('\\').join('/')).toMatch( /\/template\/test\/1\/2\/3$/, @@ -149,6 +152,7 @@ describe('GitLabPreparer', () => { const preparer = new GitlabPreparer(ConfigReader.fromConfigs([])); mockEntity.spec.path = './template/test/1/2/3'; const response = await preparer.prepare(mockEntity, { + logger: getVoidLogger(), workingDirectory: '/workDir', }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index 4553137c26..605c8db2df 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -19,7 +19,7 @@ import path from 'path'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { parseLocationAnnotation } from '../helpers'; import { InputError } from '@backstage/backend-common'; -import { PreparerBase } from './types'; +import { PreparerBase, PreparerOptions } from './types'; import GitUriParser from 'git-url-parse'; import { Clone, Cred } from 'nodegit'; import { Config } from '@backstage/config'; @@ -35,7 +35,7 @@ export class GitlabPreparer implements PreparerBase { async prepare( template: TemplateEntityV1alpha1, - opts?: { workingDirectory?: string }, + opts: PreparerOptions, ): Promise { const { protocol, location } = parseLocationAnnotation(template); const workingDirectory = opts?.workingDirectory ?? os.tmpdir(); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts index c17edc24a4..4936461c5c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/types.ts @@ -17,6 +17,11 @@ import type { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { Logger } from 'winston'; import { RemoteProtocol } from '../types'; +export type PreparerOptions = { + logger: Logger; + workingDirectory?: string; +}; + export type PreparerBase = { /** * Given an Entity definition from the Service Catalog, go and prepare a directory @@ -25,7 +30,7 @@ export type PreparerBase = { */ prepare( template: TemplateEntityV1alpha1, - opts?: { logger: Logger; workingDirectory?: string }, + opts: PreparerOptions, ): Promise; }; From 2283fd92fc0c71163f447dc21fc32cb7656215dd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 27 Oct 2020 17:03:13 +0100 Subject: [PATCH 011/198] create-app: decouple versions --- packages/create-app/package.json | 24 ++++++ packages/create-app/src/createApp.ts | 4 +- packages/create-app/src/lib/tasks.ts | 15 +++- packages/create-app/src/versions.ts | 82 +++++++++++++++++++ .../templates/default-app/package.json.hbs | 2 +- .../default-app/packages/app/package.json.hbs | 32 ++++---- .../packages/backend/package.json.hbs | 20 ++--- 7 files changed, 149 insertions(+), 30 deletions(-) create mode 100644 packages/create-app/src/versions.ts diff --git a/packages/create-app/package.json b/packages/create-app/package.json index ff9e1fa636..0dd8897d0b 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -37,6 +37,30 @@ "recursive-readdir": "^2.2.2" }, "devDependencies": { + "@backstage/backend-common": "^0.1.1-alpha.25", + "@backstage/catalog-model": "^0.1.1-alpha.25", + "@backstage/cli": "^0.1.1-alpha.25", + "@backstage/config": "^0.1.1-alpha.25", + "@backstage/core": "^0.1.1-alpha.25", + "@backstage/plugin-api-docs": "^0.1.1-alpha.25", + "@backstage/plugin-auth-backend": "^0.1.1-alpha.25", + "@backstage/plugin-catalog": "^0.1.1-alpha.25", + "@backstage/plugin-catalog-backend": "^0.1.1-alpha.25", + "@backstage/plugin-circleci": "^0.1.1-alpha.25", + "@backstage/plugin-explore": "^0.1.1-alpha.25", + "@backstage/plugin-github-actions": "^0.1.1-alpha.25", + "@backstage/plugin-lighthouse": "^0.1.1-alpha.25", + "@backstage/plugin-proxy-backend": "^0.1.1-alpha.25", + "@backstage/plugin-register-component": "^0.1.1-alpha.25", + "@backstage/plugin-rollbar-backend": "^0.1.1-alpha.25", + "@backstage/plugin-scaffolder": "^0.1.1-alpha.25", + "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.25", + "@backstage/plugin-tech-radar": "^0.1.1-alpha.25", + "@backstage/plugin-techdocs": "^0.1.1-alpha.25", + "@backstage/plugin-techdocs-backend": "^0.1.1-alpha.25", + "@backstage/plugin-user-settings": "^0.1.1-alpha.25", + "@backstage/test-utils": "^0.1.1-alpha.25", + "@backstage/theme": "^0.1.1-alpha.25", "@types/fs-extra": "^9.0.1", "@types/inquirer": "^7.3.1", "@types/ora": "^3.2.0", diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 83e052c91b..c0874d4032 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -22,7 +22,7 @@ import inquirer, { Answers, Question } from 'inquirer'; import { exec as execCb } from 'child_process'; import { resolve as resolvePath } from 'path'; import { findPaths } from '@backstage/cli-common'; -import { version } from '../package.json'; +import { versions } from './versions'; import os from 'os'; import { Task, templatingTask } from './lib/tasks'; @@ -133,7 +133,7 @@ export default async (cmd: Command): Promise => { await createTemporaryAppFolder(tempDir); Task.section('Preparing files'); - await templatingTask(templateDir, tempDir, { ...answers, version }); + await templatingTask(templateDir, tempDir, answers, versions); Task.section('Moving to final location'); await moveApp(tempDir, appDir, answers.name); diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 0753301b78..73db175709 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -68,6 +68,7 @@ export async function templatingTask( templateDir: string, destinationDir: string, context: any, + versions: { [name: string]: string }, ) { const files = await recursive(templateDir).catch(error => { throw new Error(`Failed to read template directory: ${error.message}`); @@ -83,7 +84,19 @@ export async function templatingTask( const template = await fs.readFile(file); const compiled = handlebars.compile(template.toString()); - const contents = compiled({ name: basename(destination), ...context }); + const contents = compiled( + { name: basename(destination), ...context }, + { + helpers: { + version(name: string) { + if (versions[name]) { + return versions[name]; + } + throw new Error(`No version available for package ${name}`); + }, + }, + }, + ); await fs.writeFile(destination, contents).catch(error => { throw new Error( diff --git a/packages/create-app/src/versions.ts b/packages/create-app/src/versions.ts new file mode 100644 index 0000000000..ed03903469 --- /dev/null +++ b/packages/create-app/src/versions.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. + */ + +/* eslint-disable import/no-extraneous-dependencies,monorepo/no-internal-import */ + +/* +This is a list of all packages used by the template. If dependencies are added or removed, +this list should be updated as well. + +The list, and the accompanying devDependencies entries, are here to ensure correct versioning +and bumping of this package. Without this list the version would not be bumped unless we +manually trigger a release. + +This does not create an actual dependency on these packages and does not bring in any code. +Rollup will extract the value of the version field in each package at build time without +leaving any imports in place. +*/ + +import { version as backendCommon } from '@backstage/backend-common/package.json'; +import { version as catalogModel } from '@backstage/catalog-model/package.json'; +import { version as cli } from '@backstage/cli/package.json'; +import { version as config } from '@backstage/config/package.json'; +import { version as core } from '@backstage/core/package.json'; +import { version as pluginApiDocs } from '@backstage/plugin-api-docs/package.json'; +import { version as pluginAuthBackend } from '@backstage/plugin-auth-backend/package.json'; +import { version as pluginCatalog } from '@backstage/plugin-catalog/package.json'; +import { version as pluginCatalogBackend } from '@backstage/plugin-catalog-backend/package.json'; +import { version as pluginCircleci } from '@backstage/plugin-circleci/package.json'; +import { version as pluginExplore } from '@backstage/plugin-explore/package.json'; +import { version as pluginGithubActions } from '@backstage/plugin-github-actions/package.json'; +import { version as pluginLighthouse } from '@backstage/plugin-lighthouse/package.json'; +import { version as pluginProxyBackend } from '@backstage/plugin-proxy-backend/package.json'; +import { version as pluginRegisterComponent } from '@backstage/plugin-register-component/package.json'; +import { version as pluginRollbarBackend } from '@backstage/plugin-rollbar-backend/package.json'; +import { version as pluginScaffolder } from '@backstage/plugin-scaffolder/package.json'; +import { version as pluginScaffolderBackend } from '@backstage/plugin-scaffolder-backend/package.json'; +import { version as pluginTechRadar } from '@backstage/plugin-tech-radar/package.json'; +import { version as pluginTechdocs } from '@backstage/plugin-techdocs/package.json'; +import { version as pluginTechdocsBackend } from '@backstage/plugin-techdocs-backend/package.json'; +import { version as pluginUserSettings } from '@backstage/plugin-user-settings/package.json'; +import { version as testUtils } from '@backstage/test-utils/package.json'; +import { version as theme } from '@backstage/theme/package.json'; + +export const versions = { + '@backstage/backend-common': backendCommon, + '@backstage/catalog-model': catalogModel, + '@backstage/cli': cli, + '@backstage/config': config, + '@backstage/core': core, + '@backstage/plugin-api-docs': pluginApiDocs, + '@backstage/plugin-auth-backend': pluginAuthBackend, + '@backstage/plugin-catalog': pluginCatalog, + '@backstage/plugin-catalog-backend': pluginCatalogBackend, + '@backstage/plugin-circleci': pluginCircleci, + '@backstage/plugin-explore': pluginExplore, + '@backstage/plugin-github-actions': pluginGithubActions, + '@backstage/plugin-lighthouse': pluginLighthouse, + '@backstage/plugin-proxy-backend': pluginProxyBackend, + '@backstage/plugin-register-component': pluginRegisterComponent, + '@backstage/plugin-rollbar-backend': pluginRollbarBackend, + '@backstage/plugin-scaffolder': pluginScaffolder, + '@backstage/plugin-scaffolder-backend': pluginScaffolderBackend, + '@backstage/plugin-tech-radar': pluginTechRadar, + '@backstage/plugin-techdocs': pluginTechdocs, + '@backstage/plugin-techdocs-backend': pluginTechdocsBackend, + '@backstage/plugin-user-settings': pluginUserSettings, + '@backstage/test-utils': testUtils, + '@backstage/theme': theme, +}; diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index 27606640f3..b9fb0f3744 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -26,7 +26,7 @@ ] }, "devDependencies": { - "@backstage/cli": "^{{version}}", + "@backstage/cli": "^{{version '@backstage/cli'}}", "@spotify/prettier-config": "^7.0.0", "lerna": "^3.20.2", "prettier": "^1.19.1" diff --git a/packages/create-app/templates/default-app/packages/app/package.json.hbs b/packages/create-app/templates/default-app/packages/app/package.json.hbs index 103e75f6e0..fd0d5cbf36 100644 --- a/packages/create-app/templates/default-app/packages/app/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/app/package.json.hbs @@ -6,22 +6,22 @@ "dependencies": { "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", - "@backstage/cli": "^{{version}}", - "@backstage/core": "^{{version}}", - "@backstage/plugin-api-docs": "^{{version}}", - "@backstage/plugin-catalog": "^{{version}}", - "@backstage/plugin-register-component": "^{{version}}", - "@backstage/plugin-scaffolder": "^{{version}}", - "@backstage/plugin-techdocs": "^{{version}}", - "@backstage/catalog-model": "^{{version}}", - "@backstage/plugin-circleci": "^{{version}}", - "@backstage/plugin-explore": "^{{version}}", - "@backstage/plugin-lighthouse": "^{{version}}", - "@backstage/plugin-tech-radar": "^{{version}}", - "@backstage/plugin-github-actions": "^{{version}}", - "@backstage/plugin-user-settings": "^{{version}}", - "@backstage/test-utils": "^{{version}}", - "@backstage/theme": "^{{version}}", + "@backstage/cli": "^{{version '@backstage/cli'}}", + "@backstage/core": "^{{version '@backstage/core'}}", + "@backstage/plugin-api-docs": "^{{version '@backstage/plugin-api-docs'}}", + "@backstage/plugin-catalog": "^{{version '@backstage/plugin-catalog'}}", + "@backstage/plugin-register-component": "^{{version '@backstage/plugin-register-component'}}", + "@backstage/plugin-scaffolder": "^{{version '@backstage/plugin-scaffolder'}}", + "@backstage/plugin-techdocs": "^{{version '@backstage/plugin-techdocs'}}", + "@backstage/catalog-model": "^{{version '@backstage/catalog-model'}}", + "@backstage/plugin-circleci": "^{{version '@backstage/plugin-circleci'}}", + "@backstage/plugin-explore": "^{{version '@backstage/plugin-explore'}}", + "@backstage/plugin-lighthouse": "^{{version '@backstage/plugin-lighthouse'}}", + "@backstage/plugin-tech-radar": "^{{version '@backstage/plugin-tech-radar'}}", + "@backstage/plugin-github-actions": "^{{version '@backstage/plugin-github-actions'}}", + "@backstage/plugin-user-settings": "^{{version '@backstage/plugin-user-settings'}}", + "@backstage/test-utils": "^{{version '@backstage/test-utils'}}", + "@backstage/theme": "^{{version '@backstage/theme'}}", "history": "^5.0.0", "react": "^16.13.1", "react-dom": "^16.13.1", diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index 7f811e3739..dde04868b9 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -17,15 +17,15 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^{{version}}", - "@backstage/catalog-model": "^{{version}}", - "@backstage/config": "^{{version}}", - "@backstage/plugin-auth-backend": "^{{version}}", - "@backstage/plugin-catalog-backend": "^{{version}}", - "@backstage/plugin-proxy-backend": "^{{version}}", - "@backstage/plugin-rollbar-backend": "^{{version}}", - "@backstage/plugin-scaffolder-backend": "^{{version}}", - "@backstage/plugin-techdocs-backend": "^{{version}}", + "@backstage/backend-common": "^{{version '@backstage/backend-common'}}", + "@backstage/catalog-model": "^{{version '@backstage/catalog-model'}}", + "@backstage/config": "^{{version '@backstage/config'}}", + "@backstage/plugin-auth-backend": "^{{version '@backstage/plugin-auth-backend'}}", + "@backstage/plugin-catalog-backend": "^{{version '@backstage/plugin-catalog-backend'}}", + "@backstage/plugin-proxy-backend": "^{{version '@backstage/plugin-proxy-backend'}}", + "@backstage/plugin-rollbar-backend": "^{{version '@backstage/plugin-rollbar-backend'}}", + "@backstage/plugin-scaffolder-backend": "^{{version '@backstage/plugin-scaffolder-backend'}}", + "@backstage/plugin-techdocs-backend": "^{{version '@backstage/plugin-techdocs-backend'}}", "@octokit/rest": "^18.0.0", "@gitbeaker/node": "^23.5.0", "dockerode": "^3.2.0", @@ -41,7 +41,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^{{version}}", + "@backstage/cli": "^{{version '@backstage/cli'}}", "@types/dockerode": "^2.5.32", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5", From 2711b9da42d62f61bfbe951f5918827138a58337 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 27 Oct 2020 18:22:21 +0100 Subject: [PATCH 012/198] cli: decouple template versions from cli version --- packages/cli/package.json | 7 ++++ .../commands/create-plugin/createPlugin.ts | 26 ++++++++------- packages/cli/src/commands/plugin/diff.ts | 14 +++----- packages/cli/src/lib/diff/read.ts | 12 ++++++- packages/cli/src/lib/tasks.test.ts | 16 +++++++--- packages/cli/src/lib/tasks.ts | 15 ++++++++- packages/cli/src/lib/version.ts | 32 +++++++++++++++++++ .../default-backend-plugin/package.json.hbs | 8 ++--- .../templates/default-plugin/package.json.hbs | 12 +++---- 9 files changed, 105 insertions(+), 37 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 8a330d861b..89c0520b59 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -108,6 +108,13 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-common": "^0.1.1-alpha.25", + "@backstage/cli": "^0.1.1-alpha.25", + "@backstage/config": "^0.1.1-alpha.25", + "@backstage/core": "^0.1.1-alpha.25", + "@backstage/dev-utils": "^0.1.1-alpha.25", + "@backstage/test-utils": "^0.1.1-alpha.25", + "@backstage/theme": "^0.1.1-alpha.25", "@types/diff": "^4.0.2", "@types/fs-extra": "^9.0.1", "@types/html-webpack-plugin": "^3.2.2", diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index 057e36ceb9..b603e732b8 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -28,8 +28,8 @@ import { getCodeownersFilePath, } from '../../lib/codeowners'; import { paths } from '../../lib/paths'; +import { versions } from '../../lib/version'; import { Task, templatingTask } from '../../lib/tasks'; -import { version as backstageVersion } from '../../lib/version'; const exec = promisify(execCb); @@ -243,7 +243,7 @@ export default async (cmd: Command) => { ? paths.resolveTargetRoot('plugins', pluginId) : paths.resolveTargetRoot(pluginId); const ownerIds = parseOwnerIds(answers.owner); - const { version } = isMonoRepo + const { version: pluginVersion } = isMonoRepo ? await fs.readJson(paths.resolveTargetRoot('lerna.json')) : { version: '0.1.0' }; @@ -259,14 +259,18 @@ export default async (cmd: Command) => { Task.section('Preparing files'); - await templatingTask(templateDir, tempDir, { - ...answers, - version, - backstageVersion, - name, - privatePackage, - npmRegistry, - }); + await templatingTask( + templateDir, + tempDir, + { + ...answers, + pluginVersion, + name, + privatePackage, + npmRegistry, + }, + versions, + ); Task.section('Moving to final location'); await movePlugin(tempDir, pluginDir, pluginId); @@ -276,7 +280,7 @@ export default async (cmd: Command) => { if ((await fs.pathExists(appPackage)) && !cmd.backend) { Task.section('Adding plugin as dependency in app'); - await addPluginDependencyToApp(paths.targetRoot, name, version); + await addPluginDependencyToApp(paths.targetRoot, name, pluginVersion); Task.section('Import plugin in app'); await addPluginToApp(paths.targetRoot, pluginId, name); diff --git a/packages/cli/src/commands/plugin/diff.ts b/packages/cli/src/commands/plugin/diff.ts index 012c061f74..416d67869b 100644 --- a/packages/cli/src/commands/plugin/diff.ts +++ b/packages/cli/src/commands/plugin/diff.ts @@ -25,13 +25,12 @@ import { yesPromptFunc, } from '../../lib/diff'; import { paths } from '../../lib/paths'; -import { version as backstageVersion } from '../../lib/version'; export type PluginData = { id: string; name: string; privatePackage: string; - version: string; + pluginVersion: string; npmRegistry: string; }; @@ -62,10 +61,7 @@ export default async (cmd: Command) => { } const data = await readPluginData(); - const templateFiles = await diffTemplateFiles('default-plugin', { - backstageVersion, - ...data, - }); + const templateFiles = await diffTemplateFiles('default-plugin', data); await handleAllFiles(fileHandlers, templateFiles, promptFunc); await finalize(); }; @@ -74,13 +70,13 @@ export default async (cmd: Command) => { async function readPluginData(): Promise { let name: string; let privatePackage: string; - let version: string; + let pluginVersion: string; let npmRegistry: string; try { const pkg = require(paths.resolveTarget('package.json')); name = pkg.name; privatePackage = pkg.private; - version = pkg.version; + pluginVersion = pkg.version; const scope = name.split('/')[0]; if (`${scope}:registry` in pkg.publishConfig) { const registryURL = pkg.publishConfig[`${scope}:registry`]; @@ -102,5 +98,5 @@ async function readPluginData(): Promise { const id = pluginIdMatch[1]; - return { id, name, privatePackage, version, npmRegistry }; + return { id, name, privatePackage, pluginVersion, npmRegistry }; } diff --git a/packages/cli/src/lib/diff/read.ts b/packages/cli/src/lib/diff/read.ts index 75e1b86d24..a4a7355d2d 100644 --- a/packages/cli/src/lib/diff/read.ts +++ b/packages/cli/src/lib/diff/read.ts @@ -24,6 +24,7 @@ import handlebars from 'handlebars'; import recursiveReadDir from 'recursive-readdir'; import { paths } from '../paths'; import { FileDiff } from './types'; +import { versions } from '../../lib/version'; export type TemplatedFile = { path: string; @@ -40,7 +41,16 @@ async function readTemplateFile( return contents; } - return handlebars.compile(contents)(templateVars); + return handlebars.compile(contents)(templateVars, { + helpers: { + version(name: string) { + if (versions[name]) { + return versions[name]; + } + throw new Error(`No version available for package ${name}`); + }, + }, + }); } async function readTemplate( diff --git a/packages/cli/src/lib/tasks.test.ts b/packages/cli/src/lib/tasks.test.ts index d80fcbe9b0..a42902c60b 100644 --- a/packages/cli/src/lib/tasks.test.ts +++ b/packages/cli/src/lib/tasks.test.ts @@ -33,7 +33,8 @@ describe('templatingTask', () => { // Files content const testFileContent = 'testing'; - const testVersionFileContent = 'version: {{version}}'; + const testVersionFileContent = + "version: {{pluginVersion}} {{version 'mock-pkg'}}"; mockFs({ [tmplDir]: { @@ -45,15 +46,20 @@ describe('templatingTask', () => { [destDir]: {}, }); - await templatingTask(tmplDir, destDir, { - version: '0.0.0', - }); + await templatingTask( + tmplDir, + destDir, + { + version: '0.0.0', + }, + { 'mock-pkg': '0.1.2' }, + ); await expect( fs.readFile(resolvePath(destDir, 'test.txt'), 'utf8'), ).resolves.toBe(testFileContent); await expect( fs.readFile(resolvePath(destDir, 'sub/version.txt'), 'utf8'), - ).resolves.toBe('version: 0.0.0'); + ).resolves.toBe('version: 0.0.0 0.1.2'); }); }); diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts index 80dc1a5ed7..4b92c6b9cb 100644 --- a/packages/cli/src/lib/tasks.ts +++ b/packages/cli/src/lib/tasks.ts @@ -69,6 +69,7 @@ export async function templatingTask( templateDir: string, destinationDir: string, context: any, + versions: { [name: string]: string }, ) { const files = await recursive(templateDir).catch(error => { throw new Error(`Failed to read template directory: ${error.message}`); @@ -85,7 +86,19 @@ export async function templatingTask( const template = await fs.readFile(file); const compiled = handlebars.compile(template.toString()); - const contents = compiled({ name: basename(destination), ...context }); + const contents = compiled( + { name: basename(destination), ...context }, + { + helpers: { + version(name: string) { + if (versions[name]) { + return versions[name]; + } + throw new Error(`No version available for package ${name}`); + }, + }, + }, + ); await fs.writeFile(destination, contents).catch(error => { throw new Error( diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/lib/version.ts index 24734b87cc..5cc3eb7fbb 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/lib/version.ts @@ -17,6 +17,38 @@ import fs from 'fs-extra'; import { paths } from './paths'; +/* eslint-disable import/no-extraneous-dependencies,monorepo/no-internal-import */ +/* +This is a list of all packages used by the templates. If dependencies are added or removed, +this list should be updated as well. + +The list, and the accompanying devDependencies entries, are here to ensure correct versioning +and bumping of this package. Without this list the version would not be bumped unless we +manually trigger a release. + +This does not create an actual dependency on these packages and does not bring in any code. +Rollup will extract the value of the version field in each package at build time without +leaving any imports in place. +*/ + +import { version as backendCommon } from '@backstage/backend-common/package.json'; +import { version as cli } from '@backstage/cli/package.json'; +import { version as config } from '@backstage/config/package.json'; +import { version as core } from '@backstage/core/package.json'; +import { version as devUtils } from '@backstage/dev-utils/package.json'; +import { version as testUtils } from '@backstage/test-utils/package.json'; +import { version as theme } from '@backstage/theme/package.json'; + +export const versions: { [name: string]: string } = { + '@backstage/backend-common': backendCommon, + '@backstage/cli': cli, + '@backstage/config': config, + '@backstage/core': core, + '@backstage/dev-utils': devUtils, + '@backstage/test-utils': testUtils, + '@backstage/theme': theme, +}; + export function findVersion() { const pkgContent = fs.readFileSync(paths.resolveOwn('package.json'), 'utf8'); return JSON.parse(pkgContent).version; diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index 1a73515622..b762bb9645 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -1,6 +1,6 @@ { "name": "{{name}}", - "version": "{{version}}", + "version": "{{pluginVersion}}", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,8 +23,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^{{backstageVersion}}", - "@backstage/config": "^{{backstageVersion}}", + "@backstage/backend-common": "^{{version '@backstage/backend-common'}}", + "@backstage/config": "^{{version '@backstage/config'}}", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^3.0.3", @@ -33,7 +33,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^{{backstageVersion}}", + "@backstage/cli": "^{{version '@backstage/cli'}}", "@types/supertest": "^2.0.8", "supertest": "^4.0.2", "msw": "^0.21.2" diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index 41f942fb79..a3f924408a 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -1,6 +1,6 @@ { "name": "{{name}}", - "version": "{{version}}", + "version": "{{pluginVersion}}", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,8 +24,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^{{backstageVersion}}", - "@backstage/theme": "^{{backstageVersion}}", + "@backstage/core": "^{{version '@backstage/core'}}", + "@backstage/theme": "^{{version '@backstage/theme'}}", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -34,9 +34,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^{{backstageVersion}}", - "@backstage/dev-utils": "^{{backstageVersion}}", - "@backstage/test-utils": "^{{backstageVersion}}", + "@backstage/cli": "^{{version '@backstage/cli'}}", + "@backstage/dev-utils": "^{{version '@backstage/dev-utils'}}", + "@backstage/test-utils": "^{{version '@backstage/test-utils'}}", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", From 0efbdfbbb861d52d0e1331bb3e42377132d6c94b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 27 Oct 2020 19:09:08 +0100 Subject: [PATCH 013/198] cli,create-app: refactor package versions handling + fix tasks test --- packages/cli/src/commands/create-plugin/createPlugin.ts | 4 ++-- packages/cli/src/lib/diff/read.ts | 8 ++++---- packages/cli/src/lib/tasks.test.ts | 2 +- packages/cli/src/lib/version.ts | 2 +- packages/create-app/src/createApp.ts | 3 +-- packages/create-app/src/lib/tasks.ts | 8 ++++---- packages/create-app/src/{ => lib}/versions.ts | 2 +- 7 files changed, 14 insertions(+), 15 deletions(-) rename packages/create-app/src/{ => lib}/versions.ts (99%) diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index b603e732b8..f54a62f613 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -28,7 +28,7 @@ import { getCodeownersFilePath, } from '../../lib/codeowners'; import { paths } from '../../lib/paths'; -import { versions } from '../../lib/version'; +import { packageVersions } from '../../lib/version'; import { Task, templatingTask } from '../../lib/tasks'; const exec = promisify(execCb); @@ -269,7 +269,7 @@ export default async (cmd: Command) => { privatePackage, npmRegistry, }, - versions, + packageVersions, ); Task.section('Moving to final location'); diff --git a/packages/cli/src/lib/diff/read.ts b/packages/cli/src/lib/diff/read.ts index a4a7355d2d..d37dfe4b03 100644 --- a/packages/cli/src/lib/diff/read.ts +++ b/packages/cli/src/lib/diff/read.ts @@ -24,7 +24,7 @@ import handlebars from 'handlebars'; import recursiveReadDir from 'recursive-readdir'; import { paths } from '../paths'; import { FileDiff } from './types'; -import { versions } from '../../lib/version'; +import { packageVersions } from '../../lib/version'; export type TemplatedFile = { path: string; @@ -43,9 +43,9 @@ async function readTemplateFile( return handlebars.compile(contents)(templateVars, { helpers: { - version(name: string) { - if (versions[name]) { - return versions[name]; + version(name: keyof typeof packageVersions) { + if (name in packageVersions) { + return packageVersions[name]; } throw new Error(`No version available for package ${name}`); }, diff --git a/packages/cli/src/lib/tasks.test.ts b/packages/cli/src/lib/tasks.test.ts index a42902c60b..88d8050290 100644 --- a/packages/cli/src/lib/tasks.test.ts +++ b/packages/cli/src/lib/tasks.test.ts @@ -50,7 +50,7 @@ describe('templatingTask', () => { tmplDir, destDir, { - version: '0.0.0', + pluginVersion: '0.0.0', }, { 'mock-pkg': '0.1.2' }, ); diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/lib/version.ts index 5cc3eb7fbb..b2b793717d 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/lib/version.ts @@ -39,7 +39,7 @@ import { version as devUtils } from '@backstage/dev-utils/package.json'; import { version as testUtils } from '@backstage/test-utils/package.json'; import { version as theme } from '@backstage/theme/package.json'; -export const versions: { [name: string]: string } = { +export const packageVersions = { '@backstage/backend-common': backendCommon, '@backstage/cli': cli, '@backstage/config': config, diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index c0874d4032..775d53dfdc 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -22,7 +22,6 @@ import inquirer, { Answers, Question } from 'inquirer'; import { exec as execCb } from 'child_process'; import { resolve as resolvePath } from 'path'; import { findPaths } from '@backstage/cli-common'; -import { versions } from './versions'; import os from 'os'; import { Task, templatingTask } from './lib/tasks'; @@ -133,7 +132,7 @@ export default async (cmd: Command): Promise => { await createTemporaryAppFolder(tempDir); Task.section('Preparing files'); - await templatingTask(templateDir, tempDir, answers, versions); + await templatingTask(templateDir, tempDir, answers); Task.section('Moving to final location'); await moveApp(tempDir, appDir, answers.name); diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 73db175709..b615692830 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -20,6 +20,7 @@ import handlebars from 'handlebars'; import ora from 'ora'; import { basename, dirname } from 'path'; import recursive from 'recursive-readdir'; +import { packageVersions } from './versions'; const TASK_NAME_MAX_LENGTH = 14; @@ -68,7 +69,6 @@ export async function templatingTask( templateDir: string, destinationDir: string, context: any, - versions: { [name: string]: string }, ) { const files = await recursive(templateDir).catch(error => { throw new Error(`Failed to read template directory: ${error.message}`); @@ -88,9 +88,9 @@ export async function templatingTask( { name: basename(destination), ...context }, { helpers: { - version(name: string) { - if (versions[name]) { - return versions[name]; + version(name: keyof typeof packageVersions) { + if (name in packageVersions) { + return packageVersions[name]; } throw new Error(`No version available for package ${name}`); }, diff --git a/packages/create-app/src/versions.ts b/packages/create-app/src/lib/versions.ts similarity index 99% rename from packages/create-app/src/versions.ts rename to packages/create-app/src/lib/versions.ts index ed03903469..196e0004c1 100644 --- a/packages/create-app/src/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -54,7 +54,7 @@ import { version as pluginUserSettings } from '@backstage/plugin-user-settings/p import { version as testUtils } from '@backstage/test-utils/package.json'; import { version as theme } from '@backstage/theme/package.json'; -export const versions = { +export const packageVersions = { '@backstage/backend-common': backendCommon, '@backstage/catalog-model': catalogModel, '@backstage/cli': cli, From 24c05700829d17708a34c9c9711002bd359bc055 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 27 Oct 2020 19:12:16 +0100 Subject: [PATCH 014/198] e2e-test: update to provide version lookup helper to template --- packages/e2e-test/src/commands/run.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 905cb20d52..be00b99e54 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -84,11 +84,23 @@ async function buildDistWorkspace(workspaceName: string, rootDir: string) { const path = paths.resolveOwnRoot(pkgJsonPath); const pkgTemplate = await fs.readFile(path, 'utf8'); const { dependencies = {}, devDependencies = {} } = JSON.parse( - handlebars.compile(pkgTemplate)({ - version: '0.0.0', - privatePackage: true, - scopeName: '@backstage', - }), + handlebars.compile(pkgTemplate)( + { + privatePackage: true, + scopeName: '@backstage', + }, + { + helpers: { + version(name: string) { + const pkg = require(`${name}/package.json`); + if (!pkg) { + throw new Error(`No version available for package ${name}`); + } + return pkg.version; + }, + }, + }, + ), ); Array() From 41ad8904a3a81af3b84de3b0951245b56bbbbb73 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 28 Oct 2020 01:04:22 +0100 Subject: [PATCH 015/198] cli: fix a derpendency --- packages/cli/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 89c0520b59..74fc2d0968 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -109,7 +109,6 @@ }, "devDependencies": { "@backstage/backend-common": "^0.1.1-alpha.25", - "@backstage/cli": "^0.1.1-alpha.25", "@backstage/config": "^0.1.1-alpha.25", "@backstage/core": "^0.1.1-alpha.25", "@backstage/dev-utils": "^0.1.1-alpha.25", From 1e7af7c07e7ce19f36947e73cbf4e29ec79e0e42 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 28 Oct 2020 01:18:03 +0100 Subject: [PATCH 016/198] cli,create-app: bump dev deps --- packages/cli/package.json | 12 ++++---- packages/create-app/package.json | 48 ++++++++++++++++---------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 74fc2d0968..668ec5e000 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -108,12 +108,12 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.1.1-alpha.25", - "@backstage/config": "^0.1.1-alpha.25", - "@backstage/core": "^0.1.1-alpha.25", - "@backstage/dev-utils": "^0.1.1-alpha.25", - "@backstage/test-utils": "^0.1.1-alpha.25", - "@backstage/theme": "^0.1.1-alpha.25", + "@backstage/backend-common": "^0.1.1-alpha.26", + "@backstage/config": "^0.1.1-alpha.26", + "@backstage/core": "^0.1.1-alpha.26", + "@backstage/dev-utils": "^0.1.1-alpha.26", + "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/theme": "^0.1.1-alpha.26", "@types/diff": "^4.0.2", "@types/fs-extra": "^9.0.1", "@types/html-webpack-plugin": "^3.2.2", diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 0dd8897d0b..bb321dc504 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -37,30 +37,30 @@ "recursive-readdir": "^2.2.2" }, "devDependencies": { - "@backstage/backend-common": "^0.1.1-alpha.25", - "@backstage/catalog-model": "^0.1.1-alpha.25", - "@backstage/cli": "^0.1.1-alpha.25", - "@backstage/config": "^0.1.1-alpha.25", - "@backstage/core": "^0.1.1-alpha.25", - "@backstage/plugin-api-docs": "^0.1.1-alpha.25", - "@backstage/plugin-auth-backend": "^0.1.1-alpha.25", - "@backstage/plugin-catalog": "^0.1.1-alpha.25", - "@backstage/plugin-catalog-backend": "^0.1.1-alpha.25", - "@backstage/plugin-circleci": "^0.1.1-alpha.25", - "@backstage/plugin-explore": "^0.1.1-alpha.25", - "@backstage/plugin-github-actions": "^0.1.1-alpha.25", - "@backstage/plugin-lighthouse": "^0.1.1-alpha.25", - "@backstage/plugin-proxy-backend": "^0.1.1-alpha.25", - "@backstage/plugin-register-component": "^0.1.1-alpha.25", - "@backstage/plugin-rollbar-backend": "^0.1.1-alpha.25", - "@backstage/plugin-scaffolder": "^0.1.1-alpha.25", - "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.25", - "@backstage/plugin-tech-radar": "^0.1.1-alpha.25", - "@backstage/plugin-techdocs": "^0.1.1-alpha.25", - "@backstage/plugin-techdocs-backend": "^0.1.1-alpha.25", - "@backstage/plugin-user-settings": "^0.1.1-alpha.25", - "@backstage/test-utils": "^0.1.1-alpha.25", - "@backstage/theme": "^0.1.1-alpha.25", + "@backstage/backend-common": "^0.1.1-alpha.26", + "@backstage/catalog-model": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1-alpha.26", + "@backstage/config": "^0.1.1-alpha.26", + "@backstage/core": "^0.1.1-alpha.26", + "@backstage/plugin-api-docs": "^0.1.1-alpha.26", + "@backstage/plugin-auth-backend": "^0.1.1-alpha.26", + "@backstage/plugin-catalog": "^0.1.1-alpha.26", + "@backstage/plugin-catalog-backend": "^0.1.1-alpha.26", + "@backstage/plugin-circleci": "^0.1.1-alpha.26", + "@backstage/plugin-explore": "^0.1.1-alpha.26", + "@backstage/plugin-github-actions": "^0.1.1-alpha.26", + "@backstage/plugin-lighthouse": "^0.1.1-alpha.26", + "@backstage/plugin-proxy-backend": "^0.1.1-alpha.26", + "@backstage/plugin-register-component": "^0.1.1-alpha.26", + "@backstage/plugin-rollbar-backend": "^0.1.1-alpha.26", + "@backstage/plugin-scaffolder": "^0.1.1-alpha.26", + "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.26", + "@backstage/plugin-tech-radar": "^0.1.1-alpha.26", + "@backstage/plugin-techdocs": "^0.1.1-alpha.26", + "@backstage/plugin-techdocs-backend": "^0.1.1-alpha.26", + "@backstage/plugin-user-settings": "^0.1.1-alpha.26", + "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/theme": "^0.1.1-alpha.26", "@types/fs-extra": "^9.0.1", "@types/inquirer": "^7.3.1", "@types/ora": "^3.2.0", From 097e3d222a61889c029c109d9693f8ec298ada0d Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Wed, 28 Oct 2020 10:54:38 +0530 Subject: [PATCH 017/198] fix: error handling --- plugins/scaffolder-backend/src/service/router.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 83a0cbd398..efe8ee46f6 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -115,10 +115,8 @@ export async function createRouter( let template: TemplateEntityV1alpha1; try { template = await entityClient.findTemplate(templateName); - } catch (e) { - // help me here - res.status(400).json({ errors: e }); - return; + } catch (err) { + throw err; } const validationResult: ValidatorResult = validate( From e3d063ffa808256b906f0cddbdb9c17fe6d5e5dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Frinnstr=C3=B6m?= Date: Wed, 28 Oct 2020 07:12:59 +0100 Subject: [PATCH 018/198] Add changeset --- .changeset/three-horses-juggle.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/three-horses-juggle.md diff --git a/.changeset/three-horses-juggle.md b/.changeset/three-horses-juggle.md new file mode 100644 index 0000000000..bebcf77e43 --- /dev/null +++ b/.changeset/three-horses-juggle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Introduce PreparerOptions for PreparerBase From 833a652d055a8dafa21a5bda57b8a404fab81696 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 28 Oct 2020 10:20:43 +0100 Subject: [PATCH 019/198] v0.1.1 --- lerna.json | 2 +- packages/app/package.json | 56 ++++++++++++------------- packages/backend-common/package.json | 12 +++--- packages/backend/package.json | 32 +++++++------- packages/catalog-model/package.json | 6 +-- packages/cli-common/package.json | 2 +- packages/cli/package.json | 20 ++++----- packages/config-loader/package.json | 4 +- packages/config/package.json | 2 +- packages/core-api/package.json | 12 +++--- packages/core/package.json | 12 +++--- packages/create-app/package.json | 52 +++++++++++------------ packages/dev-utils/package.json | 10 ++--- packages/docgen/package.json | 2 +- packages/e2e-test/package.json | 4 +- packages/storybook/package.json | 4 +- packages/techdocs-cli/package.json | 4 +- packages/test-utils-core/package.json | 2 +- packages/test-utils/package.json | 10 ++--- packages/theme/package.json | 4 +- plugins/api-docs/package.json | 16 +++---- plugins/app-backend/package.json | 8 ++-- plugins/auth-backend/package.json | 10 ++--- plugins/catalog-backend/package.json | 12 +++--- plugins/catalog-graphql/package.json | 12 +++--- plugins/catalog/package.json | 18 ++++---- plugins/circleci/package.json | 16 +++---- plugins/cloudbuild/package.json | 16 +++---- plugins/cost-insights/package.json | 16 +++---- plugins/explore/package.json | 12 +++--- plugins/gcp-projects/package.json | 12 +++--- plugins/github-actions/package.json | 18 ++++---- plugins/gitops-profiles/package.json | 12 +++--- plugins/graphiql/package.json | 12 +++--- plugins/graphql/package.json | 10 ++--- plugins/jenkins/package.json | 16 +++---- plugins/kubernetes-backend/package.json | 8 ++-- plugins/kubernetes/package.json | 18 ++++---- plugins/lighthouse/package.json | 20 ++++----- plugins/newrelic/package.json | 12 +++--- plugins/proxy-backend/package.json | 8 ++-- plugins/register-component/package.json | 16 +++---- plugins/rollbar-backend/package.json | 8 ++-- plugins/rollbar/package.json | 16 +++---- plugins/scaffolder-backend/package.json | 10 ++--- plugins/scaffolder/package.json | 16 +++---- plugins/sentry-backend/package.json | 6 +-- plugins/sentry/package.json | 14 +++---- plugins/tech-radar/package.json | 14 +++---- plugins/techdocs-backend/package.json | 10 ++--- plugins/techdocs/package.json | 20 ++++----- plugins/user-settings/package.json | 12 +++--- plugins/welcome/package.json | 12 +++--- 53 files changed, 344 insertions(+), 344 deletions(-) diff --git a/lerna.json b/lerna.json index 5871423f78..dd2dd884eb 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,5 @@ "packages": ["packages/*", "plugins/*"], "npmClient": "yarn", "useWorkspaces": true, - "version": "0.1.1-alpha.26" + "version": "0.1.1" } diff --git a/packages/app/package.json b/packages/app/package.json index 594c12b5e5..5c0b90f0fe 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,36 +1,36 @@ { "name": "example-app", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "private": true, "bundled": true, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/plugin-api-docs": "^0.1.1-alpha.26", - "@backstage/plugin-catalog": "^0.1.1-alpha.26", - "@backstage/plugin-circleci": "^0.1.1-alpha.26", - "@backstage/plugin-cloudbuild": "^0.1.1-alpha.26", - "@backstage/plugin-cost-insights": "^0.1.1-alpha.26", - "@backstage/plugin-explore": "^0.1.1-alpha.26", - "@backstage/plugin-gcp-projects": "^0.1.1-alpha.26", - "@backstage/plugin-github-actions": "^0.1.1-alpha.26", - "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.26", - "@backstage/plugin-graphiql": "^0.1.1-alpha.26", - "@backstage/plugin-jenkins": "^0.1.1-alpha.26", - "@backstage/plugin-kubernetes": "^0.1.1-alpha.26", - "@backstage/plugin-lighthouse": "^0.1.1-alpha.26", - "@backstage/plugin-newrelic": "^0.1.1-alpha.26", - "@backstage/plugin-register-component": "^0.1.1-alpha.26", - "@backstage/plugin-rollbar": "^0.1.1-alpha.26", - "@backstage/plugin-scaffolder": "^0.1.1-alpha.26", - "@backstage/plugin-sentry": "^0.1.1-alpha.26", - "@backstage/plugin-tech-radar": "^0.1.1-alpha.26", - "@backstage/plugin-techdocs": "^0.1.1-alpha.26", - "@backstage/plugin-user-settings": "^0.1.1-alpha.26", - "@backstage/plugin-welcome": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/catalog-model": "^0.1.1", + "@backstage/cli": "^0.1.1", + "@backstage/core": "^0.1.1", + "@backstage/plugin-api-docs": "^0.1.1", + "@backstage/plugin-catalog": "^0.1.1", + "@backstage/plugin-circleci": "^0.1.1", + "@backstage/plugin-cloudbuild": "^0.1.1", + "@backstage/plugin-cost-insights": "^0.1.1", + "@backstage/plugin-explore": "^0.1.1", + "@backstage/plugin-gcp-projects": "^0.1.1", + "@backstage/plugin-github-actions": "^0.1.1", + "@backstage/plugin-gitops-profiles": "^0.1.1", + "@backstage/plugin-graphiql": "^0.1.1", + "@backstage/plugin-jenkins": "^0.1.1", + "@backstage/plugin-kubernetes": "^0.1.1", + "@backstage/plugin-lighthouse": "^0.1.1", + "@backstage/plugin-newrelic": "^0.1.1", + "@backstage/plugin-register-component": "^0.1.1", + "@backstage/plugin-rollbar": "^0.1.1", + "@backstage/plugin-scaffolder": "^0.1.1", + "@backstage/plugin-sentry": "^0.1.1", + "@backstage/plugin-tech-radar": "^0.1.1", + "@backstage/plugin-techdocs": "^0.1.1", + "@backstage/plugin-user-settings": "^0.1.1", + "@backstage/plugin-welcome": "^0.1.1", + "@backstage/test-utils": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@octokit/rest": "^18.0.0", diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index dea2f16bae..990c8597ce 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli-common": "^0.1.1-alpha.26", - "@backstage/config": "^0.1.1-alpha.26", - "@backstage/config-loader": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli-common": "^0.1.1", + "@backstage/config": "^0.1.1", + "@backstage/config-loader": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@types/cors": "^2.8.6", "@types/express": "^4.17.6", "compression": "^1.7.4", @@ -62,7 +62,7 @@ } }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", "@types/compression": "^1.7.0", "@types/http-errors": "^1.6.3", "@types/minimist": "^1.2.0", diff --git a/packages/backend/package.json b/packages/backend/package.json index 9f8e66bc61..76d8861717 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "dist/index.cjs.js", "types": "src/index.ts", "private": true, @@ -18,24 +18,24 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.26", - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/config": "^0.1.1-alpha.26", - "@backstage/plugin-app-backend": "^0.1.1-alpha.26", - "@backstage/plugin-auth-backend": "^0.1.1-alpha.26", - "@backstage/plugin-catalog-backend": "^0.1.1-alpha.26", - "@backstage/plugin-graphql-backend": "^0.1.1-alpha.26", - "@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.26", - "@backstage/plugin-proxy-backend": "^0.1.1-alpha.26", - "@backstage/plugin-rollbar-backend": "^0.1.1-alpha.26", - "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.26", - "@backstage/plugin-sentry-backend": "^0.1.1-alpha.26", - "@backstage/plugin-techdocs-backend": "^0.1.1-alpha.26", + "@backstage/backend-common": "^0.1.1", + "@backstage/catalog-model": "^0.1.1", + "@backstage/config": "^0.1.1", + "@backstage/plugin-app-backend": "^0.1.1", + "@backstage/plugin-auth-backend": "^0.1.1", + "@backstage/plugin-catalog-backend": "^0.1.1", + "@backstage/plugin-graphql-backend": "^0.1.1", + "@backstage/plugin-kubernetes-backend": "^0.1.1", + "@backstage/plugin-proxy-backend": "^0.1.1", + "@backstage/plugin-rollbar-backend": "^0.1.1", + "@backstage/plugin-scaffolder-backend": "^0.1.1", + "@backstage/plugin-sentry-backend": "^0.1.1", + "@backstage/plugin-techdocs-backend": "^0.1.1", "@gitbeaker/node": "^23.5.0", "@octokit/rest": "^18.0.0", "azure-devops-node-api": "^10.1.1", "dockerode": "^3.2.0", - "example-app": "^0.1.1-alpha.26", + "example-app": "^0.1.1", "express": "^4.17.1", "express-promise-router": "^3.0.3", "knex": "^0.21.1", @@ -45,7 +45,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", "@types/dockerode": "^2.5.32", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5", diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index c97aea9572..0d555ef5d2 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.26", + "@backstage/config": "^0.1.1", "@types/json-schema": "^7.0.5", "@types/yup": "^0.29.8", "json-schema": "^0.2.5", @@ -29,7 +29,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", "@types/express": "^4.17.6", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", diff --git a/packages/cli-common/package.json b/packages/cli-common/package.json index 56fb0d3417..2186f3d045 100644 --- a/packages/cli-common/package.json +++ b/packages/cli-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli-common", "description": "Common functionality used by cli, backend, and create-app", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "private": false, "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/cli/package.json b/packages/cli/package.json index 668ec5e000..4ad33c997b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "private": false, "publishConfig": { "access": "public" @@ -28,9 +28,9 @@ "backstage-cli": "bin/backstage-cli" }, "dependencies": { - "@backstage/cli-common": "^0.1.1-alpha.26", - "@backstage/config": "^0.1.1-alpha.26", - "@backstage/config-loader": "^0.1.1-alpha.26", + "@backstage/cli-common": "^0.1.1", + "@backstage/config": "^0.1.1", + "@backstage/config-loader": "^0.1.1", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^3.18.5", "@lerna/project": "^3.18.0", @@ -108,12 +108,12 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.1.1-alpha.26", - "@backstage/config": "^0.1.1-alpha.26", - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/backend-common": "^0.1.1", + "@backstage/config": "^0.1.1", + "@backstage/core": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", + "@backstage/theme": "^0.1.1", "@types/diff": "^4.0.2", "@types/fs-extra": "^9.0.1", "@types/html-webpack-plugin": "^3.2.2", diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 2eef7db027..8c43832ab6 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "private": false, "publishConfig": { "access": "public", @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.26", + "@backstage/config": "^0.1.1", "fs-extra": "^9.0.0", "yaml": "^1.9.2", "yup": "^0.29.3" diff --git a/packages/config/package.json b/packages/config/package.json index d8ee50e97c..4bd4bedf5d 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config", "description": "Config API used by Backstage core, backend, and CLI", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "private": false, "publishConfig": { "access": "public", diff --git a/packages/core-api/package.json b/packages/core-api/package.json index 34949e4315..d183ce9433 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-api", "description": "Internal Core API used by Backstage plugins and apps", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "private": false, "publishConfig": { "access": "public", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/config": "^0.1.1", + "@backstage/test-utils": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@types/react": "^16.9", @@ -42,8 +42,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/test-utils-core": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/test-utils-core": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/packages/core/package.json b/packages/core/package.json index ec4411b390..535fb856cf 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core", "description": "Core API used by Backstage plugins and apps", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "private": false, "publishConfig": { "access": "public", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.26", - "@backstage/core-api": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/config": "^0.1.1", + "@backstage/core-api": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -55,8 +55,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/packages/create-app/package.json b/packages/create-app/package.json index bb321dc504..4362fc7cff 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "Create app package for Backstage", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "private": false, "publishConfig": { "access": "public" @@ -27,7 +27,7 @@ "start": "nodemon --" }, "dependencies": { - "@backstage/cli-common": "^0.1.1-alpha.26", + "@backstage/cli-common": "^0.1.1", "chalk": "^4.0.0", "commander": "^6.1.0", "fs-extra": "^9.0.0", @@ -37,30 +37,30 @@ "recursive-readdir": "^2.2.2" }, "devDependencies": { - "@backstage/backend-common": "^0.1.1-alpha.26", - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/config": "^0.1.1-alpha.26", - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/plugin-api-docs": "^0.1.1-alpha.26", - "@backstage/plugin-auth-backend": "^0.1.1-alpha.26", - "@backstage/plugin-catalog": "^0.1.1-alpha.26", - "@backstage/plugin-catalog-backend": "^0.1.1-alpha.26", - "@backstage/plugin-circleci": "^0.1.1-alpha.26", - "@backstage/plugin-explore": "^0.1.1-alpha.26", - "@backstage/plugin-github-actions": "^0.1.1-alpha.26", - "@backstage/plugin-lighthouse": "^0.1.1-alpha.26", - "@backstage/plugin-proxy-backend": "^0.1.1-alpha.26", - "@backstage/plugin-register-component": "^0.1.1-alpha.26", - "@backstage/plugin-rollbar-backend": "^0.1.1-alpha.26", - "@backstage/plugin-scaffolder": "^0.1.1-alpha.26", - "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.26", - "@backstage/plugin-tech-radar": "^0.1.1-alpha.26", - "@backstage/plugin-techdocs": "^0.1.1-alpha.26", - "@backstage/plugin-techdocs-backend": "^0.1.1-alpha.26", - "@backstage/plugin-user-settings": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/backend-common": "^0.1.1", + "@backstage/catalog-model": "^0.1.1", + "@backstage/cli": "^0.1.1", + "@backstage/config": "^0.1.1", + "@backstage/core": "^0.1.1", + "@backstage/plugin-api-docs": "^0.1.1", + "@backstage/plugin-auth-backend": "^0.1.1", + "@backstage/plugin-catalog": "^0.1.1", + "@backstage/plugin-catalog-backend": "^0.1.1", + "@backstage/plugin-circleci": "^0.1.1", + "@backstage/plugin-explore": "^0.1.1", + "@backstage/plugin-github-actions": "^0.1.1", + "@backstage/plugin-lighthouse": "^0.1.1", + "@backstage/plugin-proxy-backend": "^0.1.1", + "@backstage/plugin-register-component": "^0.1.1", + "@backstage/plugin-rollbar-backend": "^0.1.1", + "@backstage/plugin-scaffolder": "^0.1.1", + "@backstage/plugin-scaffolder-backend": "^0.1.1", + "@backstage/plugin-tech-radar": "^0.1.1", + "@backstage/plugin-techdocs": "^0.1.1", + "@backstage/plugin-techdocs-backend": "^0.1.1", + "@backstage/plugin-user-settings": "^0.1.1", + "@backstage/test-utils": "^0.1.1", + "@backstage/theme": "^0.1.1", "@types/fs-extra": "^9.0.1", "@types/inquirer": "^7.3.1", "@types/ora": "^3.2.0", diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index d61336465f..b8420d17b1 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "private": false, "publishConfig": { "access": "public", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/core": "^0.1.1", + "@backstage/test-utils": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/docgen/package.json b/packages/docgen/package.json index 18f26d6941..db045baedb 100644 --- a/packages/docgen/package.json +++ b/packages/docgen/package.json @@ -1,7 +1,7 @@ { "name": "docgen", "description": "Tool for generating API Documentation for itself", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "private": true, "homepage": "https://backstage.io", "repository": { diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 9098b3cd07..0420b426c6 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -1,7 +1,7 @@ { "name": "e2e-test", "description": "E2E test for verifying Backstage packages", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "private": true, "homepage": "https://backstage.io", "repository": { @@ -24,7 +24,7 @@ "e2e-test": "bin/e2e-test" }, "devDependencies": { - "@backstage/cli-common": "^0.1.1-alpha.26", + "@backstage/cli-common": "^0.1.1", "@types/fs-extra": "^9.0.1", "@types/node": "^13.7.2", "chalk": "^4.0.0", diff --git a/packages/storybook/package.json b/packages/storybook/package.json index 9eefd85128..4c9cc88dd0 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -1,6 +1,6 @@ { "name": "storybook", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "description": "Storybook build for core package", "private": true, "scripts": { @@ -14,7 +14,7 @@ ] }, "dependencies": { - "@backstage/theme": "^0.1.1-alpha.26" + "@backstage/theme": "^0.1.1" }, "devDependencies": { "@storybook/addon-actions": "^6.0.21", diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index c111a2973a..a81d4b9044 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,7 +1,7 @@ { "name": "@techdocs/cli", "description": "CLI for running TechDocs locally.", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "private": false, "publishConfig": { "access": "public" @@ -40,7 +40,7 @@ "ext": "ts" }, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", "commander": "^6.1.0", "fs-extra": "^9.0.1", "http-proxy": "^1.18.1", diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index 1db8f6f023..1b69f48b08 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils-core", "description": "Utilities to test Backstage core", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "private": false, "publishConfig": { "access": "public", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 74e0c8dcf7..cf05dd583c 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "private": false, "publishConfig": { "access": "public", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/core-api": "^0.1.1-alpha.26", - "@backstage/test-utils-core": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/core-api": "^0.1.1", + "@backstage/test-utils-core": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/packages/theme/package.json b/packages/theme/package.json index ddec0e7981..47b0c497eb 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/theme", "description": "material-ui theme for use with Backstage.", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "private": false, "publishConfig": { "access": "public", @@ -31,7 +31,7 @@ "@material-ui/core": "^4.11.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26" + "@backstage/cli": "^0.1.1" }, "files": [ "dist" diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index baf574d5b5..23c072201e 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,10 +20,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/plugin-catalog": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/catalog-model": "^0.1.1", + "@backstage/core": "^0.1.1", + "@backstage/plugin-catalog": "^0.1.1", + "@backstage/theme": "^0.1.1", "@kyma-project/asyncapi-react": "^0.13.1", "@material-icons/font": "^1.0.2", "@material-ui/core": "^4.11.0", @@ -39,9 +39,9 @@ "swagger-ui-react": "^3.31.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 8297b466af..4e91bf2161 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-backend", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.26", - "@backstage/config-loader": "^0.1.1-alpha.26", + "@backstage/backend-common": "^0.1.1", + "@backstage/config-loader": "^0.1.1", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^3.0.3", @@ -30,7 +30,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", "@types/supertest": "^2.0.8", "msw": "^0.20.5", "supertest": "^4.0.2" diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 6489c8547d..217aaa9e58 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.26", - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/config": "^0.1.1-alpha.26", + "@backstage/backend-common": "^0.1.1", + "@backstage/catalog-model": "^0.1.1", + "@backstage/config": "^0.1.1", "@types/express": "^4.17.6", "compression": "^1.7.4", "cookie-parser": "^1.4.5", @@ -51,7 +51,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/jwt-decode": "2.2.1", diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index cd8e363901..ef1401d4d3 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.26", - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/config": "^0.1.1-alpha.26", + "@backstage/backend-common": "^0.1.1", + "@backstage/catalog-model": "^0.1.1", + "@backstage/config": "^0.1.1", "@octokit/graphql": "^4.5.6", "@types/express": "^4.17.6", "codeowners-utils": "^1.0.2", @@ -45,8 +45,8 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", "@types/ldapjs": "^1.0.9", diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index f74aaf7b57..a43f512856 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graphql", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.26", - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/config": "^0.1.1-alpha.26", + "@backstage/backend-common": "^0.1.1", + "@backstage/catalog-model": "^0.1.1", + "@backstage/config": "^0.1.1", "@graphql-modules/core": "^0.7.17", "apollo-server": "^2.16.1", "cross-fetch": "^3.0.6", @@ -32,8 +32,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@graphql-codegen/cli": "^1.17.7", "@graphql-codegen/typescript": "^1.17.7", "@graphql-codegen/typescript-resolvers": "^1.17.7", diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 663ac7554b..da03d1a584 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,11 +21,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/plugin-scaffolder": "^0.1.1-alpha.26", - "@backstage/plugin-techdocs": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/catalog-model": "^0.1.1", + "@backstage/core": "^0.1.1", + "@backstage/plugin-scaffolder": "^0.1.1", + "@backstage/plugin-techdocs": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -41,9 +41,9 @@ "swr": "^0.3.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/react-hooks": "^3.3.0", diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 7733d713ab..dc09713024 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-circleci", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "postpack": "backstage-cli postpack" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/plugin-catalog": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/catalog-model": "^0.1.1", + "@backstage/core": "^0.1.1", + "@backstage/plugin-catalog": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -38,9 +38,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 29bfd1b261..f29f30f255 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cloudbuild", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,10 +20,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/plugin-catalog": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/catalog-model": "^0.1.1", + "@backstage/core": "^0.1.1", + "@backstage/plugin-catalog": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -39,9 +39,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index abefb601d2..f978e13a8c 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cost-insights", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.26", - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/config": "^0.1.1", + "@backstage/core": "^0.1.1", + "@backstage/test-utils": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -46,9 +46,9 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 4db68909b9..fa682e8dde 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/core": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -33,9 +33,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 4c6375ebf6..86b525f1c9 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gcp-projects", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/core": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -31,9 +31,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 75fed63465..ea5d8c4278 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-actions", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,11 +21,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/core-api": "^0.1.1-alpha.26", - "@backstage/plugin-catalog": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/catalog-model": "^0.1.1", + "@backstage/core": "^0.1.1", + "@backstage/core-api": "^0.1.1", + "@backstage/plugin-catalog": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -40,9 +40,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 18486bc2b7..714c63da20 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gitops-profiles", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/core": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -32,9 +32,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 9e1ffd5ef1..ea61845e18 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphiql", "description": "Backstage plugin for browsing GraphQL APIs", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "private": false, "publishConfig": { "access": "public", @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/core": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -43,9 +43,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/graphql/package.json b/plugins/graphql/package.json index 58b6c763c6..16c3572251 100644 --- a/plugins/graphql/package.json +++ b/plugins/graphql/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-graphql-backend", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,9 +19,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.26", - "@backstage/config": "^0.1.1-alpha.26", - "@backstage/plugin-catalog-graphql": "^0.1.1-alpha.26", + "@backstage/backend-common": "^0.1.1", + "@backstage/config": "^0.1.1", + "@backstage/plugin-catalog-graphql": "^0.1.1", "@graphql-modules/core": "^0.7.17", "@types/express": "^4.17.6", "apollo-server": "^2.16.1", @@ -35,7 +35,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", "@types/supertest": "^2.0.8", "eslint-plugin-graphql": "^4.0.0", "msw": "^0.20.5", diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 194ae71429..cbf9791809 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/plugin-catalog": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/catalog-model": "^0.1.1", + "@backstage/core": "^0.1.1", + "@backstage/plugin-catalog": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -36,9 +36,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 26cc842db9..c66c364871 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-backend", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.26", - "@backstage/config": "^0.1.1-alpha.26", + "@backstage/backend-common": "^0.1.1", + "@backstage/config": "^0.1.1", "@kubernetes/client-node": "^0.12.1", "@types/express": "^4.17.6", "compression": "^1.7.4", @@ -37,7 +37,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 6544d751e1..850dd4cada 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,11 +20,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/config": "^0.1.1-alpha.26", - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/catalog-model": "^0.1.1", + "@backstage/config": "^0.1.1", + "@backstage/core": "^0.1.1", + "@backstage/plugin-kubernetes-backend": "^0.1.1", + "@backstage/theme": "^0.1.1", "@kubernetes/client-node": "^0.12.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -35,9 +35,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 8285d29635..587d826d9d 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-lighthouse", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,12 +21,12 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/config": "^0.1.1-alpha.26", - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/core-api": "^0.1.1-alpha.26", - "@backstage/plugin-catalog": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/catalog-model": "^0.1.1", + "@backstage/config": "^0.1.1", + "@backstage/core": "^0.1.1", + "@backstage/core-api": "^0.1.1", + "@backstage/plugin-catalog": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -39,9 +39,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index b9a12cc648..0b52220239 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-newrelic", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/core": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -31,9 +31,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index fcb68dcd78..57594cfe3f 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-proxy-backend", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,8 +19,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.26", - "@backstage/config": "^0.1.1-alpha.26", + "@backstage/backend-common": "^0.1.1", + "@backstage/config": "^0.1.1", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^3.0.3", @@ -33,7 +33,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", "@types/http-proxy-middleware": "^0.19.3", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 5c3f985b4d..58d3b122ca 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-register-component", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/plugin-catalog": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/catalog-model": "^0.1.1", + "@backstage/core": "^0.1.1", + "@backstage/plugin-catalog": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -36,9 +36,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index cb553d859b..91e0000d6c 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-rollbar-backend", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.26", - "@backstage/config": "^0.1.1-alpha.26", + "@backstage/backend-common": "^0.1.1", + "@backstage/config": "^0.1.1", "@types/express": "^4.17.6", "axios": "^0.20.0", "camelcase-keys": "^6.2.2", @@ -37,7 +37,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", "@types/supertest": "^2.0.8", "supertest": "^4.0.2" }, diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 27421cfd4d..a3b11ace55 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-rollbar", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/plugin-catalog": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/catalog-model": "^0.1.1", + "@backstage/core": "^0.1.1", + "@backstage/plugin-catalog": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -37,9 +37,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/react-hooks": "^3.3.0", diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 1c85adc23a..676ddc9b8a 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.26", - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/config": "^0.1.1-alpha.26", + "@backstage/backend-common": "^0.1.1", + "@backstage/catalog-model": "^0.1.1", + "@backstage/config": "^0.1.1", "@gitbeaker/core": "^23.5.0", "@gitbeaker/node": "^23.5.0", "@octokit/rest": "^18.0.0", @@ -47,7 +47,7 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", "@octokit/types": "^5.4.1", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 751f4075b2..4f7dbdae20 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/plugin-catalog": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/catalog-model": "^0.1.1", + "@backstage/core": "^0.1.1", + "@backstage/plugin-catalog": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -41,9 +41,9 @@ "swr": "^0.3.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/sentry-backend/package.json b/plugins/sentry-backend/package.json index b51eb1509a..cf9bf7886f 100644 --- a/plugins/sentry-backend/package.json +++ b/plugins/sentry-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry-backend", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.26", + "@backstage/backend-common": "^0.1.1", "@types/express": "^4.17.6", "axios": "^0.20.0", "compression": "^1.7.4", @@ -34,7 +34,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26" + "@backstage/cli": "^0.1.1" }, "files": [ "dist" diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 4d9cd59bc3..69c6f130eb 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/catalog-model": "^0.1.1", + "@backstage/core": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -36,9 +36,9 @@ "timeago.js": "^4.0.2" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index f27f4c1784..c7163cc918 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-radar", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/core": "^0.1.1", + "@backstage/test-utils": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -35,9 +35,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 5bdc7ba282..b65540be97 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.26", - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/config": "^0.1.1-alpha.26", + "@backstage/backend-common": "^0.1.1", + "@backstage/catalog-model": "^0.1.1", + "@backstage/config": "^0.1.1", "@types/dockerode": "^2.5.34", "@types/express": "^4.17.6", "command-exists-promise": "^2.0.2", @@ -37,7 +37,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 9d14f0a919..551932369c 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,12 +22,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.26", - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/core-api": "^0.1.1-alpha.26", - "@backstage/plugin-catalog": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/catalog-model": "^0.1.1", + "@backstage/core": "^0.1.1", + "@backstage/core-api": "^0.1.1", + "@backstage/plugin-catalog": "^0.1.1", + "@backstage/test-utils": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -40,9 +40,9 @@ "sanitize-html": "^1.27.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 6f96d7b2fd..3e67c8cae6 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/core": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -32,9 +32,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 380a4b0a46..846388eaec 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-welcome", - "version": "0.1.1-alpha.26", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -21,8 +21,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.26", - "@backstage/theme": "^0.1.1-alpha.26", + "@backstage/core": "^0.1.1", + "@backstage/theme": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -32,9 +32,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.26", - "@backstage/dev-utils": "^0.1.1-alpha.26", - "@backstage/test-utils": "^0.1.1-alpha.26", + "@backstage/cli": "^0.1.1", + "@backstage/dev-utils": "^0.1.1", + "@backstage/test-utils": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", From cef9f38bc710d3dfe70e69a2aa60954d01f39305 Mon Sep 17 00:00:00 2001 From: Sebastian Qvarfordt Date: Wed, 28 Oct 2020 10:45:52 +0100 Subject: [PATCH 020/198] [TechDocs] Fix broken graphviz (#3139) * Pin version of Markdown package * Bumped techdocs-core package version * Added changelog --- packages/techdocs-container/techdocs-core/README.md | 4 ++++ packages/techdocs-container/techdocs-core/requirements.txt | 1 + packages/techdocs-container/techdocs-core/setup.py | 3 ++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/techdocs-container/techdocs-core/README.md b/packages/techdocs-container/techdocs-core/README.md index 7f34722e81..423a076cbf 100644 --- a/packages/techdocs-container/techdocs-core/README.md +++ b/packages/techdocs-container/techdocs-core/README.md @@ -82,6 +82,10 @@ Extensions: ## Changelog +### 0.0.10 + +- Pin Markdown version to fix issue with Graphviz + ### 0.0.9 - Change development status to 3 - Alpha diff --git a/packages/techdocs-container/techdocs-core/requirements.txt b/packages/techdocs-container/techdocs-core/requirements.txt index 75546fa992..fc56ae6aee 100644 --- a/packages/techdocs-container/techdocs-core/requirements.txt +++ b/packages/techdocs-container/techdocs-core/requirements.txt @@ -8,6 +8,7 @@ plantuml-markdown==3.1.2 markdown_inline_graphviz_extension==1.1 pygments==2.6.1 pymdown-extensions==7.1 +Markdown==3.2.2 # The linter using for Python # Note: This requires Python 3.6+ to run, but can format Python 2 code too. diff --git a/packages/techdocs-container/techdocs-core/setup.py b/packages/techdocs-container/techdocs-core/setup.py index 4385bc2e38..44ff653d72 100644 --- a/packages/techdocs-container/techdocs-core/setup.py +++ b/packages/techdocs-container/techdocs-core/setup.py @@ -23,7 +23,7 @@ with open(path.join(this_dir, "README.md"), encoding="utf-8") as file: setup( name="mkdocs-techdocs-core", - version="0.0.9", + version="0.0.10", description="A Mkdocs package that contains TechDocs defaults", long_description=long_description, long_description_content_type="text/markdown", @@ -41,6 +41,7 @@ setup( "markdown_inline_graphviz_extension==1.1", "pygments==2.6.1", "pymdown-extensions==7.1", + "Markdown==3.2.2", ], classifiers=[ "Development Status :: 3 - Alpha", From f0a6c09f876a3ac996bed1e01f906b1468fb460f Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 28 Oct 2020 10:46:11 +0100 Subject: [PATCH 021/198] Upgrade version of mkdocs-techdocs-core Graphviz issue https://github.com/spotify/backstage/pull/3139 --- packages/techdocs-container/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/techdocs-container/Dockerfile b/packages/techdocs-container/Dockerfile index 49f7940d32..1c1ca264db 100644 --- a/packages/techdocs-container/Dockerfile +++ b/packages/techdocs-container/Dockerfile @@ -1,11 +1,11 @@ # 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. @@ -18,7 +18,7 @@ FROM python:3.8-alpine RUN apk update && apk --no-cache add gcc musl-dev openjdk11-jdk curl graphviz ttf-dejavu fontconfig RUN curl -o plantuml.jar -L http://sourceforge.net/projects/plantuml/files/plantuml.1.2020.16.jar/download && echo "c789ace48347c43073232b1458badc5810c01fe8 plantuml.jar" | sha1sum -c - && mv plantuml.jar /opt/plantuml.jar -RUN pip install --upgrade pip && pip install mkdocs-techdocs-core==0.0.8 +RUN pip install --upgrade pip && pip install mkdocs-techdocs-core==0.0.10 # Create script to call plantuml.jar from a location in path From 4fc1d440e37710d96549f17eb632dc05bc167cbc Mon Sep 17 00:00:00 2001 From: Michael Varrieur Date: Wed, 28 Oct 2020 09:08:20 -0400 Subject: [PATCH 022/198] Add settings button to sidebar (#3135) * Add settings button to UI * Add changeset * Fix prettier issues * Remove user SidebarItem * Fix tsc error --- .changeset/nice-candles-argue.md | 5 +++++ plugins/user-settings/src/components/Settings.tsx | 10 +++------- 2 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 .changeset/nice-candles-argue.md diff --git a/.changeset/nice-candles-argue.md b/.changeset/nice-candles-argue.md new file mode 100644 index 0000000000..971000410a --- /dev/null +++ b/.changeset/nice-candles-argue.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': minor +--- + +Add settings button to sidebar diff --git a/plugins/user-settings/src/components/Settings.tsx b/plugins/user-settings/src/components/Settings.tsx index f754f1e7c3..25559299a8 100644 --- a/plugins/user-settings/src/components/Settings.tsx +++ b/plugins/user-settings/src/components/Settings.tsx @@ -16,19 +16,15 @@ import React from 'react'; import { SidebarItem } from '@backstage/core'; -import { SignInAvatar } from './General'; -import { useUserProfile } from './useUserProfileInfo'; +import SettingsIcon from '@material-ui/icons/Settings'; import { settingsRouteRef } from '../plugin'; export const Settings = () => { - const { displayName } = useUserProfile(); - const SidebarAvatar = () => ; - return ( ); }; From 8b9c8196f12aa666622c6b3ded2ba247acc49cc8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 28 Oct 2020 14:37:21 +0100 Subject: [PATCH 023/198] catalog,register-component: always register locations using a 'url' type --- .changeset/fresh-maps-complain.md | 6 +++ plugins/catalog/src/api/CatalogClient.ts | 12 +++++- plugins/catalog/src/api/types.ts | 7 +++- .../components/CatalogPage/CatalogPage.tsx | 2 +- .../src/filter/useEntityFilterGroup.test.tsx | 2 +- .../RegisterComponentForm.tsx | 39 +++---------------- .../RegisterComponentPage.test.tsx | 2 +- .../RegisterComponentPage.tsx | 15 +------ .../components/TemplatePage/TemplatePage.tsx | 9 +++-- 9 files changed, 39 insertions(+), 55 deletions(-) create mode 100644 .changeset/fresh-maps-complain.md diff --git a/.changeset/fresh-maps-complain.md b/.changeset/fresh-maps-complain.md new file mode 100644 index 0000000000..ef2021df37 --- /dev/null +++ b/.changeset/fresh-maps-complain.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog': minor +'@backstage/plugin-register-component': patch +--- + +Locations registered through the catalog client now default to the 'url' type. The type selection dropdown in the register-component form has been removed. diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index 642183f916..40622da60a 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -19,7 +19,12 @@ import { Location, LOCATION_ANNOTATION, } from '@backstage/catalog-model'; -import { CatalogApi, EntityCompoundName } from './types'; +import { + AddLocationRequest, + AddLocationResponse, + CatalogApi, + EntityCompoundName, +} from './types'; import { DiscoveryApi } from '@backstage/core'; export class CatalogClient implements CatalogApi { @@ -87,7 +92,10 @@ export class CatalogClient implements CatalogApi { return this.getOptional(`/entities/by-name/${kind}/${namespace}/${name}`); } - async addLocation(type: string, target: string) { + async addLocation({ + type = 'url', + target, + }: AddLocationRequest): Promise { const response = await fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/locations`, { diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index 36c86f2e96..324a8d43fd 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -35,11 +35,16 @@ export interface CatalogApi { compoundName: EntityCompoundName, ): Promise; getEntities(filter?: Record): Promise; - addLocation(type: string, target: string): Promise; + addLocation(location: AddLocationRequest): Promise; getLocationByEntity(entity: Entity): Promise; removeEntityByUid(uid: string): Promise; } +export type AddLocationRequest = { + type?: string; + target: string; +}; + export type AddLocationResponse = { location: Location; entities: Entity[]; diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 00be785754..1a9a723f16 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -76,7 +76,7 @@ const CatalogPageContents = () => { const root = configApi.getConfig('catalog.exampleEntityLocations'); for (const type of root.keys()) { for (const target of root.getStringArray(type)) { - promises.push(catalogApi.addLocation(type, target)); + promises.push(catalogApi.addLocation({ target })); } } await Promise.all(promises); diff --git a/plugins/catalog/src/filter/useEntityFilterGroup.test.tsx b/plugins/catalog/src/filter/useEntityFilterGroup.test.tsx index acf4790ebf..3d11ffe7a9 100644 --- a/plugins/catalog/src/filter/useEntityFilterGroup.test.tsx +++ b/plugins/catalog/src/filter/useEntityFilterGroup.test.tsx @@ -30,7 +30,7 @@ describe('useEntityFilterGroup', () => { beforeEach(() => { catalogApi = { /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ - addLocation: jest.fn((_a, _b) => new Promise(() => {})), + addLocation: jest.fn(_a => new Promise(() => {})), getEntities: jest.fn(), getLocationByEntity: jest.fn(), getLocationById: jest.fn(), diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx index bc433e1c36..56dd50ea67 100644 --- a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx +++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx @@ -19,15 +19,12 @@ import { Button, FormControl, FormHelperText, - InputLabel, LinearProgress, - MenuItem, - Select, TextField, } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; import React from 'react'; -import { Controller, useForm } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; import { ComponentIdValidators } from '../../util/validate'; const useStyles = makeStyles(theme => ({ @@ -50,11 +47,11 @@ export type Props = { }; export const RegisterComponentForm = ({ onSubmit, submitting }: Props) => { - const { control, register, handleSubmit, errors, formState } = useForm({ + const { register, handleSubmit, errors, formState } = useForm({ mode: 'onChange', }); const classes = useStyles(); - const hasErrors = !!errors.componentLocation; + const hasErrors = !!errors.entityLocation; const dirty = formState?.isDirty; return submitting ? ( @@ -71,10 +68,9 @@ export const RegisterComponentForm = ({ onSubmit, submitting }: Props) => { id="registerComponentInput" variant="outlined" label="Entity file URL" - data-testid="componentLocationInput" error={hasErrors} placeholder="https://example.com/user/some-service/blob/master/catalog-info.yaml" - name="componentLocation" + name="entityLocation" required margin="normal" helperText="Enter the full path to the catalog-info.yaml file in GitHub, GitLab, Bitbucket or Azure to start tracking your component." @@ -84,36 +80,13 @@ export const RegisterComponentForm = ({ onSubmit, submitting }: Props) => { })} /> - {errors.componentLocation && ( + {errors.entityLocation && ( - {errors.componentLocation.message} + {errors.entityLocation.message} )} - - Host type - ( - - )} - /> - +
+ + { featureFlags.isActive('enable-example-feature') && } +
); }; ``` From 1f010f7828f47d37a523bb06ba8e2c16f5ef7d3a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 28 Oct 2020 22:14:23 +0100 Subject: [PATCH 043/198] core-api: move LocalStorageFeatureFlags to api implementations --- .../LocalStorageFeatureFlags.test.tsx} | 4 ++-- .../LocalStorageFeatureFlags.tsx} | 2 +- .../implementations/FeatureFlagsApi/index.ts | 17 +++++++++++++++++ .../core-api/src/apis/implementations/index.ts | 3 ++- packages/core-api/src/app/App.tsx | 2 +- 5 files changed, 23 insertions(+), 5 deletions(-) rename packages/core-api/src/{app/FeatureFlags.test.tsx => apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.test.tsx} (98%) rename packages/core-api/src/{app/FeatureFlags.tsx => apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.tsx} (99%) create mode 100644 packages/core-api/src/apis/implementations/FeatureFlagsApi/index.ts diff --git a/packages/core-api/src/app/FeatureFlags.test.tsx b/packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.test.tsx similarity index 98% rename from packages/core-api/src/app/FeatureFlags.test.tsx rename to packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.test.tsx index 02641d6d18..2902e399c9 100644 --- a/packages/core-api/src/app/FeatureFlags.test.tsx +++ b/packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.test.tsx @@ -14,8 +14,8 @@ * limitations under the License. */ -import { LocalStorageFeatureFlags } from './FeatureFlags'; -import { FeatureFlagState, FeatureFlagsApi } from '../apis/definitions'; +import { LocalStorageFeatureFlags } from './LocalStorageFeatureFlags'; +import { FeatureFlagState, FeatureFlagsApi } from '../../definitions'; describe('FeatureFlags', () => { beforeEach(() => { diff --git a/packages/core-api/src/app/FeatureFlags.tsx b/packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.tsx similarity index 99% rename from packages/core-api/src/app/FeatureFlags.tsx rename to packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.tsx index 73215481b7..00b3eff906 100644 --- a/packages/core-api/src/app/FeatureFlags.tsx +++ b/packages/core-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags.tsx @@ -19,7 +19,7 @@ import { FeatureFlagsApi, FeatureFlag, FeatureFlagsSaveOptions, -} from '../apis/definitions'; +} from '../../definitions'; export function validateFlagName(name: string): void { if (name.length < 3) { diff --git a/packages/core-api/src/apis/implementations/FeatureFlagsApi/index.ts b/packages/core-api/src/apis/implementations/FeatureFlagsApi/index.ts new file mode 100644 index 0000000000..33990584f3 --- /dev/null +++ b/packages/core-api/src/apis/implementations/FeatureFlagsApi/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 { LocalStorageFeatureFlags } from './LocalStorageFeatureFlags'; diff --git a/packages/core-api/src/apis/implementations/index.ts b/packages/core-api/src/apis/implementations/index.ts index 30aeb81d44..d0df2760ab 100644 --- a/packages/core-api/src/apis/implementations/index.ts +++ b/packages/core-api/src/apis/implementations/index.ts @@ -23,7 +23,8 @@ export * from './auth'; export * from './AlertApi'; export * from './AppThemeApi'; export * from './ConfigApi'; -export * from './ErrorApi'; export * from './DiscoveryApi'; +export * from './ErrorApi'; +export * from './FeatureFlagsApi'; export * from './OAuthRequestApi'; export * from './StorageApi'; diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 91b55a7d2e..9c38c20b44 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -50,11 +50,11 @@ import { useApi, AnyApiFactory, ApiHolder, + LocalStorageFeatureFlags, } from '../apis'; import { useAsync } from 'react-use'; import { AppIdentity } from './AppIdentity'; import { ApiResolver, ApiFactoryRegistry } from '../apis/system'; -import { LocalStorageFeatureFlags } from './FeatureFlags'; type FullAppOptions = { apis: Iterable; From cbab5bbf8aefc2df4a737204d7f2d1a3544eadea Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 28 Oct 2020 22:46:21 +0100 Subject: [PATCH 044/198] changesets: added changeset for FeatureFlagsApi refactoring --- .changeset/swift-emus-mate.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/swift-emus-mate.md diff --git a/.changeset/swift-emus-mate.md b/.changeset/swift-emus-mate.md new file mode 100644 index 0000000000..1903742115 --- /dev/null +++ b/.changeset/swift-emus-mate.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-api': minor +--- + +Refactored the FeatureFlagsApi to make it easier to re-implement. Existing usage of particularly getUserFlags can be replaced with isActive() or save(). From a3ae6ddfdbeab25c9d5437dd47a3656e98f5a010 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 28 Oct 2020 22:50:37 +0100 Subject: [PATCH 045/198] docgen: add support for picking up the type from the declaration instead of initialization --- packages/docgen/src/docgen/TypeLocator.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/docgen/src/docgen/TypeLocator.ts b/packages/docgen/src/docgen/TypeLocator.ts index 6030258112..a5a0b1fc5b 100644 --- a/packages/docgen/src/docgen/TypeLocator.ts +++ b/packages/docgen/src/docgen/TypeLocator.ts @@ -76,13 +76,7 @@ export default class TypeLocator { return; } - docMap.get(decl.constructorType)!.push({ - node, - name: decl.name, - source, - args: Array.from(decl.initializer.arguments || []), - typeArgs: Array.from(decl.initializer.typeArguments || []), - }); + docMap.get(decl.constructorType)!.push({ node, source, ...decl }); }); }); @@ -103,7 +97,8 @@ export default class TypeLocator { ): | { constructorType: ts.Type; - initializer: ts.CallExpression | ts.NewExpression; + args: ts.Expression[]; + typeArgs: ts.TypeNode[]; name: string; } | undefined { @@ -137,6 +132,14 @@ export default class TypeLocator { const constructorType = this.checker.getTypeAtLocation( initializer.expression, ); - return { constructorType, initializer, name: name.text }; + const args = Array.from(initializer.arguments ?? []); + const typeNode = declaration.type; + const typeArgs = Array.from( + (typeNode && ts.isTypeReferenceNode(typeNode) + ? typeNode.typeArguments + : initializer.typeArguments) ?? [], + ); + + return { constructorType, args, typeArgs, name: name.text }; } } From 0a5d8606824accb38f23adbd870725ee5584359b Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Thu, 29 Oct 2020 04:40:05 -0400 Subject: [PATCH 046/198] Fix typo in template comment (#3152) --- packages/create-app/templates/default-app/app-config.yaml.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index d643c72e0c..3c7bc40c2b 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -38,7 +38,7 @@ backend: #ca: # if you have a CA file and want to verify it you can uncomment this section # $file: /ca/server.crt {{/if}} - # workingDirectory: /tmp # Use this to configure a working direcotry for the scaffolder, defaults to the OS temp-dir + # workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir integrations: github: From e3ceaa97bebc13f26295df6adf8ffbafc6acb9db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2020 09:40:30 +0100 Subject: [PATCH 047/198] chore(deps): bump commander from 6.1.0 to 6.2.0 (#3154) Bumps [commander](https://github.com/tj/commander.js) from 6.1.0 to 6.2.0. - [Release notes](https://github.com/tj/commander.js/releases) - [Changelog](https://github.com/tj/commander.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/tj/commander.js/compare/v6.1.0...v6.2.0) 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 cdf9fd9c31..3e2e8e4e9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8781,9 +8781,9 @@ commander@^5.0.0, commander@^5.1.0: integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== commander@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/commander/-/commander-6.1.0.tgz#f8d722b78103141006b66f4c7ba1e97315ba75bc" - integrity sha512-wl7PNrYWd2y5mp1OK/LhTlv8Ff4kQJQRXXAvF+uU/TPNiVJUxZLRYGj/B0y/lPGAVcSbJqH2Za/cvHmrPMC8mA== + version "6.2.0" + resolved "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz#b990bfb8ac030aedc6d11bc04d1488ffef56db75" + integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== common-tags@1.8.0, common-tags@^1.8.0: version "1.8.0" From a4c727f1a20aa3676e2b24b9737f569f3f60fae8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2020 09:41:57 +0100 Subject: [PATCH 048/198] chore(deps): bump eslint from 7.4.0 to 7.12.1 (#3153) Bumps [eslint](https://github.com/eslint/eslint) from 7.4.0 to 7.12.1. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v7.4.0...v7.12.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 137 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 77 insertions(+), 60 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3e2e8e4e9d..b2b87be5e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1673,6 +1673,22 @@ resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== +"@eslint/eslintrc@^0.2.1": + version "0.2.1" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.1.tgz#f72069c330461a06684d119384435e12a5d76e3c" + integrity sha512-XRUeBZ5zBWLYgSANMpThFddrZZkEbGHgUdt5UJjZfnlN9BGCiUBrf+nvbRupSjMvqzwnQN0qwCmOxITt1cfywA== + dependencies: + ajv "^6.12.4" + debug "^4.1.1" + espree "^7.3.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^3.13.1" + lodash "^4.17.19" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + "@evocateur/libnpmaccess@^3.1.2": version "3.1.2" resolved "https://registry.npmjs.org/@evocateur/libnpmaccess/-/libnpmaccess-3.1.2.tgz#ecf7f6ce6b004e9f942b098d92200be4a4b1c845" @@ -6337,11 +6353,16 @@ acorn@^6.0.1, acorn@^6.4.1: resolved "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== -acorn@^7.1.1, acorn@^7.2.0: +acorn@^7.1.1: version "7.3.1" resolved "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz#85010754db53c3fbaf3b9ea3e083aa5c5d147ffd" integrity sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA== +acorn@^7.4.0: + version "7.4.1" + resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + address@1.1.2, address@^1.0.1: version "1.1.2" resolved "https://registry.npmjs.org/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" @@ -6436,17 +6457,7 @@ ajv@^5.0.0: fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.1, ajv@^6.10.2, ajv@^6.12.4, ajv@^6.5.5, ajv@^6.7.0: - version "6.12.4" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz#0614facc4522127fa713445c6bfd3ebd376e2234" - integrity sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^6.12.5: +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.1, ajv@^6.10.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.5, ajv@^6.7.0: version "6.12.5" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== @@ -6475,7 +6486,7 @@ ansi-align@^3.0.0: dependencies: string-width "^3.0.0" -ansi-colors@^3.0.0, ansi-colors@^3.2.1: +ansi-colors@^3.0.0: version "3.2.4" resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== @@ -9826,7 +9837,14 @@ debug@3.1.0, debug@=3.1.0: dependencies: ms "2.0.0" -debug@4, debug@4.1.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: +debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" + integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== + dependencies: + ms "2.1.2" + +debug@4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -9840,13 +9858,6 @@ debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: dependencies: ms "^2.1.1" -debug@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" - integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== - dependencies: - ms "2.1.2" - debuglog@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -10601,20 +10612,13 @@ enhanced-resolve@^4.0.0, enhanced-resolve@^4.3.0: memory-fs "^0.5.0" tapable "^1.0.0" -enquirer@^2.3.0: +enquirer@^2.3.0, enquirer@^2.3.5: version "2.3.6" resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== dependencies: ansi-colors "^4.1.1" -enquirer@^2.3.5: - version "2.3.5" - resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.5.tgz#3ab2b838df0a9d8ab9e7dff235b0e8712ef92381" - integrity sha512-BNT1C08P9XD0vNg3J475yIUG+mVdp9T6towYFHUv897X0KoHBjB1shyrNmhmtHWKP17iSWgo7Gqh7BBuzLZMSA== - dependencies: - ansi-colors "^3.2.1" - entities@^1.1.1, entities@^1.1.2, entities@~1.1.1: version "1.1.2" resolved "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" @@ -10965,25 +10969,25 @@ eslint-scope@^4.0.3: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-scope@^5.0.0, eslint-scope@^5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" - integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== +eslint-scope@^5.0.0, eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== dependencies: - esrecurse "^4.1.0" + esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-utils@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz#7be1cc70f27a72a76cd14aa698bcabed6890e1cd" - integrity sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA== +eslint-utils@^2.0.0, eslint-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== dependencies: eslint-visitor-keys "^1.1.0" -eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.2.0.tgz#74415ac884874495f78ec2a97349525344c981fa" - integrity sha512-WFb4ihckKil6hu3Dp798xdzSfddwKKU3+nGniKF6HfeW6OLd2OUDEPP7TcHtB5+QXOKg2s6B2DaMPE1Nn/kxKQ== +eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== eslint-visitor-keys@^2.0.0: version "2.0.0" @@ -10991,21 +10995,22 @@ eslint-visitor-keys@^2.0.0: integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== eslint@^7.1.0: - version "7.4.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-7.4.0.tgz#4e35a2697e6c1972f9d6ef2b690ad319f80f206f" - integrity sha512-gU+lxhlPHu45H3JkEGgYhWhkR9wLHHEXC9FbWFnTlEkbKyZKWgWRLgf61E8zWmBuI6g5xKBph9ltg3NtZMVF8g== + version "7.12.1" + resolved "https://registry.npmjs.org/eslint/-/eslint-7.12.1.tgz#bd9a81fa67a6cfd51656cdb88812ce49ccec5801" + integrity sha512-HlMTEdr/LicJfN08LB3nM1rRYliDXOmfoO4vj39xN6BLpFzF00hbwBoqHk8UcJ2M/3nlARZWy/mslvGEuZFvsg== dependencies: "@babel/code-frame" "^7.0.0" + "@eslint/eslintrc" "^0.2.1" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.0.1" doctrine "^3.0.0" enquirer "^2.3.5" - eslint-scope "^5.1.0" - eslint-utils "^2.0.0" - eslint-visitor-keys "^1.2.0" - espree "^7.1.0" + eslint-scope "^5.1.1" + eslint-utils "^2.1.0" + eslint-visitor-keys "^2.0.0" + espree "^7.3.0" esquery "^1.2.0" esutils "^2.0.2" file-entry-cache "^5.0.1" @@ -11019,7 +11024,7 @@ eslint@^7.1.0: js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" - lodash "^4.17.14" + lodash "^4.17.19" minimatch "^3.0.4" natural-compare "^1.4.0" optionator "^0.9.1" @@ -11037,14 +11042,14 @@ esm@^3.2.25: resolved "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== -espree@^7.1.0: - version "7.1.0" - resolved "https://registry.npmjs.org/espree/-/espree-7.1.0.tgz#a9c7f18a752056735bf1ba14cb1b70adc3a5ce1c" - integrity sha512-dcorZSyfmm4WTuTnE5Y7MEN1DyoPYy1ZR783QW1FJoenn7RailyWFsq/UL6ZAAA7uXurN9FIpYyUs3OfiIW+Qw== +espree@^7.3.0: + version "7.3.0" + resolved "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz#dc30437cf67947cf576121ebd780f15eeac72348" + integrity sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw== dependencies: - acorn "^7.2.0" + acorn "^7.4.0" acorn-jsx "^5.2.0" - eslint-visitor-keys "^1.2.0" + eslint-visitor-keys "^1.3.0" esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: version "4.0.1" @@ -11065,6 +11070,13 @@ esrecurse@^4.1.0: dependencies: estraverse "^4.1.0" +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: version "4.3.0" resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" @@ -11075,6 +11087,11 @@ estraverse@^5.1.0: resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642" integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw== +estraverse@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" + integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + estree-walker@^0.6.1: version "0.6.1" resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" @@ -21874,10 +21891,10 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" -strip-json-comments@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180" - integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w== +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== strip-json-comments@~2.0.1: version "2.0.1" From 55af83a78a9ce8c0591728c2fec13dbdd2d39e9a Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Thu, 29 Oct 2020 14:33:09 +0530 Subject: [PATCH 049/198] chore: move entityClient to createPlugin, mock entityClient in tests --- packages/backend/src/plugins/scaffolder.ts | 6 ++ plugins/scaffolder-backend/src/index.ts | 1 + .../src/service/router.test.ts | 91 ++++++++++--------- .../scaffolder-backend/src/service/router.ts | 5 +- 4 files changed, 59 insertions(+), 44 deletions(-) diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index 6eacd143c1..5d36d508a5 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -21,7 +21,9 @@ import { Publishers, CreateReactAppTemplater, Templaters, + CatalogEntityClient, } from '@backstage/plugin-scaffolder-backend'; +import { SingleHostDiscovery } from '@backstage/backend-common'; import type { PluginEnvironment } from '../types'; import Docker from 'dockerode'; @@ -40,6 +42,9 @@ export default async function createPlugin({ const dockerClient = new Docker(); + const discovery = SingleHostDiscovery.fromConfig(config); + const entityClient = new CatalogEntityClient({ discovery }); + return await createRouter({ preparers, templaters, @@ -47,5 +52,6 @@ export default async function createPlugin({ logger, config, dockerClient, + entityClient, }); } diff --git a/plugins/scaffolder-backend/src/index.ts b/plugins/scaffolder-backend/src/index.ts index c461bfede6..28e6a2b24c 100644 --- a/plugins/scaffolder-backend/src/index.ts +++ b/plugins/scaffolder-backend/src/index.ts @@ -16,3 +16,4 @@ export * from './scaffolder'; export * from './service/router'; +export * from './lib/catalog'; diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index fae7ee8beb..c2b60a2040 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -35,6 +35,10 @@ import Docker from 'dockerode'; jest.mock('dockerode'); +const generateEntityClient: any = (template: any) => ({ + findTemplate: () => Promise.resolve(template), +}); + describe('createRouter - working directory', () => { const mockPrepare = jest.fn(); const mockPreparers = new Preparers(); @@ -74,6 +78,8 @@ describe('createRouter - working directory', () => { }, }; + const mockedEntityClient = generateEntityClient(template); + it('should throw an error when working directory does not exist or is not writable', async () => { mockAccess.mockImplementation(() => { throw new Error('access error'); @@ -87,6 +93,7 @@ describe('createRouter - working directory', () => { publishers: new Publishers(), config: ConfigReader.fromConfigs([workDirConfig('/path')]), dockerClient: new Docker(), + entityClient: mockedEntityClient, }), ).rejects.toThrow('access error'); }); @@ -99,11 +106,12 @@ describe('createRouter - working directory', () => { publishers: new Publishers(), config: ConfigReader.fromConfigs([workDirConfig('/path')]), dockerClient: new Docker(), + entityClient: mockedEntityClient, }); const app = express().use(router); await request(app).post('/v1/jobs').send({ - template, + templateName: '', values: {}, }); @@ -121,11 +129,12 @@ describe('createRouter - working directory', () => { publishers: new Publishers(), config: ConfigReader.fromConfigs([]), dockerClient: new Docker(), + entityClient: mockedEntityClient, }); const app = express().use(router); await request(app).post('/v1/jobs').send({ - template, + templateName: '', values: {}, }); @@ -137,6 +146,43 @@ describe('createRouter - working directory', () => { describe('createRouter', () => { let app: express.Express; + const template = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Template', + metadata: { + description: 'Create a new CRA website project', + name: 'create-react-app-template', + tags: ['experimental', 'react', 'cra'], + title: 'Create React App Template', + }, + spec: { + owner: 'web@example.com', + path: '.', + schema: { + properties: { + component_id: { + description: 'Unique name of the component', + title: 'Name', + type: 'string', + }, + description: { + description: 'Description of the component', + title: 'Description', + type: 'string', + }, + use_typescript: { + default: true, + description: 'Include typescript', + title: 'Use Typescript', + type: 'boolean', + }, + }, + required: ['component_id', 'use_typescript'], + }, + templater: 'cra', + type: 'website', + }, + }; beforeAll(async () => { const router = await createRouter({ @@ -146,6 +192,7 @@ describe('createRouter', () => { publishers: new Publishers(), config: ConfigReader.fromConfigs([]), dockerClient: new Docker(), + entityClient: generateEntityClient(template), }); app = express().use(router); }); @@ -155,47 +202,9 @@ describe('createRouter', () => { }); describe('POST /v1/jobs', () => { - const template = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Template', - metadata: { - description: 'Create a new CRA website project', - name: 'create-react-app-template', - tags: ['experimental', 'react', 'cra'], - title: 'Create React App Template', - }, - spec: { - owner: 'web@example.com', - path: '.', - schema: { - properties: { - component_id: { - description: 'Unique name of the component', - title: 'Name', - type: 'string', - }, - description: { - description: 'Description of the component', - title: 'Description', - type: 'string', - }, - use_typescript: { - default: true, - description: 'Include typescript', - title: 'Use Typescript', - type: 'boolean', - }, - }, - required: ['component_id', 'use_typescript'], - }, - templater: 'cra', - type: 'website', - }, - }; - it('rejects template values which do not match the template schema definition', async () => { const response = await request(app).post('/v1/jobs').send({ - template, + templateName: '', values: {}, }); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index efe8ee46f6..e4d3d5c45a 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { SingleHostDiscovery } from '@backstage/backend-common'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { Config, JsonValue } from '@backstage/config'; import fs from 'fs-extra'; @@ -41,6 +40,7 @@ export interface RouterOptions { logger: Logger; config: Config; dockerClient: Docker; + entityClient: CatalogEntityClient; } export async function createRouter( @@ -56,13 +56,12 @@ export async function createRouter( logger: parentLogger, config, dockerClient, + entityClient, } = options; const logger = parentLogger.child({ plugin: 'scaffolder' }); const jobProcessor = new JobProcessor(); - const discovery = SingleHostDiscovery.fromConfig(config); - const entityClient = new CatalogEntityClient({ discovery }); let workingDirectory: string; if (config.has('backend.workingDirectory')) { workingDirectory = config.getString('backend.workingDirectory'); From 1ddcf191dbf5f0280383e3bb20f251016ed14889 Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Thu, 29 Oct 2020 14:46:55 +0530 Subject: [PATCH 050/198] fix: use entityClient in create-app package --- .../default-app/packages/backend/src/plugins/scaffolder.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts index 8de15920e9..2dc69feb45 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts @@ -11,7 +11,9 @@ import { CreateReactAppTemplater, Templaters, RepoVisibilityOptions, + CatalogEntityClient, } from '@backstage/plugin-scaffolder-backend'; +import { SingleHostDiscovery } from '@backstage/backend-common'; import { Octokit } from '@octokit/rest'; import { Gitlab } from '@gitbeaker/node'; import type { PluginEnvironment } from '../types'; @@ -97,6 +99,10 @@ export default async function createPlugin({ } const dockerClient = new Docker(); + + const discovery = SingleHostDiscovery.fromConfig(config); + const entityClient = new CatalogEntityClient({ discovery }); + return await createRouter({ preparers, templaters, @@ -104,5 +110,6 @@ export default async function createPlugin({ logger, config, dockerClient, + entityClient, }); } From 364939e5e3dee6511e62185a6d492a2f904e8f4a Mon Sep 17 00:00:00 2001 From: Marvin9 Date: Thu, 29 Oct 2020 14:54:30 +0530 Subject: [PATCH 051/198] chore: remove try/catch --- plugins/scaffolder-backend/src/service/router.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index e4d3d5c45a..2dc6a794fb 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { Config, JsonValue } from '@backstage/config'; import fs from 'fs-extra'; import Docker from 'dockerode'; @@ -111,12 +110,7 @@ export async function createRouter( const values: RequiredTemplateValues & Record = req.body.values; - let template: TemplateEntityV1alpha1; - try { - template = await entityClient.findTemplate(templateName); - } catch (err) { - throw err; - } + const template = await entityClient.findTemplate(templateName); const validationResult: ValidatorResult = validate( values, From b1b3f094f10470fff5feee99a89a365e729a2284 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 29 Oct 2020 15:51:54 +0100 Subject: [PATCH 052/198] docs: tweak feature-flags docs --- docs/reference/createPlugin-feature-flags.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/createPlugin-feature-flags.md b/docs/reference/createPlugin-feature-flags.md index 7108941edd..bcea80e26b 100644 --- a/docs/reference/createPlugin-feature-flags.md +++ b/docs/reference/createPlugin-feature-flags.md @@ -24,14 +24,14 @@ export default createPlugin({ ## Using with useApi To inspect the state of a feature flag inside your plugin, you can use the -`FeatureFlagsApi`, accessed via the `FeatureFlagsApiRef`. For example: +`FeatureFlagsApi`, accessed via the `featureFlagsApiRef`. For example: ```tsx import React, { FC } from 'react'; import { Button } from '@material-ui/core'; import { featureFlagsApiRef, useApi } from '@backstage/core'; -const ExamplePage: FC<{}> = () => { +const ExamplePage = () => { const featureFlags = useApi(featureFlagsApiRef); return ( From fa56f46158f78c665a7e0f69f7b2818632f560af Mon Sep 17 00:00:00 2001 From: Vividh Chandna Date: Thu, 29 Oct 2020 21:28:18 +0530 Subject: [PATCH 053/198] fix: Update tags validation message & documentation (#3145) * Update dns label validation message to match regex pattern * Update documentation for tags validation & examples * Add changeset --- .changeset/ninety-ads-dance.md | 5 +++++ docs/features/software-catalog/descriptor-format.md | 6 +++--- .../src/entity/policies/FieldFormatEntityPolicy.ts | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .changeset/ninety-ads-dance.md diff --git a/.changeset/ninety-ads-dance.md b/.changeset/ninety-ads-dance.md new file mode 100644 index 0000000000..a03298fd7f --- /dev/null +++ b/.changeset/ninety-ads-dance.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': patch +--- + +Fix documentation and validation message for tags diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 62ac9da8d8..a330ef196a 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -256,8 +256,8 @@ component, like `java` or `go`. This field is optional, and currently has no special semantics. -Each tag must be sequences of `[a-zA-Z0-9]` separated by `-`, at most 63 -characters in total. +Each tag must be sequences of `[a-z0-9]` separated by `-`, at most 63 characters +in total. ## Kind: Component @@ -418,7 +418,7 @@ of the `metadata.name` field. ### `metadata.tags` [optional] A list of strings that can be associated with the template, e.g. -`['Recommended', 'React']`. +`['recommended', 'react']`. This list will also be used in the frontend to display to the user so you can potentially search and group templates by these tags. diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index 7193c1a89b..acd4469fcb 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -74,7 +74,7 @@ export class FieldFormatEntityPolicy implements EntityPolicy { case 'isValidNamespace': case 'isValidDnsLabel': expectation = - 'a string that is sequences of [a-zA-Z0-9] separated by [-], at most 63 characters in total'; + 'a string that is sequences of [a-z0-9] separated by [-], at most 63 characters in total'; break; case 'isValidAnnotationValue': expectation = 'a string'; From 6d4f8c6366a5dd5a7b4c1edf9b4714fde20a5e73 Mon Sep 17 00:00:00 2001 From: Abhishek Jakhar Date: Thu, 29 Oct 2020 16:32:55 +0530 Subject: [PATCH 054/198] chore: add test case for ErrorBoundary component --- .../ErrorBoundary/ErrorBoundary.test.tsx | 71 +++++++++++++++++++ .../layout/ErrorBoundary/ErrorBoundary.tsx | 2 +- 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/layout/ErrorBoundary/ErrorBoundary.test.tsx diff --git a/packages/core/src/layout/ErrorBoundary/ErrorBoundary.test.tsx b/packages/core/src/layout/ErrorBoundary/ErrorBoundary.test.tsx new file mode 100644 index 0000000000..4a3126c73c --- /dev/null +++ b/packages/core/src/layout/ErrorBoundary/ErrorBoundary.test.tsx @@ -0,0 +1,71 @@ +/* eslint-disable no-console */ +/* + * 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 { ErrorBoundary } from './ErrorBoundary'; +import { renderInTestApp, withLogCollector } from '@backstage/test-utils'; + +type BombProps = { + shouldThrow?: boolean; + children?: React.ReactNode; +}; + +const Bomb = ({ shouldThrow }: BombProps) => { + if (shouldThrow) { + throw new Error('Bomb'); + } else { + return

Working Component

; + } +}; + +describe('', () => { + it('should render error boundary with and without error', async () => { + const { error } = await withLogCollector(['error'], async () => { + const { + rerender, + queryByRole, + getByRole, + getByText, + } = await renderInTestApp( + + + , + ); + + expect(queryByRole('alert')).not.toBeInTheDocument(); + expect(getByText(/working component/i)).toBeInTheDocument(); + + rerender( + + + , + ); + + expect(getByRole('alert')).toBeInTheDocument(); + expect(getByText(/something went wrong here/i)).toBeInTheDocument(); + }); + + expect(error).toEqual([ + expect.stringMatching(/^Error: Uncaught \[Error: Bomb\]/), + expect.stringMatching( + /^The above error occurred in the component:/, + ), + expect.stringMatching(/^ErrorBoundary/), + ]); + expect(error.length).toEqual(3); + }); +}); diff --git a/packages/core/src/layout/ErrorBoundary/ErrorBoundary.tsx b/packages/core/src/layout/ErrorBoundary/ErrorBoundary.tsx index cc48e26b1d..17900ad8e3 100644 --- a/packages/core/src/layout/ErrorBoundary/ErrorBoundary.tsx +++ b/packages/core/src/layout/ErrorBoundary/ErrorBoundary.tsx @@ -65,7 +65,7 @@ type EProps = { const Error = ({ slackChannel }: EProps) => { return ( -
+
Something went wrong here.{' '} {slackChannel && <>Please contact {slackChannel} for help.}
From 21e071fa6c26ebec4dcca013d55351d56e024d59 Mon Sep 17 00:00:00 2001 From: Forrest Waters Date: Wed, 28 Oct 2020 18:23:13 -0500 Subject: [PATCH 055/198] working onelogin auth implementation --- app-config.yaml | 8 + packages/app/src/identityProviders.ts | 7 + .../core-api/src/apis/definitions/auth.ts | 11 ++ .../src/apis/implementations/auth/index.ts | 1 + .../auth/onelogin/OktaAuth.test.ts | 61 +++++++ .../auth/onelogin/OneLoginAuth.ts | 82 +++++++++ .../implementations/auth/onelogin/index.ts | 17 ++ packages/core/src/api-wrappers/defaultApis.ts | 11 ++ .../lib/passport/PassportStrategyHelper.ts | 1 - .../auth-backend/src/providers/factories.ts | 2 + .../src/providers/onelogin/index.ts | 1 + .../src/providers/onelogin/provider.ts | 160 ++++++++++++++++++ .../src/providers/onelogin/types.d.ts | 20 +++ 13 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 packages/core-api/src/apis/implementations/auth/onelogin/OktaAuth.test.ts create mode 100644 packages/core-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts create mode 100644 packages/core-api/src/apis/implementations/auth/onelogin/index.ts create mode 100644 plugins/auth-backend/src/providers/onelogin/index.ts create mode 100644 plugins/auth-backend/src/providers/onelogin/provider.ts create mode 100644 plugins/auth-backend/src/providers/onelogin/types.d.ts diff --git a/app-config.yaml b/app-config.yaml index 4746e822da..a495df0263 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -232,6 +232,14 @@ auth: $env: AUTH_MICROSOFT_CLIENT_SECRET tenantId: $env: AUTH_MICROSOFT_TENANT_ID + onelogin: + development: + clientId: + $env: AUTH_ONELOGIN_CLIENT_ID + clientSecret: + $env: AUTH_ONELOGIN_CLIENT_SECRET + issuer: + $env: AUTH_ONELOGIN_ISSUER costInsights: engineerCost: 200000 products: diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index 388bc8d8b6..06ff6b07d3 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -21,6 +21,7 @@ import { githubAuthApiRef, samlAuthApiRef, microsoftAuthApiRef, + oneloginAuthApiRef, } from '@backstage/core'; export const providers = [ @@ -60,4 +61,10 @@ export const providers = [ message: 'Sign In using SAML', apiRef: samlAuthApiRef, }, + { + id: 'onelogin-auth-provider', + title: 'OneLogin', + message: 'Sign In using OneLogin', + apiRef: oneloginAuthApiRef, + }, ]; diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index 946baeabd9..c538065f3a 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -320,3 +320,14 @@ export const samlAuthApiRef: ApiRef< id: 'core.auth.saml', description: 'Example of how to use SAML custom provider', }); + +export const oneloginAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +> = createApiRef({ + id: 'core.auth.onelogin', + description: 'Provides authentication towards OneLogin APIs and identities', +}); diff --git a/packages/core-api/src/apis/implementations/auth/index.ts b/packages/core-api/src/apis/implementations/auth/index.ts index 39223e8e15..7ef78d19cf 100644 --- a/packages/core-api/src/apis/implementations/auth/index.ts +++ b/packages/core-api/src/apis/implementations/auth/index.ts @@ -22,3 +22,4 @@ export * from './okta'; export * from './saml'; export * from './auth0'; export * from './microsoft'; +export * from './onelogin'; diff --git a/packages/core-api/src/apis/implementations/auth/onelogin/OktaAuth.test.ts b/packages/core-api/src/apis/implementations/auth/onelogin/OktaAuth.test.ts new file mode 100644 index 0000000000..d6b1a07d9d --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/onelogin/OktaAuth.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import OktaAuth from './OktaAuth'; +import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; +import { UrlPatternDiscovery } from '../../DiscoveryApi'; + +const PREFIX = 'okta.'; + +const getSession = jest.fn(); + +jest.mock('../../../../lib/AuthSessionManager', () => ({ + ...(jest.requireActual('../../../../lib/AuthSessionManager') as any), + RefreshingAuthSessionManager: class { + getSession = getSession; + }, +})); + +describe('OktaAuth', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it.each([ + ['openid', ['openid']], + ['profile email', ['profile', 'email']], + [`${PREFIX}groups.manage`, [`${PREFIX}groups.manage`]], + ['groups.read', [`${PREFIX}groups.read`]], + [ + `${PREFIX}groups.manage groups.read, openid`, + [`${PREFIX}groups.manage`, `${PREFIX}groups.read`, 'openid'], + ], + [`email\t ${PREFIX}groups.read`, ['email', `${PREFIX}groups.read`]], + + // Some incorrect scopes that we don't try to fix + [`${PREFIX}email`, [`${PREFIX}email`]], + [`${PREFIX}profile`, [`${PREFIX}profile`]], + [`${PREFIX}openid`, [`${PREFIX}openid`]], + ])(`should normalize scopes correctly - %p`, (scope, scopes) => { + const auth = OktaAuth.create({ + oauthRequestApi: new MockOAuthApi(), + discoveryApi: UrlPatternDiscovery.compile('http://example.com'), + }); + + auth.getAccessToken(scope); + expect(getSession).toHaveBeenCalledWith({ scopes: new Set(scopes) }); + }); +}); diff --git a/packages/core-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts b/packages/core-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts new file mode 100644 index 0000000000..7420b21510 --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/onelogin/OneLoginAuth.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 OneLoginIcon from '@material-ui/icons/AcUnit'; +import { oneloginAuthApiRef } from '@backstage/core-api/src/apis/definitions/auth'; +import { + OAuthRequestApi, + AuthProvider, + DiscoveryApi, +} from '@backstage/core-api/src/apis/definitions'; +import { OAuth2 } from '@backstage/core-api/src/apis/implementations/auth/oauth2'; + +type CreateOptions = { + discoveryApi: DiscoveryApi; + oauthRequestApi: OAuthRequestApi; + + environment?: string; + provider?: AuthProvider & { id: string }; +}; + +const DEFAULT_PROVIDER = { + id: 'onelogin', + title: 'onelogin', + icon: OneLoginIcon, +}; + +const OIDC_SCOPES: Set = new Set([ + 'openid', + 'profile', + 'email', + 'phone', + 'address', + 'groups', + 'offline_access', +]); + +const SCOPE_PREFIX: string = 'onelogin.'; + +class OneLoginAuth { + static create({ + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + }: CreateOptions): typeof oneloginAuthApiRef.T { + return OAuth2.create({ + discoveryApi, + oauthRequestApi, + provider, + environment, + defaultScopes: ['openid', 'email', 'profile', 'offline_access'], + scopeTransform(scopes) { + return scopes.map(scope => { + if (OIDC_SCOPES.has(scope)) { + return scope; + } + + if (scope.startsWith(SCOPE_PREFIX)) { + return scope; + } + + return `${SCOPE_PREFIX}${scope}`; + }); + }, + }); + } +} + +export default OneLoginAuth; diff --git a/packages/core-api/src/apis/implementations/auth/onelogin/index.ts b/packages/core-api/src/apis/implementations/auth/onelogin/index.ts new file mode 100644 index 0000000000..1d163207db --- /dev/null +++ b/packages/core-api/src/apis/implementations/auth/onelogin/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 { default as OneLoginAuth } from './OneLoginAuth'; diff --git a/packages/core/src/api-wrappers/defaultApis.ts b/packages/core/src/api-wrappers/defaultApis.ts index d0a777dc94..61d4f3a7e1 100644 --- a/packages/core/src/api-wrappers/defaultApis.ts +++ b/packages/core/src/api-wrappers/defaultApis.ts @@ -44,6 +44,8 @@ import { UrlPatternDiscovery, samlAuthApiRef, SamlAuth, + oneloginAuthApiRef, + OneLoginAuth, } from '@backstage/core-api'; export const defaultApis = [ @@ -142,4 +144,13 @@ export const defaultApis = [ }, factory: ({ discoveryApi }) => SamlAuth.create({ discoveryApi }), }), + createApiFactory({ + api: oneloginAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi }) => + OneLoginAuth.create({ discoveryApi, oauthRequestApi }), + }), ]; diff --git a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts index 3264fd8faa..28f15ee9e8 100644 --- a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts @@ -116,7 +116,6 @@ export const executeFrameHandlerStrategy = async ( strategy.redirect = () => { reject(new Error('Unexpected redirect')); }; - strategy.authenticate(req, {}); }, ); diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 37761c2305..a764d0b50e 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -22,6 +22,7 @@ import { createOktaProvider } from './okta'; import { createSamlProvider } from './saml'; import { createAuth0Provider } from './auth0'; import { createMicrosoftProvider } from './microsoft'; +import { createOneLoginProvider } from './onelogin'; import { AuthProviderFactory, AuthProviderFactoryOptions } from './types'; const factories: { [providerId: string]: AuthProviderFactory } = { @@ -33,6 +34,7 @@ const factories: { [providerId: string]: AuthProviderFactory } = { auth0: createAuth0Provider, microsoft: createMicrosoftProvider, oauth2: createOAuth2Provider, + onelogin: createOneLoginProvider, }; export function createAuthProvider( diff --git a/plugins/auth-backend/src/providers/onelogin/index.ts b/plugins/auth-backend/src/providers/onelogin/index.ts new file mode 100644 index 0000000000..4c110c56ac --- /dev/null +++ b/plugins/auth-backend/src/providers/onelogin/index.ts @@ -0,0 +1 @@ +export { createOneLoginProvider } from './provider'; \ No newline at end of file diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts new file mode 100644 index 0000000000..a9b43fc8d4 --- /dev/null +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -0,0 +1,160 @@ +//import { Strategy as OneLoginStrategy } from 'passport-openidconnect'; +import { Strategy as OneLoginStrategy } from 'passport-onelogin'; +import express from 'express'; +import { + OAuthAdapter, + OAuthProviderOptions, + OAuthHandlers, + OAuthResponse, + OAuthEnvironmentHandler, + OAuthStartRequest, + encodeState, + OAuthRefreshRequest, +} from '../../lib/oauth'; +import passport from 'passport'; +import { + executeFrameHandlerStrategy, + executeRedirectStrategy, + executeRefreshTokenStrategy, + makeProfileInfo, + executeFetchUserProfileStrategy, + PassportDoneCallback, +} from '../../lib/passport'; +import { RedirectInfo, AuthProviderFactory } from '../types'; + +type PrivateInfo = { + refreshToken: string; +}; + +export type OneLoginProviderOptions = OAuthProviderOptions & { + issuer: string +} + +export class OneLoginProvider implements OAuthHandlers { + private readonly _strategy: any; + + constructor(options: OneLoginProviderOptions) { + this._strategy = new OneLoginStrategy( + { + issuer: options.issuer, + clientID: options.clientId, + clientSecret: options.clientSecret, + callbackURL: options.callbackUrl, + passReqToCallback: false as true, + }, + ( + accessToken: any, + refreshToken: any, + params: any, + rawProfile: passport.Profile, + done: PassportDoneCallback, + ) => { + const profile = makeProfileInfo(rawProfile, params.id_token); + + done( + undefined, + { + providerInfo: { + idToken: params.id_token, + accessToken, + scope: params.scope, + //scope: 'profile', + expiresInSeconds: params.expires_in, + }, + profile, + }, + { + refreshToken, + }, + ); + }, + ); + } + async start(req: OAuthStartRequest): Promise { + return await executeRedirectStrategy(req, this._strategy, { + accessType: 'offline', + prompt: 'consent', + scope: 'openid', + state: encodeState(req.state), + }); + } + + async handler( + req: express.Request, + ): Promise<{ response: OAuthResponse; refreshToken: string }> { + const { response, privateInfo } = await executeFrameHandlerStrategy< + OAuthResponse, + PrivateInfo + >(req, this._strategy); + + return { + response: await this.populateIdentity(response), + refreshToken: privateInfo.refreshToken, + }; + } + + async refresh(req: OAuthRefreshRequest): Promise { + const { accessToken, params } = await executeRefreshTokenStrategy( + this._strategy, + req.refreshToken, + req.scope, + ); + + const profile = await executeFetchUserProfileStrategy( + this._strategy, + accessToken, + params.id_token, + ); + + return this.populateIdentity({ + providerInfo: { + accessToken, + idToken: params.id_token, + expiresInSeconds: params.expires_in, + scope: params.scope, + }, + profile, + }); + } + + private async populateIdentity( + response: OAuthResponse, + ): Promise { + const { profile } = response; + + if (!profile.email) { + throw new Error('OIDC profile contained no email'); + } + + const id = profile.email.split('@')[0]; + + return { ...response, backstageIdentity: { id } }; + } +} + + +export const createOneLoginProvider: AuthProviderFactory = ({ + globalConfig, + config, + tokenIssuer, +}) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const providerId = 'onelogin'; + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const issuer = envConfig.getString('issuer'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + + const provider = new OneLoginProvider({ + clientId, + clientSecret, + callbackUrl, + issuer, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); + }); diff --git a/plugins/auth-backend/src/providers/onelogin/types.d.ts b/plugins/auth-backend/src/providers/onelogin/types.d.ts new file mode 100644 index 0000000000..bff5a7e33c --- /dev/null +++ b/plugins/auth-backend/src/providers/onelogin/types.d.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. + */ +declare module 'passport-openidconnect' { + export class Strategy { + constructor(options: any, verify: any); + } +} From 159f5ee77c5042eb84acce2d5563eef9ca214df0 Mon Sep 17 00:00:00 2001 From: Forrest Waters Date: Wed, 28 Oct 2020 19:18:13 -0500 Subject: [PATCH 056/198] add passport-onelogin dependency --- .../auth/onelogin/OktaAuth.test.ts | 61 ------------------- plugins/auth-backend/package.json | 1 + yarn.lock | 9 +++ 3 files changed, 10 insertions(+), 61 deletions(-) delete mode 100644 packages/core-api/src/apis/implementations/auth/onelogin/OktaAuth.test.ts diff --git a/packages/core-api/src/apis/implementations/auth/onelogin/OktaAuth.test.ts b/packages/core-api/src/apis/implementations/auth/onelogin/OktaAuth.test.ts deleted file mode 100644 index d6b1a07d9d..0000000000 --- a/packages/core-api/src/apis/implementations/auth/onelogin/OktaAuth.test.ts +++ /dev/null @@ -1,61 +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 OktaAuth from './OktaAuth'; -import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; -import { UrlPatternDiscovery } from '../../DiscoveryApi'; - -const PREFIX = 'okta.'; - -const getSession = jest.fn(); - -jest.mock('../../../../lib/AuthSessionManager', () => ({ - ...(jest.requireActual('../../../../lib/AuthSessionManager') as any), - RefreshingAuthSessionManager: class { - getSession = getSession; - }, -})); - -describe('OktaAuth', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - - it.each([ - ['openid', ['openid']], - ['profile email', ['profile', 'email']], - [`${PREFIX}groups.manage`, [`${PREFIX}groups.manage`]], - ['groups.read', [`${PREFIX}groups.read`]], - [ - `${PREFIX}groups.manage groups.read, openid`, - [`${PREFIX}groups.manage`, `${PREFIX}groups.read`, 'openid'], - ], - [`email\t ${PREFIX}groups.read`, ['email', `${PREFIX}groups.read`]], - - // Some incorrect scopes that we don't try to fix - [`${PREFIX}email`, [`${PREFIX}email`]], - [`${PREFIX}profile`, [`${PREFIX}profile`]], - [`${PREFIX}openid`, [`${PREFIX}openid`]], - ])(`should normalize scopes correctly - %p`, (scope, scopes) => { - const auth = OktaAuth.create({ - oauthRequestApi: new MockOAuthApi(), - discoveryApi: UrlPatternDiscovery.compile('http://example.com'), - }); - - auth.getAccessToken(scope); - expect(getSession).toHaveBeenCalledWith({ scopes: new Set(scopes) }); - }); -}); diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 6489c8547d..7266395c78 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -45,6 +45,7 @@ "passport-microsoft": "^0.1.0", "passport-oauth2": "^1.5.0", "passport-okta-oauth": "^0.0.1", + "passport-onelogin-oauth": "^0.0.1", "passport-saml": "^1.3.3", "uuid": "^8.0.0", "winston": "^3.2.1", diff --git a/yarn.lock b/yarn.lock index b2b87be5e0..4d1a4cd856 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17915,6 +17915,15 @@ passport-okta-oauth@^0.0.1: pkginfo "0.2.x" uid2 "0.0.3" +passport-onelogin-oauth@^0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/passport-onelogin-oauth/-/passport-onelogin-oauth-0.0.1.tgz#6e991ac6720783fdd80d4caa08c36cc71ef19962" + integrity sha512-EXFBqlJdHf5AX4QaiZsLfhgQUOR6z3zGA5479SUJF4I4rnAt7yasZEbs27pg8MRiQh/uLZEWLGMoVXr6LHV9mQ== + dependencies: + passport-oauth "1.0.0" + pkginfo "0.2.x" + uid2 "0.0.3" + passport-saml@^1.3.3: version "1.3.3" resolved "https://registry.npmjs.org/passport-saml/-/passport-saml-1.3.3.tgz#cbea1a2b21ff32b3bc4bfd84dc39c3a370df9935" From 11664e3a99c79e95f92ec60ad681ca8849e282ea Mon Sep 17 00:00:00 2001 From: Forrest Waters Date: Wed, 28 Oct 2020 19:34:02 -0500 Subject: [PATCH 057/198] run prettier --- plugins/auth-backend/src/providers/onelogin/index.ts | 2 +- plugins/auth-backend/src/providers/onelogin/provider.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/auth-backend/src/providers/onelogin/index.ts b/plugins/auth-backend/src/providers/onelogin/index.ts index 4c110c56ac..c356ef2d6d 100644 --- a/plugins/auth-backend/src/providers/onelogin/index.ts +++ b/plugins/auth-backend/src/providers/onelogin/index.ts @@ -1 +1 @@ -export { createOneLoginProvider } from './provider'; \ No newline at end of file +export { createOneLoginProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index a9b43fc8d4..bafc0c61ca 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -27,8 +27,8 @@ type PrivateInfo = { }; export type OneLoginProviderOptions = OAuthProviderOptions & { - issuer: string -} + issuer: string; +}; export class OneLoginProvider implements OAuthHandlers { private readonly _strategy: any; @@ -132,7 +132,6 @@ export class OneLoginProvider implements OAuthHandlers { } } - export const createOneLoginProvider: AuthProviderFactory = ({ globalConfig, config, From b652bf2cc58ae56e34a73d4f74f25904d53ab61e Mon Sep 17 00:00:00 2001 From: Forrest Waters Date: Wed, 28 Oct 2020 19:48:21 -0500 Subject: [PATCH 058/198] add changeset for onelogin --- .changeset/beige-pandas-thank.md | 5 +++++ .../src/providers/onelogin/index.ts | 16 ++++++++++++++++ .../src/providers/onelogin/provider.ts | 19 +++++++++++++++++-- .../src/providers/onelogin/types.d.ts | 2 +- 4 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 .changeset/beige-pandas-thank.md diff --git a/.changeset/beige-pandas-thank.md b/.changeset/beige-pandas-thank.md new file mode 100644 index 0000000000..b2c52842fc --- /dev/null +++ b/.changeset/beige-pandas-thank.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Add OneLogin Identity Provider to Auth Backend diff --git a/plugins/auth-backend/src/providers/onelogin/index.ts b/plugins/auth-backend/src/providers/onelogin/index.ts index c356ef2d6d..79af226cd1 100644 --- a/plugins/auth-backend/src/providers/onelogin/index.ts +++ b/plugins/auth-backend/src/providers/onelogin/index.ts @@ -1 +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 { createOneLoginProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index bafc0c61ca..d11baa605d 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -1,5 +1,20 @@ -//import { Strategy as OneLoginStrategy } from 'passport-openidconnect'; -import { Strategy as OneLoginStrategy } from 'passport-onelogin'; +/* + * 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 { Strategy as OneLoginStrategy } from 'passport-onelogin-oauth'; import express from 'express'; import { OAuthAdapter, diff --git a/plugins/auth-backend/src/providers/onelogin/types.d.ts b/plugins/auth-backend/src/providers/onelogin/types.d.ts index bff5a7e33c..d418f1bcd4 100644 --- a/plugins/auth-backend/src/providers/onelogin/types.d.ts +++ b/plugins/auth-backend/src/providers/onelogin/types.d.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -declare module 'passport-openidconnect' { +declare module 'passport-onelogin-oauth' { export class Strategy { constructor(options: any, verify: any); } From 842acf4f5d95915bae902c89650a22573aafd596 Mon Sep 17 00:00:00 2001 From: Forrest Waters Date: Wed, 28 Oct 2020 19:56:05 -0500 Subject: [PATCH 059/198] remove commented code --- plugins/auth-backend/src/providers/onelogin/provider.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index d11baa605d..97cf182e5a 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -73,7 +73,6 @@ export class OneLoginProvider implements OAuthHandlers { idToken: params.id_token, accessToken, scope: params.scope, - //scope: 'profile', expiresInSeconds: params.expires_in, }, profile, From 05b0ed81f543a2eac67606bb4ad225b0d2b95d30 Mon Sep 17 00:00:00 2001 From: Forrest Waters Date: Thu, 29 Oct 2020 12:02:29 -0500 Subject: [PATCH 060/198] fix imports --- .../src/apis/implementations/auth/onelogin/OneLoginAuth.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts b/packages/core-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts index 7420b21510..ff5c7c1990 100644 --- a/packages/core-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts @@ -15,13 +15,13 @@ */ import OneLoginIcon from '@material-ui/icons/AcUnit'; -import { oneloginAuthApiRef } from '@backstage/core-api/src/apis/definitions/auth'; +import { oneloginAuthApiRef } from '../../../definitions/auth'; import { OAuthRequestApi, AuthProvider, DiscoveryApi, -} from '@backstage/core-api/src/apis/definitions'; -import { OAuth2 } from '@backstage/core-api/src/apis/implementations/auth/oauth2'; +} from '../../../definitions'; +import { OAuth2 } from '../oauth2'; type CreateOptions = { discoveryApi: DiscoveryApi; From 59e42b4ca77d7458ebf8648b5fdcb17be17424f4 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Thu, 29 Oct 2020 14:28:42 -0400 Subject: [PATCH 061/198] preserve enums in types --- .../cost-insights/src/api/CostInsightsApi.ts | 2 +- .../src/api/ExampleCostInsightsClient.ts | 8 ++-- .../AlertActionCard.test.tsx | 3 +- .../components/CostGrowth/CostGrowth.test.tsx | 13 +++-- .../src/components/CostGrowth/CostGrowth.tsx | 5 +- .../components/CostOverviewCard/selector.tsx | 3 +- .../CurrencySelect/CurrencySelect.tsx | 3 +- .../PeriodSelect/PeriodSelect.test.tsx | 3 +- .../components/PeriodSelect/PeriodSelect.tsx | 2 +- .../ProductInsightsCard.test.tsx | 3 +- .../ProductInsightsCard.tsx | 3 +- .../ProductInsightsCard/selector.ts | 3 +- .../ProjectGrowthAlertCard.tsx | 8 ++-- .../ProjectGrowthInstructionsPage.tsx | 4 +- .../ResourceGrowthBarChartLegend.test.tsx | 2 +- .../ResourceGrowthBarChartLegend.tsx | 3 +- plugins/cost-insights/src/index.ts | 3 +- .../src/types/{Alert.tsx => Alert.ts} | 45 ------------------ plugins/cost-insights/src/types/Currency.ts | 7 +++ .../src/{utils/icon.ts => types/Duration.ts} | 18 ++++--- plugins/cost-insights/src/types/Filters.ts | 2 +- plugins/cost-insights/src/types/Icon.ts | 9 ++++ plugins/cost-insights/src/types/index.ts | 1 + plugins/cost-insights/src/utils/alerts.tsx | 47 +++++++++++++++++++ plugins/cost-insights/src/utils/currency.ts | 10 +--- .../cost-insights/src/utils/duration.test.ts | 3 +- plugins/cost-insights/src/utils/duration.ts | 16 +------ plugins/cost-insights/src/utils/filters.ts | 3 +- .../src/utils/formatters.test.ts | 2 +- plugins/cost-insights/src/utils/formatters.ts | 2 +- plugins/cost-insights/src/utils/history.ts | 3 +- plugins/cost-insights/src/utils/loading.ts | 16 +++++++ plugins/cost-insights/src/utils/mockData.ts | 2 +- .../cost-insights/src/utils/navigation.tsx | 2 +- .../cost-insights/src/utils/public/index.ts | 20 -------- plugins/cost-insights/src/utils/tests.tsx | 3 +- 36 files changed, 136 insertions(+), 146 deletions(-) rename plugins/cost-insights/src/types/{Alert.tsx => Alert.ts} (59%) rename plugins/cost-insights/src/{utils/icon.ts => types/Duration.ts} (59%) create mode 100644 plugins/cost-insights/src/utils/alerts.tsx delete mode 100644 plugins/cost-insights/src/utils/public/index.ts diff --git a/plugins/cost-insights/src/api/CostInsightsApi.ts b/plugins/cost-insights/src/api/CostInsightsApi.ts index 8e165ba72a..be5cd54fdb 100644 --- a/plugins/cost-insights/src/api/CostInsightsApi.ts +++ b/plugins/cost-insights/src/api/CostInsightsApi.ts @@ -18,13 +18,13 @@ import { createApiRef } from '@backstage/core'; import { Alert, Cost, + Duration, Group, Project, ProductCost, Maybe, MetricData, } from '../types'; -import { Duration } from '../utils/duration'; export type CostInsightsApi = { /** diff --git a/plugins/cost-insights/src/api/ExampleCostInsightsClient.ts b/plugins/cost-insights/src/api/ExampleCostInsightsClient.ts index 159e2a2e7d..70695beb25 100644 --- a/plugins/cost-insights/src/api/ExampleCostInsightsClient.ts +++ b/plugins/cost-insights/src/api/ExampleCostInsightsClient.ts @@ -23,20 +23,22 @@ import { ChangeStatistic, Cost, DateAggregation, + Duration, Group, Maybe, MetricData, ProductCost, Project, - ProjectGrowthAlert, ProjectGrowthData, Trendline, - UnlabeledDataflowAlert, UnlabeledDataflowData, } from '../types'; +import { + ProjectGrowthAlert, + UnlabeledDataflowAlert +} from '../utils/alerts'; import { DEFAULT_DATE_FORMAT, - Duration, exclusiveEndDateOf, inclusiveStartDateOf, } from '../utils/duration'; diff --git a/plugins/cost-insights/src/components/AlertActionCardList/AlertActionCard.test.tsx b/plugins/cost-insights/src/components/AlertActionCardList/AlertActionCard.test.tsx index 3e2ca4f8c8..d896bf0d47 100644 --- a/plugins/cost-insights/src/components/AlertActionCardList/AlertActionCard.test.tsx +++ b/plugins/cost-insights/src/components/AlertActionCardList/AlertActionCard.test.tsx @@ -17,8 +17,9 @@ import React from 'react'; import { renderInTestApp } from '@backstage/test-utils'; import { AlertActionCard } from './AlertActionCard'; -import { ProjectGrowthAlert, ProjectGrowthData } from '../../types'; import { MockScrollProvider } from '../../utils/tests'; +import { ProjectGrowthAlert } from '../../utils/alerts'; +import { ProjectGrowthData } from '../../types'; const data: ProjectGrowthData = { aggregation: [500000.8, 970502.8], diff --git a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx index 7c319354aa..046394ca7a 100644 --- a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx +++ b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx @@ -17,11 +17,10 @@ import React, { PropsWithChildren } from 'react'; import { renderInTestApp } from '@backstage/test-utils'; import { CostGrowth } from './CostGrowth'; -import { Currency } from '../../types'; -import { Duration } from '../../utils/duration'; +import { Currency, CurrencyType, Duration } from '../../types'; import { findAlways } from '../../utils/assert'; import { MockConfigProvider, MockCurrencyProvider } from '../../utils/tests'; -import { defaultCurrencies, CurrencyType } from '../../utils/currency'; +import { defaultCurrencies } from '../../utils/currency'; const engineers = findAlways(defaultCurrencies, c => c.kind === null); const usd = findAlways(defaultCurrencies, c => c.kind === CurrencyType.USD); @@ -38,10 +37,10 @@ const MockContext = ({ currency: Currency; engineerCost: number; }>) => ( - - {children} - -); + + {children} + + ); describe.each` engineerCost | ratio | amount | expected diff --git a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx index bdb4e00394..3f5e0f55eb 100644 --- a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx +++ b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx @@ -16,9 +16,8 @@ import React from 'react'; import classnames from 'classnames'; -import { ChangeStatistic } from '../../types'; -import { rateOf, CurrencyType } from '../../utils/currency'; -import { Duration } from '../../utils/duration'; +import { ChangeStatistic, CurrencyType, Duration } from '../../types'; +import { rateOf } from '../../utils/currency'; import { growthOf, GrowthType, EngineerThreshold } from '../../utils/change'; import { useCostGrowthStyles as useStyles } from '../../utils/styles'; import { formatPercent, formatCurrency } from '../../utils/formatters'; diff --git a/plugins/cost-insights/src/components/CostOverviewCard/selector.tsx b/plugins/cost-insights/src/components/CostOverviewCard/selector.tsx index 3440500215..ef65f38dbb 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/selector.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/selector.tsx @@ -13,8 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Maybe, PageFilters } from '../../types'; -import { Duration } from '../../utils/duration'; +import { Duration, Maybe, PageFilters } from '../../types'; import { MapFiltersToProps } from '../../hooks/useFilters'; type CostOverviewFilterProps = PageFilters & { diff --git a/plugins/cost-insights/src/components/CurrencySelect/CurrencySelect.tsx b/plugins/cost-insights/src/components/CurrencySelect/CurrencySelect.tsx index 2780c728f3..1b5920ffcb 100644 --- a/plugins/cost-insights/src/components/CurrencySelect/CurrencySelect.tsx +++ b/plugins/cost-insights/src/components/CurrencySelect/CurrencySelect.tsx @@ -16,8 +16,7 @@ import React from 'react'; import { MenuItem, Select, SelectProps } from '@material-ui/core'; -import { Currency } from '../../types'; -import { CurrencyType } from '../../utils/currency'; +import { Currency, CurrencyType } from '../../types'; import { findAlways } from '../../utils/assert'; import { useSelectStyles as useStyles } from '../../utils/styles'; diff --git a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx index 67f40550f2..a3519ff61b 100644 --- a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx +++ b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx @@ -20,9 +20,8 @@ import { renderInTestApp } from '@backstage/test-utils'; import UserEvent from '@testing-library/user-event'; import { PeriodSelect, getDefaultOptions } from './PeriodSelect'; import { getDefaultPageFilters } from '../../utils/filters'; -import { Duration } from '../../utils/duration'; import { MockBillingDateProvider } from '../../utils/tests'; -import { Group } from '../../types'; +import { Group, Duration } from '../../types'; const DefaultPageFilters = getDefaultPageFilters([{ id: 'tools' }] as Group[]); const lastCompleteBillingDate = '2020-05-01'; diff --git a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.tsx b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.tsx index f1a80a55f5..4908641d6f 100644 --- a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.tsx +++ b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.tsx @@ -16,11 +16,11 @@ import React from 'react'; import { MenuItem, Select, SelectProps } from '@material-ui/core'; +import { Duration } from '../../types'; import { formatLastTwoLookaheadQuarters, formatLastTwoMonths, } from '../../utils/formatters'; -import { Duration } from '../../utils/duration'; import { findAlways } from '../../utils/assert'; import { useSelectStyles as useStyles } from '../../utils/styles'; import { useLastCompleteBillingDate } from '../../hooks'; diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx index f27d7349a3..9d1610e298 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx @@ -35,8 +35,7 @@ import { MockScrollProvider, MockLoadingProvider, } from '../../utils/tests'; -import { Duration } from '../../utils/duration'; -import { Product, ProductCost, ProductPeriod } from '../../types'; +import { Duration, Product, ProductCost, ProductPeriod } from '../../types'; const costInsightsApi = ( productCost: ProductCost, diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx index fec73f0c06..840c99522a 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx @@ -28,10 +28,9 @@ import { useLoading, useScroll, } from '../../hooks'; -import { Duration } from '../../utils/duration'; import { useProductInsightsCardStyles as useStyles } from '../../utils/styles'; import { mapFiltersToProps, mapLoadingToProps } from './selector'; -import { Maybe, Product, ProductCost } from '../../types'; +import { Duration, Maybe, Product, ProductCost } from '../../types'; import { pluralOf } from '../../utils/grammar'; import { formatPeriod } from '../../utils/formatters'; diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/selector.ts b/plugins/cost-insights/src/components/ProductInsightsCard/selector.ts index 6007070707..efc8448034 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/selector.ts +++ b/plugins/cost-insights/src/components/ProductInsightsCard/selector.ts @@ -16,8 +16,7 @@ import { MapFiltersToProps } from '../../hooks/useFilters'; import { MapLoadingToProps } from '../../hooks/useLoading'; -import { PageFilters, ProductPeriod } from '../../types'; -import { Duration } from '../../utils/duration'; +import { Duration, PageFilters, ProductPeriod } from '../../types'; import { findAlways } from '../../utils/assert'; type ProductInsightsCardFilterProps = PageFilters & { diff --git a/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.tsx b/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.tsx index c32afcd257..18f7fa151d 100644 --- a/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.tsx +++ b/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.tsx @@ -20,8 +20,7 @@ import { Box } from '@material-ui/core'; import { InfoCard } from '@backstage/core'; import { ResourceGrowthBarChart } from '../ResourceGrowthBarChart'; import { ResourceGrowthBarChartLegend } from '../ResourceGrowthBarChartLegend'; -import { ProjectGrowthData } from '../../types'; -import { Duration } from '../../utils/duration'; +import { Duration, ProjectGrowthData } from '../../types'; import { pluralOf } from '../../utils/grammar'; type ProjectGrowthAlertProps = { @@ -32,9 +31,8 @@ export const ProjectGrowthAlertCard = ({ alert }: ProjectGrowthAlertProps) => { const [costStart, costEnd] = alert.aggregation; const subheader = ` - ${alert.products.length} ${pluralOf(alert.products.length, 'product')}${ - alert.products.length > 1 ? ', sorted by cost' : '' - }`; + ${alert.products.length} ${pluralOf(alert.products.length, 'product')}${alert.products.length > 1 ? ', sorted by cost' : '' + }`; const previousName = moment(alert.periodStart, 'YYYY-[Q]Q').format( '[Q]Q YYYY', ); diff --git a/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx b/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx index 074d3e2b92..f2083388ae 100644 --- a/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx +++ b/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx @@ -18,14 +18,14 @@ import React from 'react'; import { Box, Typography } from '@material-ui/core'; import { InfoCard } from '@backstage/core'; import { AlertInstructionsLayout } from '../AlertInstructionsLayout'; -import { Duration } from '../../utils/duration'; import { Alert, + Duration, Entity, Product, - ProjectGrowthAlert, ProjectGrowthData, } from '../../types'; +import { ProjectGrowthAlert } from '../../utils/alerts'; import { ResourceGrowthBarChartLegend } from '../ResourceGrowthBarChartLegend'; import { ResourceGrowthBarChart } from '../ResourceGrowthBarChart'; diff --git a/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.test.tsx b/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.test.tsx index bf5c1021cc..47285a2363 100644 --- a/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.test.tsx +++ b/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.test.tsx @@ -17,10 +17,10 @@ import React, { PropsWithChildren } from 'react'; import { renderInTestApp } from '@backstage/test-utils'; import { ResourceGrowthBarChartLegend } from './ResourceGrowthBarChartLegend'; -import { Duration } from '../../utils/duration'; import { defaultCurrencies } from '../../utils/currency'; import { findAlways } from '../../utils/assert'; import { MockConfigProvider, MockCurrencyProvider } from '../../utils/tests'; +import { Duration } from '../../types'; const engineers = findAlways(defaultCurrencies, c => c.kind === null); diff --git a/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.tsx b/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.tsx index 0c281e6266..2e8e0f07a1 100644 --- a/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.tsx +++ b/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.tsx @@ -19,8 +19,7 @@ import { Box, useTheme } from '@material-ui/core'; import { LegendItem } from '../LegendItem'; import { CostGrowth } from '../CostGrowth'; import { currencyFormatter } from '../../utils/formatters'; -import { Duration } from '../../utils/duration'; -import { ChangeStatistic, CostInsightsTheme } from '../../types'; +import { ChangeStatistic, Duration, CostInsightsTheme } from '../../types'; export type ResourceGrowthBarChartLegendProps = { change: ChangeStatistic; diff --git a/plugins/cost-insights/src/index.ts b/plugins/cost-insights/src/index.ts index 119fc2035c..48b2425946 100644 --- a/plugins/cost-insights/src/index.ts +++ b/plugins/cost-insights/src/index.ts @@ -19,4 +19,5 @@ export * from './api'; export * from './components'; export { useCurrency } from './hooks'; export * from './types'; -export * from './utils/public'; +export * from './utils/tests'; +export * from './utils/duration'; diff --git a/plugins/cost-insights/src/types/Alert.tsx b/plugins/cost-insights/src/types/Alert.ts similarity index 59% rename from plugins/cost-insights/src/types/Alert.tsx rename to plugins/cost-insights/src/types/Alert.ts index 4ed55a844c..d6046e3241 100644 --- a/plugins/cost-insights/src/types/Alert.tsx +++ b/plugins/cost-insights/src/types/Alert.ts @@ -14,11 +14,8 @@ * limitations under the License. */ -import React from 'react'; import { ChangeStatistic } from './ChangeStatistic'; import { Maybe } from './Maybe'; -import { UnlabeledDataflowAlertCard } from '../components/UnlabeledDataflowAlertCard'; -import { ProjectGrowthAlertCard } from '../components/ProjectGrowthAlertCard'; /** * Generic alert type with required fields for display. The `element` field will be rendered in @@ -57,11 +54,6 @@ export enum DataKey { Name = 'name', } -/** - * The alerts below are examples of Alert implementation; the CostInsightsApi permits returning - * any implementation of the Alert type, so adopters can create their own. The CostInsightsApi - * fetches alert data from the backend, then creates Alert classes with the data. - */ export interface ProjectGrowthData { project: string; periodStart: string; @@ -71,26 +63,6 @@ export interface ProjectGrowthData { products: Array; } -export class ProjectGrowthAlert implements Alert { - data: ProjectGrowthData; - - constructor(data: ProjectGrowthData) { - this.data = data; - } - - get title() { - return `Investigate cost growth in project ${this.data.project}`; - } - - subtitle = - 'Cost growth outpacing business growth is unsustainable long-term.'; - url = '/cost-insights/investigating-growth'; - - get element() { - return ; - } -} - export interface UnlabeledDataflowData { periodStart: string; periodEnd: string; @@ -99,23 +71,6 @@ export interface UnlabeledDataflowData { labeledCost: number; } -export class UnlabeledDataflowAlert implements Alert { - data: UnlabeledDataflowData; - - constructor(data: UnlabeledDataflowData) { - this.data = data; - } - - title = 'Add labels to workflows'; - subtitle = - 'Labels show in billing data, enabling cost insights for each workflow.'; - url = '/cost-insights/labeling-jobs'; - - get element() { - return ; - } -} - export interface UnlabeledDataflowAlertProject { id: string; unlabeledCost: number; diff --git a/plugins/cost-insights/src/types/Currency.ts b/plugins/cost-insights/src/types/Currency.ts index 16ba432be8..8a8af81b0e 100644 --- a/plugins/cost-insights/src/types/Currency.ts +++ b/plugins/cost-insights/src/types/Currency.ts @@ -21,3 +21,10 @@ export interface Currency { prefix?: string; rate?: number; } + +export enum CurrencyType { + USD = 'USD', + CarbonOffsetTons = 'CARBON_OFFSET_TONS', + Beers = 'BEERS', + IceCream = 'PINTS_OF_ICE_CREAM', +} diff --git a/plugins/cost-insights/src/utils/icon.ts b/plugins/cost-insights/src/types/Duration.ts similarity index 59% rename from plugins/cost-insights/src/utils/icon.ts rename to plugins/cost-insights/src/types/Duration.ts index de957743ba..a1eb9aa31e 100644 --- a/plugins/cost-insights/src/utils/icon.ts +++ b/plugins/cost-insights/src/types/Duration.ts @@ -14,11 +14,15 @@ * limitations under the License. */ -export enum IconType { - Compute = 'compute', - Data = 'data', - Database = 'database', - Storage = 'storage', - Search = 'search', - ML = 'ml', +/** + * 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 - + * September 15. + */ +export enum Duration { + P30D = 'P30D', + P90D = 'P90D', + P1M = 'P1M', + P3M = 'P3M', } diff --git a/plugins/cost-insights/src/types/Filters.ts b/plugins/cost-insights/src/types/Filters.ts index da71471f50..120738fcc7 100644 --- a/plugins/cost-insights/src/types/Filters.ts +++ b/plugins/cost-insights/src/types/Filters.ts @@ -15,7 +15,7 @@ */ import { Maybe } from './Maybe'; -import { Duration } from '../utils/duration'; +import { Duration } from './Duration'; export interface PageFilters { group: Maybe; diff --git a/plugins/cost-insights/src/types/Icon.ts b/plugins/cost-insights/src/types/Icon.ts index db5520ca15..70f43a6813 100644 --- a/plugins/cost-insights/src/types/Icon.ts +++ b/plugins/cost-insights/src/types/Icon.ts @@ -18,3 +18,12 @@ export type Icon = { kind: string; component: JSX.Element; }; + +export enum IconType { + Compute = 'compute', + Data = 'data', + Database = 'database', + Storage = 'storage', + Search = 'search', + ML = 'ml', +} diff --git a/plugins/cost-insights/src/types/index.ts b/plugins/cost-insights/src/types/index.ts index cced505f86..398110ec80 100644 --- a/plugins/cost-insights/src/types/index.ts +++ b/plugins/cost-insights/src/types/index.ts @@ -19,6 +19,7 @@ export * from './ChangeStatistic'; export * from './ChartData'; export * from './Cost'; export * from './DateAggregation'; +export * from './Duration'; export * from './Currency'; export * from './Entity'; export * from './Icon'; diff --git a/plugins/cost-insights/src/utils/alerts.tsx b/plugins/cost-insights/src/utils/alerts.tsx new file mode 100644 index 0000000000..fcb41f1e3b --- /dev/null +++ b/plugins/cost-insights/src/utils/alerts.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { Alert, UnlabeledDataflowData, ProjectGrowthData } from '../types'; +import { UnlabeledDataflowAlertCard } from '../components/UnlabeledDataflowAlertCard'; +import { ProjectGrowthAlertCard } from '../components/ProjectGrowthAlertCard'; + +/** + * The alerts below are examples of Alert implementation; the CostInsightsApi permits returning + * any implementation of the Alert type, so adopters can create their own. The CostInsightsApi + * fetches alert data from the backend, then creates Alert classes with the data. + */ + +export class UnlabeledDataflowAlert implements Alert { + data: UnlabeledDataflowData; + + constructor(data: UnlabeledDataflowData) { + this.data = data; + } + + title = 'Add labels to workflows'; + subtitle = + 'Labels show in billing data, enabling cost insights for each workflow.'; + url = '/cost-insights/labeling-jobs'; + + get element() { + return ; + } +} + +export class ProjectGrowthAlert implements Alert { + data: ProjectGrowthData; + + constructor(data: ProjectGrowthData) { + this.data = data; + } + + get title() { + return `Investigate cost growth in project ${this.data.project}`; + } + + subtitle = + 'Cost growth outpacing business growth is unsustainable long-term.'; + url = '/cost-insights/investigating-growth'; + + get element() { + return ; + } +} diff --git a/plugins/cost-insights/src/utils/currency.ts b/plugins/cost-insights/src/utils/currency.ts index 7f43acc66b..663a29a112 100644 --- a/plugins/cost-insights/src/utils/currency.ts +++ b/plugins/cost-insights/src/utils/currency.ts @@ -13,17 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Currency } from '../types'; -import { Duration } from '../utils/duration'; +import { Currency, CurrencyType, Duration } from '../types'; import { assertNever } from '../utils/assert'; -export enum CurrencyType { - USD = 'USD', - CarbonOffsetTons = 'CARBON_OFFSET_TONS', - Beers = 'BEERS', - IceCream = 'PINTS_OF_ICE_CREAM', -} - export const rateOf = (cost: number, duration: Duration) => { switch (duration) { case Duration.P1M: diff --git a/plugins/cost-insights/src/utils/duration.test.ts b/plugins/cost-insights/src/utils/duration.test.ts index 675a90b54a..a5509eda07 100644 --- a/plugins/cost-insights/src/utils/duration.test.ts +++ b/plugins/cost-insights/src/utils/duration.test.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Duration, inclusiveEndDateOf, inclusiveStartDateOf } from './duration'; +import { Duration } from '../types'; +import { inclusiveEndDateOf, inclusiveStartDateOf } from './duration'; const lastCompleteBillingDate = '2020-06-05'; diff --git a/plugins/cost-insights/src/utils/duration.ts b/plugins/cost-insights/src/utils/duration.ts index 09034ae9df..d90e78dc5f 100644 --- a/plugins/cost-insights/src/utils/duration.ts +++ b/plugins/cost-insights/src/utils/duration.ts @@ -15,20 +15,8 @@ */ import moment from 'moment'; -import { assertNever } from '../utils/assert'; - -/** - * 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 - - * September 15. - */ -export enum Duration { - P30D = 'P30D', - P90D = 'P90D', - P1M = 'P1M', - P3M = 'P3M', -} +import { Duration } from '../types'; +import { assertNever } from './assert'; export const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD'; diff --git a/plugins/cost-insights/src/utils/filters.ts b/plugins/cost-insights/src/utils/filters.ts index 26d2bc6cf3..fbe1bf46cc 100644 --- a/plugins/cost-insights/src/utils/filters.ts +++ b/plugins/cost-insights/src/utils/filters.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { Group, PageFilters } from '../types'; -import { Duration } from './duration'; +import { Duration, Group, PageFilters } from '../types'; export function getDefaultPageFilters(groups: Group[]): PageFilters { return { diff --git a/plugins/cost-insights/src/utils/formatters.test.ts b/plugins/cost-insights/src/utils/formatters.test.ts index 7be6e1dffb..69e9086be1 100644 --- a/plugins/cost-insights/src/utils/formatters.test.ts +++ b/plugins/cost-insights/src/utils/formatters.test.ts @@ -19,7 +19,7 @@ import { lengthyCurrencyFormatter, quarterOf, } from './formatters'; -import { Duration } from '../utils/duration'; +import { Duration } from '../types'; Date.now = jest.fn(() => new Date(Date.parse('2019-12-07')).valueOf()); diff --git a/plugins/cost-insights/src/utils/formatters.ts b/plugins/cost-insights/src/utils/formatters.ts index 48b0b77188..fc563bf836 100644 --- a/plugins/cost-insights/src/utils/formatters.ts +++ b/plugins/cost-insights/src/utils/formatters.ts @@ -15,8 +15,8 @@ */ import moment from 'moment'; +import { Duration } from '../types'; import { - Duration, inclusiveEndDateOf, inclusiveStartDateOf, } from '../utils/duration'; diff --git a/plugins/cost-insights/src/utils/history.ts b/plugins/cost-insights/src/utils/history.ts index 36f4e9a5b0..7d45cfdad2 100644 --- a/plugins/cost-insights/src/utils/history.ts +++ b/plugins/cost-insights/src/utils/history.ts @@ -16,8 +16,7 @@ import qs from 'qs'; import * as yup from 'yup'; -import { Group, PageFilters } from '../types'; -import { Duration } from '../utils/duration'; +import { Duration, Group, PageFilters } from '../types'; import { getDefaultPageFilters } from '../utils/filters'; import { ConfigContextProps } from '../hooks/useConfig'; diff --git a/plugins/cost-insights/src/utils/loading.ts b/plugins/cost-insights/src/utils/loading.ts index 7a0225154f..4652603926 100644 --- a/plugins/cost-insights/src/utils/loading.ts +++ b/plugins/cost-insights/src/utils/loading.ts @@ -1,3 +1,19 @@ +/* + * 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 { Loading } from '../types'; export enum DefaultLoadingAction { diff --git a/plugins/cost-insights/src/utils/mockData.ts b/plugins/cost-insights/src/utils/mockData.ts index 91cd2f8ad2..58ba4a4251 100644 --- a/plugins/cost-insights/src/utils/mockData.ts +++ b/plugins/cost-insights/src/utils/mockData.ts @@ -17,6 +17,7 @@ import { Config } from '@backstage/config'; import { ConfigApi } from '@backstage/core'; import { + Duration, Entity, Product, ProductCost, @@ -30,7 +31,6 @@ import { getDefaultState as getDefaultLoadingState, } from '../utils/loading'; import { findAlways } from '../utils/assert'; -import { Duration } from '../utils/duration'; type mockAlertRenderer = (alert: T) => T; type mockEntityRenderer = (entity: T) => T; diff --git a/plugins/cost-insights/src/utils/navigation.tsx b/plugins/cost-insights/src/utils/navigation.tsx index 39016f6092..d2cb1d20bb 100644 --- a/plugins/cost-insights/src/utils/navigation.tsx +++ b/plugins/cost-insights/src/utils/navigation.tsx @@ -23,7 +23,7 @@ import Search from '@material-ui/icons/Search'; import CloudQueue from '@material-ui/icons/CloudQueue'; import School from '@material-ui/icons/School'; import ViewHeadline from '@material-ui/icons/ViewHeadline'; -import { IconType } from '../utils/icon'; +import { IconType } from '../types'; export enum DefaultNavigation { CostOverviewCard = 'cost-overview-card', diff --git a/plugins/cost-insights/src/utils/public/index.ts b/plugins/cost-insights/src/utils/public/index.ts deleted file mode 100644 index ffc2d5c882..0000000000 --- a/plugins/cost-insights/src/utils/public/index.ts +++ /dev/null @@ -1,20 +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. - */ - -export * from '../tests'; -export { Duration } from '../duration'; -export { CurrencyType } from '../currency'; -export { IconType } from '../icon'; diff --git a/plugins/cost-insights/src/utils/tests.tsx b/plugins/cost-insights/src/utils/tests.tsx index 09eab89139..852330acc3 100644 --- a/plugins/cost-insights/src/utils/tests.tsx +++ b/plugins/cost-insights/src/utils/tests.tsx @@ -32,8 +32,7 @@ import { BillingDateContextProps, } from '../hooks/useLastCompleteBillingDate'; import { ScrollContext, ScrollContextProps } from '../hooks/useScroll'; -import { Duration } from '../utils/duration'; -import { Group } from '../types'; +import { Group, Duration } from '../types'; /* Mock Providers and types are exposed publicly to allow users to test custom implementations From bbf52b659570988c6d9f0b2cdddaba9a16981750 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Thu, 29 Oct 2020 14:43:02 -0400 Subject: [PATCH 062/198] fix alert link --- plugins/cost-insights/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cost-insights/README.md b/plugins/cost-insights/README.md index 8c2fd11761..3b6e1e4fd5 100644 --- a/plugins/cost-insights/README.md +++ b/plugins/cost-insights/README.md @@ -106,7 +106,7 @@ costInsights: ## Alerts -The CostInsightsApi `getAlerts` method may return any type of alert or recommendation (called collectively "Action Items" in Cost Insights) that implements the [Alert type](https://github.com/spotify/backstage/blob/master/plugins/cost-insights/src/types/Alert.tsx). This allows you to deliver any alerts or recommendations specific to your infrastructure or company migrations. +The CostInsightsApi `getAlerts` method may return any type of alert or recommendation (called collectively "Action Items" in Cost Insights) that implements the [Alert type](https://github.com/spotify/backstage/blob/master/plugins/cost-insights/src/types/Alert.ts). This allows you to deliver any alerts or recommendations specific to your infrastructure or company migrations. The Alert type includes an `element` field to supply the JSX Element that will be rendered in the Cost Insights "Action Items" section; we recommend using Backstage's [InfoCard](https://backstage.io/storybook/?path=/story/layout-information-card--default) and [Recharts](http://recharts.org/en-US/) to show actionable visualizations. From 948052cbb4b692bb9a6ad444cc8f19431e763a12 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 21 Oct 2020 09:34:43 +0200 Subject: [PATCH 063/198] feat: add ability to dry run adding a new location The location is now added in a transaction and afterwards rolled back. This allows users to dry run this operation to see if there entity has issues. This is probably done my automated tools. --- .changeset/angry-crabs-relate.md | 9 ++ .../features/software-catalog/installation.md | 1 + .../catalog/DatabaseEntitiesCatalog.test.ts | 46 +++++++-- .../src/catalog/DatabaseEntitiesCatalog.ts | 42 +++++--- .../src/catalog/DatabaseLocationsCatalog.ts | 18 +++- plugins/catalog-backend/src/catalog/types.ts | 14 ++- .../src/database/CommonDatabase.test.ts | 5 +- .../src/database/CommonDatabase.ts | 60 +++++++---- plugins/catalog-backend/src/database/index.ts | 1 + plugins/catalog-backend/src/database/types.ts | 33 +++++-- .../ingestion/HigherOrderOperations.test.ts | 70 ++++++++++++- .../src/ingestion/HigherOrderOperations.ts | 72 ++++++++------ .../catalog-backend/src/ingestion/types.ts | 5 +- .../src/service/CatalogBuilder.ts | 1 + .../src/service/router.test.ts | 99 +++++++++++++------ plugins/catalog-backend/src/service/router.ts | 40 ++++---- 16 files changed, 378 insertions(+), 138 deletions(-) create mode 100644 .changeset/angry-crabs-relate.md diff --git a/.changeset/angry-crabs-relate.md b/.changeset/angry-crabs-relate.md new file mode 100644 index 0000000000..66cdb29be5 --- /dev/null +++ b/.changeset/angry-crabs-relate.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Add ability to dry run adding a new location ot the catalog API. + +The location is now added in a transaction and afterwards rolled back. +This allows users to dry run this operation to see if there entity has issues. +This is probably done by automated tools in the CI/CD pipeline. diff --git a/docs/features/software-catalog/installation.md b/docs/features/software-catalog/installation.md index 26990033aa..1e037461b4 100644 --- a/docs/features/software-catalog/installation.md +++ b/docs/features/software-catalog/installation.md @@ -122,6 +122,7 @@ export default async function createPlugin({ entitiesCatalog, locationsCatalog, locationReader, + db, logger, ); diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts index 6407000b6f..7e751bab92 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts @@ -16,12 +16,13 @@ import { getVoidLogger } from '@backstage/backend-common'; import type { Entity } from '@backstage/catalog-model'; -import { Database, DatabaseManager } from '../database'; +import { Database, DatabaseManager, Transaction } from '../database'; import { DatabaseEntitiesCatalog } from './DatabaseEntitiesCatalog'; import { EntityUpsertRequest } from './types'; describe('DatabaseEntitiesCatalog', () => { let db: jest.Mocked; + let transaction: jest.Mocked; beforeAll(() => { db = { @@ -40,11 +41,14 @@ describe('DatabaseEntitiesCatalog', () => { locationHistory: jest.fn(), addLocationUpdateLogEvent: jest.fn(), }; + transaction = { + rollback: jest.fn(), + }; }); beforeEach(() => { jest.resetAllMocks(); - db.transaction.mockImplementation(async f => f('tx')); + db.transaction.mockImplementation(async f => f(transaction)); }); describe('batchAddOrUpdateEntities', () => { @@ -82,6 +86,36 @@ describe('DatabaseEntitiesCatalog', () => { expect(result).toEqual([{ entityId: 'u' }]); }); + it('adds using existing transaction', async () => { + const entity: Entity = { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'c', + namespace: 'd', + }, + }; + + const existingTransaction: jest.Mocked = { + rollback: jest.fn(), + }; + + db.entities.mockResolvedValue([]); + db.addEntities.mockResolvedValue([{ entity }]); + + const catalog = new DatabaseEntitiesCatalog(db, getVoidLogger()); + const result = await catalog.addOrUpdateEntity(entity, { + tx: existingTransaction, + }); + + expect(db.addEntities).toHaveBeenCalledTimes(1); + expect(db.addEntities).toHaveBeenCalledWith( + existingTransaction, + expect.anything(), + ); + expect(result).toBe(entity); + }); + it('updates when given uid', async () => { const entity: Entity = { apiVersion: 'a', @@ -131,10 +165,10 @@ describe('DatabaseEntitiesCatalog', () => { ]); expect(db.entityByName).not.toHaveBeenCalled(); expect(db.entityByUid).toHaveBeenCalledTimes(1); - expect(db.entityByUid).toHaveBeenCalledWith(expect.anything(), 'u'); + expect(db.entityByUid).toHaveBeenCalledWith(transaction, 'u'); expect(db.updateEntity).toHaveBeenCalledTimes(1); expect(db.updateEntity).toHaveBeenCalledWith( - expect.anything(), + transaction, { entity: { apiVersion: 'a', @@ -206,14 +240,14 @@ describe('DatabaseEntitiesCatalog', () => { }, ]); expect(db.entityByName).toHaveBeenCalledTimes(1); - expect(db.entityByName).toHaveBeenCalledWith(expect.anything(), { + expect(db.entityByName).toHaveBeenCalledWith(transaction, { kind: 'b', namespace: 'd', name: 'c', }); expect(db.updateEntity).toHaveBeenCalledTimes(1); expect(db.updateEntity).toHaveBeenCalledWith( - expect.anything(), + transaction, { entity: { apiVersion: 'a', diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index 947736dc06..28a972e8e3 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -27,7 +27,12 @@ import { import { chunk, groupBy } from 'lodash'; import limiterFactory from 'p-limit'; import { Logger } from 'winston'; -import type { Database, DbEntityResponse, EntityFilters } from '../database'; +import type { + Database, + DbEntityResponse, + EntityFilters, + Transaction, +} from '../database'; import { durationText } from '../util/timing'; import type { EntitiesCatalog, @@ -60,7 +65,14 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { private readonly logger: Logger, ) {} - async entities(filters?: EntityFilters[]): Promise { + async entities(options?: { + filters?: EntityFilters[]; + tx?: Transaction; + }): Promise { + const filters = options?.filters; + + // TODO: Support with and without transaction! + const items = await this.database.transaction(tx => this.database.entities(tx, filters), ); @@ -135,8 +147,10 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { */ async batchAddOrUpdateEntities( requests: EntityUpsertRequest[], - locationId?: string, + options?: { locationId?: string; tx?: Transaction }, ): Promise { + // TODO: How to work with the transaction here? + // Group the entities by unique kind+namespace combinations const entitiesByKindAndNamespace = groupBy(requests, ({ entity }) => { const name = getEntityName(entity); @@ -163,7 +177,11 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { ); // Retry the batch write a few times to deal with contention - const context = { kind, namespace, locationId }; + const context = { + kind, + namespace, + locationId: options?.locationId, + }; for (let attempt = 1; attempt <= BATCH_ATTEMPTS; ++attempt) { try { const { toAdd, toUpdate, toIgnore } = await this.analyzeBatch( @@ -234,13 +252,15 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { const markTimestamp = process.hrtime(); const names = requests.map(({ entity }) => entity.metadata.name); - const oldEntities = await this.entities([ - { - kind: kind, - 'metadata.namespace': namespace, - 'metadata.name': names, - }, - ]); + const oldEntities = await this.entities({ + filters: [ + { + kind: kind, + 'metadata.namespace': namespace, + 'metadata.name': names, + }, + ], + }); const oldEntitiesByName = new Map( oldEntities.map(e => [e.metadata.name, e]), diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts index f515829b91..b78b9933b9 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts @@ -15,7 +15,7 @@ */ import { Location } from '@backstage/catalog-model'; -import type { Database } from '../database'; +import type { Database, Transaction } from '../database'; import { DatabaseLocationUpdateLogEvent, DatabaseLocationUpdateLogStatus, @@ -25,9 +25,19 @@ import { LocationResponse, LocationsCatalog } from './types'; export class DatabaseLocationsCatalog implements LocationsCatalog { constructor(private readonly database: Database) {} - async addLocation(location: Location): Promise { - const added = await this.database.addLocation(location); - return added; + async addLocation( + location: Location, + options?: { tx?: Transaction }, + ): Promise { + const transaction = options?.tx; + + if (transaction) { + return await this.database.addLocation(transaction, location); + } + + return await this.database.transaction( + async tx => await this.database.addLocation(tx, location), + ); } async removeLocation(id: string): Promise { diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index 539a0e531f..1ac06618b5 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -15,7 +15,7 @@ */ import { Entity, EntityRelationSpec, Location } from '@backstage/catalog-model'; -import type { EntityFilters } from '../database'; +import type { EntityFilters, Transaction } from '../database'; // // Entities @@ -31,7 +31,10 @@ export type EntityUpsertResponse = { }; export type EntitiesCatalog = { - entities(filters?: EntityFilters[]): Promise; + entities(options?: { + filters?: EntityFilters[]; + tx?: Transaction; + }): Promise; removeEntityByUid(uid: string): Promise; /** @@ -42,7 +45,7 @@ export type EntitiesCatalog = { */ batchAddOrUpdateEntities( entities: EntityUpsertRequest[], - locationId?: string, + options?: { locationId?: string; tx?: Transaction }, ): Promise; }; @@ -70,7 +73,10 @@ export type LocationResponse = { }; export type LocationsCatalog = { - addLocation(location: Location): Promise; + addLocation( + location: Location, + options?: { tx?: Transaction }, + ): Promise; removeLocation(id: string): Promise; locations(): Promise; location(id: string): Promise; diff --git a/plugins/catalog-backend/src/database/CommonDatabase.test.ts b/plugins/catalog-backend/src/database/CommonDatabase.test.ts index ac11f1dd1a..b2a238b111 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.test.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.test.ts @@ -91,7 +91,7 @@ describe('CommonDatabase', () => { timestamp: null, }; - await db.addLocation(input); + await db.transaction(async tx => await db.addLocation(tx, input)); const locations = await db.locations(); expect(locations).toEqual( @@ -238,7 +238,8 @@ describe('CommonDatabase', () => { type: 'a', target: 'b', }; - await db.addLocation(location); + + await db.transaction(async tx => await db.addLocation(tx, location)); await db.addLocationUpdateLogEvent( 'dd12620d-0436-422f-93bd-929aa0788123', diff --git a/plugins/catalog-backend/src/database/CommonDatabase.ts b/plugins/catalog-backend/src/database/CommonDatabase.ts index 72e6bf0a5f..bd6dc29abc 100644 --- a/plugins/catalog-backend/src/database/CommonDatabase.ts +++ b/plugins/catalog-backend/src/database/CommonDatabase.ts @@ -46,6 +46,7 @@ import { DbLocationsRow, DbLocationsRowWithStatus, EntityFilters, + Transaction, } from './types'; // The number of items that are sent per batch to the database layer, when @@ -63,9 +64,23 @@ export class CommonDatabase implements Database { private readonly logger: Logger, ) {} - async transaction(fn: (tx: unknown) => Promise): Promise { + async transaction(fn: (tx: Transaction) => Promise): Promise { try { - return await this.database.transaction(fn); + let result: T | undefined = undefined; + + await this.database.transaction( + async tx => { + // We can't return here, as knex swallows the return type in case the transaction is rolled back: + // https://github.com/knex/knex/blob/e37aeaa31c8ef9c1b07d2e4d3ec6607e557d800d/lib/transaction.js#L136 + result = await fn(tx); + }, + { + // If we explicitly trigger a rollback, don't fail. + doNotRejectOnRollback: true, + }, + ); + + return result!; } catch (e) { this.logger.debug(`Error during transaction, ${e}`); @@ -81,7 +96,7 @@ export class CommonDatabase implements Database { } async addEntity( - txOpaque: unknown, + txOpaque: Transaction, request: DbEntityRequest, ): Promise { const tx = txOpaque as Knex.Transaction; @@ -110,7 +125,7 @@ export class CommonDatabase implements Database { } async addEntities( - txOpaque: unknown, + txOpaque: Transaction, request: DbEntityRequest[], ): Promise { const tx = txOpaque as Knex.Transaction; @@ -158,7 +173,7 @@ export class CommonDatabase implements Database { } async updateEntity( - txOpaque: unknown, + txOpaque: Transaction, request: DbEntityRequest, matchingEtag?: string, matchingGeneration?: number, @@ -217,7 +232,7 @@ export class CommonDatabase implements Database { } async entities( - txOpaque: unknown, + txOpaque: Transaction, filters?: EntityFilters[], ): Promise { const tx = txOpaque as Knex.Transaction; @@ -295,7 +310,7 @@ export class CommonDatabase implements Database { } async entityByName( - txOpaque: unknown, + txOpaque: Transaction, name: EntityName, ): Promise { const tx = txOpaque as Knex.Transaction; @@ -314,7 +329,7 @@ export class CommonDatabase implements Database { } async entityByUid( - txOpaque: unknown, + txOpaque: Transaction, uid: string, ): Promise { const tx = txOpaque as Knex.Transaction; @@ -330,7 +345,7 @@ export class CommonDatabase implements Database { return this.toEntityResponse(tx, rows[0]); } - async removeEntityByUid(txOpaque: unknown, uid: string): Promise { + async removeEntityByUid(txOpaque: Transaction, uid: string): Promise { const tx = txOpaque as Knex.Transaction; const result = await tx('entities').where({ id: uid }).del(); @@ -341,7 +356,7 @@ export class CommonDatabase implements Database { } async setRelations( - txOpaque: unknown, + txOpaque: Transaction, originatingEntityId: string, relations: EntityRelationSpec[], ): Promise { @@ -368,19 +383,22 @@ export class CommonDatabase implements Database { await tx.batchInsert('entities_relations', relationsRows, BATCH_SIZE); } - async addLocation(location: Location): Promise { - return await this.database.transaction(async tx => { - const row: DbLocationsRow = { - id: location.id, - type: location.type, - target: location.target, - }; - await tx('locations').insert(row); - return row; - }); + async addLocation( + txOpaque: Transaction, + location: Location, + ): Promise { + const tx = txOpaque as Knex.Transaction; + + const row: DbLocationsRow = { + id: location.id, + type: location.type, + target: location.target, + }; + await tx('locations').insert(row); + return row; } - async removeLocation(txOpaque: unknown, id: string): Promise { + async removeLocation(txOpaque: Transaction, id: string): Promise { const tx = txOpaque as Knex.Transaction; await tx('entities') diff --git a/plugins/catalog-backend/src/database/index.ts b/plugins/catalog-backend/src/database/index.ts index 565a41cfb2..0d84106815 100644 --- a/plugins/catalog-backend/src/database/index.ts +++ b/plugins/catalog-backend/src/database/index.ts @@ -22,4 +22,5 @@ export type { DbEntityResponse, EntityFilter, EntityFilters, + Transaction, } from './types'; diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index b8a2a6231a..62b2d55b88 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -101,6 +101,13 @@ export type EntityFilter = null | string | (null | string)[]; */ export type EntityFilters = Record; +/** + * An abstraction for transactions of the underlying database technology. + */ +export type Transaction = { + rollback(): Promise; +}; + /** * An abstraction on top of the underlying database, wrapping the basic CRUD * needs. @@ -114,7 +121,7 @@ export type Database = { * * @param fn The callback that implements the transaction */ - transaction(fn: (tx: unknown) => Promise): Promise; + transaction(fn: (tx: Transaction) => Promise): Promise; /** * Adds a set of new entities to the catalog. @@ -123,7 +130,7 @@ export type Database = { * @param request The entities being added */ addEntities( - tx: unknown, + tx: Transaction, request: DbEntityRequest[], ): Promise; @@ -147,22 +154,28 @@ export type Database = { * @returns The updated entity */ updateEntity( - tx: unknown, + tx: Transaction, request: DbEntityRequest, matchingEtag?: string, matchingGeneration?: number, ): Promise; - entities(tx: unknown, filters?: EntityFilters[]): Promise; + entities( + tx: Transaction, + filters?: EntityFilters[], + ): Promise; entityByName( - tx: unknown, + tx: Transaction, name: EntityName, ): Promise; - entityByUid(tx: unknown, uid: string): Promise; + entityByUid( + tx: Transaction, + uid: string, + ): Promise; - removeEntityByUid(tx: unknown, uid: string): Promise; + removeEntityByUid(tx: Transaction, uid: string): Promise; /** * Remove current relations for the entity and replace them with the new relations array @@ -171,14 +184,14 @@ export type Database = { * @param relations the relationships to be set */ setRelations( - tx: unknown, + tx: Transaction, entityUid: string, relations: EntityRelationSpec[], ): Promise; - addLocation(location: Location): Promise; + addLocation(tx: Transaction, location: Location): Promise; - removeLocation(tx: unknown, id: string): Promise; + removeLocation(tx: Transaction, id: string): Promise; location(id: string): Promise; diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts index 933433b47e..174428799d 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts @@ -18,7 +18,11 @@ import { getVoidLogger } from '@backstage/backend-common'; import { Entity, Location, LocationSpec } from '@backstage/catalog-model'; import { EntitiesCatalog, LocationsCatalog } from '../catalog'; import { LocationUpdateStatus } from '../catalog/types'; -import { DatabaseLocationUpdateLogStatus } from '../database/types'; +import { + Database, + DatabaseLocationUpdateLogStatus, + Transaction, +} from '../database/types'; import { HigherOrderOperations } from './HigherOrderOperations'; import { LocationReader } from './types'; @@ -26,6 +30,8 @@ describe('HigherOrderOperations', () => { let entitiesCatalog: jest.Mocked; let locationsCatalog: jest.Mocked; let locationReader: jest.Mocked; + let transaction: jest.Mocked; + let database: jest.Mocked; let higherOrderOperation: HigherOrderOperations; beforeAll(() => { @@ -46,10 +52,29 @@ describe('HigherOrderOperations', () => { locationReader = { read: jest.fn(), }; + transaction = { + rollback: jest.fn(), + }; + database = { + transaction: jest.fn(), + addEntities: jest.fn(), + updateEntity: jest.fn(), + entities: jest.fn(), + entityByName: jest.fn(), + entityByUid: jest.fn(), + removeEntityByUid: jest.fn(), + addLocation: jest.fn(), + removeLocation: jest.fn(), + location: jest.fn(), + locations: jest.fn(), + locationHistory: jest.fn(), + addLocationUpdateLogEvent: jest.fn(), + }; higherOrderOperation = new HigherOrderOperations( entitiesCatalog, locationsCatalog, locationReader, + database, getVoidLogger(), ); }); @@ -64,6 +89,7 @@ describe('HigherOrderOperations', () => { type: 'a', target: 'b', }; + database.transaction.mockImplementation(x => x(transaction)); locationsCatalog.addLocation.mockImplementation(x => Promise.resolve(x)); locationsCatalog.locations.mockResolvedValue([]); locationReader.read.mockResolvedValue({ @@ -90,7 +116,9 @@ describe('HigherOrderOperations', () => { id: expect.anything(), ...spec, }), + { tx: transaction }, ); + expect(transaction.rollback).not.toBeCalled(); }); it('reuses the location if a match already existed', async () => { @@ -103,6 +131,7 @@ describe('HigherOrderOperations', () => { ...spec, }; + database.transaction.mockImplementation(x => x(transaction)); locationsCatalog.locations.mockResolvedValue([ { currentStatus: { timestamp: '', status: '', message: '' }, @@ -123,6 +152,7 @@ describe('HigherOrderOperations', () => { expect(locationReader.read).toBeCalledWith({ type: 'a', target: 'b' }); expect(entitiesCatalog.batchAddOrUpdateEntities).not.toBeCalled(); expect(locationsCatalog.addLocation).not.toBeCalled(); + expect(transaction.rollback).not.toBeCalled(); }); it('rejects the whole operation if any entity could not be read', async () => { @@ -137,6 +167,7 @@ describe('HigherOrderOperations', () => { metadata: { name: 'n' }, }; + database.transaction.mockImplementation(x => x(transaction)); locationsCatalog.locations.mockResolvedValue([]); locationReader.read.mockResolvedValue({ entities: [{ entity, location, relations: [] }], @@ -149,6 +180,43 @@ describe('HigherOrderOperations', () => { expect(locationsCatalog.locations).toBeCalledTimes(1); expect(entitiesCatalog.batchAddOrUpdateEntities).not.toBeCalled(); expect(locationsCatalog.addLocation).not.toBeCalled(); + expect(transaction.rollback).not.toBeCalled(); + }); + + it('rollback everything after a dry run', async () => { + const spec = { + type: 'a', + target: 'b', + }; + database.transaction.mockImplementation(x => x(transaction)); + locationsCatalog.addLocation.mockImplementation(x => Promise.resolve(x)); + locationsCatalog.locations.mockResolvedValue([]); + locationReader.read.mockResolvedValue({ entities: [], errors: [] }); + + const result = await higherOrderOperation.addLocation(spec, { + dryRun: true, + }); + + expect(result.location).toEqual( + expect.objectContaining({ + id: expect.anything(), + ...spec, + }), + ); + expect(result.entities).toEqual([]); + expect(locationsCatalog.locations).toBeCalledTimes(1); + expect(locationReader.read).toBeCalledTimes(1); + expect(locationReader.read).toBeCalledWith({ type: 'a', target: 'b' }); + expect(entitiesCatalog.addOrUpdateEntity).not.toBeCalled(); + expect(locationsCatalog.addLocation).toBeCalledTimes(1); + expect(locationsCatalog.addLocation).toBeCalledWith( + expect.objectContaining({ + id: expect.anything(), + ...spec, + }), + { tx: transaction }, + ); + expect(transaction.rollback).toBeCalled(); }); }); diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts index 0e62815be5..2ca79c60b4 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts @@ -17,6 +17,7 @@ import { Location, LocationSpec } from '@backstage/catalog-model'; import { v4 as uuidv4 } from 'uuid'; import { Logger } from 'winston'; +import { Database } from '../database'; import { EntitiesCatalog, LocationsCatalog } from '../catalog'; import { durationText } from '../util/timing'; import { @@ -33,22 +34,13 @@ import { * database more directly. */ export class HigherOrderOperations implements HigherOrderOperation { - private readonly entitiesCatalog: EntitiesCatalog; - private readonly locationsCatalog: LocationsCatalog; - private readonly locationReader: LocationReader; - private readonly logger: Logger; - constructor( - entitiesCatalog: EntitiesCatalog, - locationsCatalog: LocationsCatalog, - locationReader: LocationReader, - logger: Logger, - ) { - this.entitiesCatalog = entitiesCatalog; - this.locationsCatalog = locationsCatalog; - this.locationReader = locationReader; - this.logger = logger; - } + private readonly entitiesCatalog: EntitiesCatalog, + private readonly locationsCatalog: LocationsCatalog, + private readonly locationReader: LocationReader, + private readonly database: Database, + private readonly logger: Logger, + ) {} /** * Adds a single location to the catalog. @@ -62,7 +54,12 @@ export class HigherOrderOperations implements HigherOrderOperation { * * @param spec The location to add */ - async addLocation(spec: LocationSpec): Promise { + async addLocation( + spec: LocationSpec, + options?: { dryRun?: boolean }, + ): Promise { + const dryRun = options?.dryRun || false; + // Attempt to find a previous location matching the spec const previousLocations = await this.locationsCatalog.locations(); const previousLocation = previousLocations.find( @@ -88,21 +85,36 @@ export class HigherOrderOperations implements HigherOrderOperation { // in the entities list. But we aren't sure what to do about those yet. // Write - if (!previousLocation) { - await this.locationsCatalog.addLocation(location); - } - if (readerOutput.entities.length === 0) { - return { location, entities: [] }; - } + const entities = await this.database.transaction(async tx => { + if (!previousLocation) { + await this.locationsCatalog.addLocation(location, { tx }); + } + if (readerOutput.entities.length === 0) { + return { location, entities: [] }; + } - const writtenEntities = await this.entitiesCatalog.batchAddOrUpdateEntities( - readerOutput.entities, - location.id, - ); + const writtenEntities = await this.entitiesCatalog.batchAddOrUpdateEntities( + readerOutput.entities, + { + locationId: location.id, + tx, + }, + ); - const entities = await this.entitiesCatalog.entities([ - { 'metadata.uid': writtenEntities.map(e => e.entityId) }, - ]); + const outputEntities = await this.entitiesCatalog.entities( + [{ 'metadata.uid': writtenEntities.map(e => e.entityId) }], + { tx }, + ); + + if (dryRun) { + // If this is only a dry run, cancel the database transaction even if it was successful. + await tx.rollback(); + + this.logger.debug(`Perfomed successful dry run of adding a location`); + } + + return outputEntities; + }); return { location, entities }; } @@ -168,7 +180,7 @@ export class HigherOrderOperations implements HigherOrderOperation { try { await this.entitiesCatalog.batchAddOrUpdateEntities( readerOutput.entities, - location.id, + { locationId: location.id }, ); } catch (e) { for (const entity of readerOutput.entities) { diff --git a/plugins/catalog-backend/src/ingestion/types.ts b/plugins/catalog-backend/src/ingestion/types.ts index c586e2d438..a83b4adecc 100644 --- a/plugins/catalog-backend/src/ingestion/types.ts +++ b/plugins/catalog-backend/src/ingestion/types.ts @@ -26,7 +26,10 @@ import type { // export type HigherOrderOperation = { - addLocation(spec: LocationSpec): Promise; + addLocation( + spec: LocationSpec, + options?: { dryRun?: boolean }, + ): Promise; refreshAllLocations(): Promise; }; diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index b041dac5ab..f2a4ac7468 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -227,6 +227,7 @@ export class CatalogBuilder { entitiesCatalog, locationsCatalog, locationReader, + db, logger, ); diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index a6b22eaad9..3a43ac96ad 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -23,6 +23,8 @@ import { LocationResponse } from '../catalog/types'; import { HigherOrderOperation } from '../ingestion/types'; import { createRouter } from './router'; +// TODO: ... + describe('createRouter', () => { let entitiesCatalog: jest.Mocked; let locationsCatalog: jest.Mocked; @@ -83,13 +85,15 @@ describe('createRouter', () => { expect(response.status).toEqual(200); expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entities).toHaveBeenCalledWith([ - { - a: ['1', null, '3'], - b: ['4'], - }, - { c: [null] }, - ]); + expect(entitiesCatalog.entities).toHaveBeenCalledWith({ + filters: [ + { + a: ['1', null, '3'], + b: ['4'], + }, + { c: [null] }, + ], + }); }); }); @@ -107,9 +111,9 @@ describe('createRouter', () => { const response = await request(app).get('/entities/by-uid/zzz'); expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entities).toHaveBeenCalledWith([ - { 'metadata.uid': 'zzz' }, - ]); + expect(entitiesCatalog.entities).toHaveBeenCalledWith({ + filters: [{ 'metadata.uid': 'zzz' }], + }); expect(response.status).toEqual(200); expect(response.body).toEqual(expect.objectContaining(entity)); }); @@ -120,9 +124,9 @@ describe('createRouter', () => { const response = await request(app).get('/entities/by-uid/zzz'); expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entities).toHaveBeenCalledWith([ - { 'metadata.uid': 'zzz' }, - ]); + expect(entitiesCatalog.entities).toHaveBeenCalledWith({ + filters: [{ 'metadata.uid': 'zzz' }], + }); expect(response.status).toEqual(404); expect(response.text).toMatch(/uid/); }); @@ -143,13 +147,15 @@ describe('createRouter', () => { const response = await request(app).get('/entities/by-name/k/ns/n'); expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entities).toHaveBeenCalledWith([ - { - kind: 'k', - 'metadata.namespace': 'ns', - 'metadata.name': 'n', - }, - ]); + expect(entitiesCatalog.entities).toHaveBeenCalledWith({ + filters: [ + { + kind: 'k', + 'metadata.namespace': 'ns', + 'metadata.name': 'n', + }, + ], + }); expect(response.status).toEqual(200); expect(response.body).toEqual(expect.objectContaining(entity)); }); @@ -160,13 +166,15 @@ describe('createRouter', () => { const response = await request(app).get('/entities/by-name/b/d/c'); expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entities).toHaveBeenCalledWith([ - { - kind: 'b', - 'metadata.namespace': 'd', - 'metadata.name': 'c', - }, - ]); + expect(entitiesCatalog.entities).toHaveBeenCalledWith({ + filters: [ + { + kind: 'b', + 'metadata.namespace': 'd', + 'metadata.name': 'c', + }, + ], + }); expect(response.status).toEqual(404); expect(response.text).toMatch(/name/); }); @@ -209,9 +217,9 @@ describe('createRouter', () => { { entity, relations: [] }, ]); expect(entitiesCatalog.entities).toHaveBeenCalledTimes(1); - expect(entitiesCatalog.entities).toHaveBeenCalledWith([ - { 'metadata.uid': 'u' }, - ]); + expect(entitiesCatalog.entities).toHaveBeenCalledWith({ + filters: [{ 'metadata.uid': 'u' }], + }); expect(response.status).toEqual(200); expect(response.body).toEqual(entity); }); @@ -285,7 +293,36 @@ describe('createRouter', () => { const response = await request(app).post('/locations').send(spec); expect(higherOrderOperation.addLocation).toHaveBeenCalledTimes(1); - expect(higherOrderOperation.addLocation).toHaveBeenCalledWith(spec); + expect(higherOrderOperation.addLocation).toHaveBeenCalledWith(spec, { + dryRun: false, + }); + expect(response.status).toEqual(201); + expect(response.body).toEqual( + expect.objectContaining({ + location: { id: 'a', ...spec }, + }), + ); + }); + + it('supports dry run', async () => { + const spec: LocationSpec = { + type: 'b', + target: 'c', + }; + + higherOrderOperation.addLocation.mockResolvedValue({ + location: { id: 'a', ...spec }, + entities: [], + }); + + const response = await request(app) + .post('/locations?dryRun=true') + .send(spec); + + expect(higherOrderOperation.addLocation).toHaveBeenCalledTimes(1); + expect(higherOrderOperation.addLocation).toHaveBeenCalledWith(spec, { + dryRun: true, + }); expect(response.status).toEqual(201); expect(response.body).toEqual( expect.objectContaining({ diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index 785e44f68c..e297bf16c6 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -20,6 +20,7 @@ import { locationSpecSchema } from '@backstage/catalog-model'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; +import yn from 'yn'; import { EntitiesCatalog, LocationsCatalog } from '../catalog'; import { HigherOrderOperation } from '../ingestion/types'; import { @@ -48,7 +49,7 @@ export async function createRouter( .get('/entities', async (req, res) => { const filters = translateQueryToEntityFilters(req.query); const fieldMapper = translateQueryToFieldMapper(req.query); - const entities = await entitiesCatalog.entities(filters); + const entities = await entitiesCatalog.entities({ filters }); res.status(200).send(entities.map(fieldMapper)); }) .post('/entities', async (req, res) => { @@ -56,18 +57,20 @@ export async function createRouter( const [result] = await entitiesCatalog.batchAddOrUpdateEntities([ { entity: body as Entity, relations: [] }, ]); - const [entity] = await entitiesCatalog.entities([ - { 'metadata.uid': result.entityId }, - ]); + const [entity] = await entitiesCatalog.entities({ + filters: [{ 'metadata.uid': result.entityId }], + }); res.status(200).send(entity); }) .get('/entities/by-uid/:uid', async (req, res) => { const { uid } = req.params; - const entities = await entitiesCatalog.entities([ - { - 'metadata.uid': uid, - }, - ]); + const entities = await entitiesCatalog.entities({ + filters: [ + { + 'metadata.uid': uid, + }, + ], + }); if (!entities.length) { res.status(404).send(`No entity with uid ${uid}`); } @@ -80,13 +83,15 @@ export async function createRouter( }) .get('/entities/by-name/:kind/:namespace/:name', async (req, res) => { const { kind, namespace, name } = req.params; - const entities = await entitiesCatalog.entities([ - { - kind: kind, - 'metadata.namespace': namespace, - 'metadata.name': name, - }, - ]); + const entities = await entitiesCatalog.entities({ + filters: [ + { + kind: kind, + 'metadata.namespace': namespace, + 'metadata.name': name, + }, + ], + }); if (!entities.length) { res .status(404) @@ -101,7 +106,8 @@ export async function createRouter( if (higherOrderOperation) { router.post('/locations', async (req, res) => { const input = await validateRequestBody(req, locationSpecSchema); - const output = await higherOrderOperation.addLocation(input); + const dryRun = yn(req.query.dryRun, { default: false }); + const output = await higherOrderOperation.addLocation(input, { dryRun }); res.status(201).send(output); }); } From 2ebcfac8d4487313c9d4bd90225395965b5945b8 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 21 Oct 2020 09:37:25 +0200 Subject: [PATCH 064/198] feat: add a validate button to the register-component page This allows the user to validate his location before adding it. --- .changeset/angry-crabs-relate.md | 2 +- .changeset/nervous-drinks-notice.md | 8 ++ .../catalog/DatabaseEntitiesCatalog.test.ts | 30 +++-- .../src/catalog/DatabaseEntitiesCatalog.ts | 126 +++++++++++------- plugins/catalog-backend/src/catalog/types.ts | 5 +- .../ingestion/HigherOrderOperations.test.ts | 99 ++++++++++++-- .../src/ingestion/HigherOrderOperations.ts | 15 +-- .../src/service/router.test.ts | 2 - plugins/catalog/src/api/CatalogClient.ts | 5 +- plugins/catalog/src/api/types.ts | 1 + .../RegisterComponentForm.test.tsx | 10 +- .../RegisterComponentForm.tsx | 48 +++++-- .../RegisterComponentPage.tsx | 12 +- .../RegisterComponentResultDialog.tsx | 115 +++++++++------- 14 files changed, 327 insertions(+), 151 deletions(-) create mode 100644 .changeset/nervous-drinks-notice.md diff --git a/.changeset/angry-crabs-relate.md b/.changeset/angry-crabs-relate.md index 66cdb29be5..678093c1ac 100644 --- a/.changeset/angry-crabs-relate.md +++ b/.changeset/angry-crabs-relate.md @@ -2,7 +2,7 @@ '@backstage/plugin-catalog-backend': minor --- -Add ability to dry run adding a new location ot the catalog API. +Add ability to dry run adding a new location to the catalog API. The location is now added in a transaction and afterwards rolled back. This allows users to dry run this operation to see if there entity has issues. diff --git a/.changeset/nervous-drinks-notice.md b/.changeset/nervous-drinks-notice.md new file mode 100644 index 0000000000..7c6af21f64 --- /dev/null +++ b/.changeset/nervous-drinks-notice.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-catalog': minor +'@backstage/plugin-register-component': minor +--- + +Add a validate button to the register-component page + +This allows the user to validate his location before adding it. diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts index 7e751bab92..90b11b179d 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.test.ts @@ -95,25 +95,33 @@ describe('DatabaseEntitiesCatalog', () => { namespace: 'd', }, }; - const existingTransaction: jest.Mocked = { rollback: jest.fn(), }; db.entities.mockResolvedValue([]); - db.addEntities.mockResolvedValue([{ entity }]); + db.addEntities.mockResolvedValue([ + { entity: { ...entity, metadata: { ...entity.metadata, uid: 'u' } } }, + ]); const catalog = new DatabaseEntitiesCatalog(db, getVoidLogger()); - const result = await catalog.addOrUpdateEntity(entity, { - tx: existingTransaction, - }); - - expect(db.addEntities).toHaveBeenCalledTimes(1); - expect(db.addEntities).toHaveBeenCalledWith( - existingTransaction, - expect.anything(), + const result = await catalog.batchAddOrUpdateEntities( + [{ entity, relations: [] }], + { tx: existingTransaction }, ); - expect(result).toBe(entity); + + expect(db.entities).toHaveBeenCalledTimes(1); + expect(db.entities).toHaveBeenCalledWith(expect.anything(), [ + { + kind: 'b', + 'metadata.namespace': 'd', + 'metadata.name': ['c'], + }, + ]); + expect(db.setRelations).toHaveBeenCalledTimes(1); + expect(db.setRelations).toHaveBeenCalledWith(expect.anything(), 'u', []); + expect(db.addEntities).toHaveBeenCalledTimes(1); + expect(result).toEqual([{ entityId: 'u' }]); }); it('updates when given uid', async () => { diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index 28a972e8e3..53fb50677b 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -70,46 +70,44 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { tx?: Transaction; }): Promise { const filters = options?.filters; - - // TODO: Support with and without transaction! - - const items = await this.database.transaction(tx => - this.database.entities(tx, filters), - ); + const items = options?.tx + ? await this.database.entities(options?.tx, filters) + : await this.database.transaction(tx => + this.database.entities(tx, filters), + ); return items.map(i => i.entity); } private async addOrUpdateEntity( entity: Entity, + tx: Transaction, locationId?: string, ): Promise { - return await this.database.transaction(async tx => { - // Find a matching (by uid, or by compound name, depending on the given - // entity) existing entity, to know whether to update or add - const existing = entity.metadata.uid - ? await this.database.entityByUid(tx, entity.metadata.uid) - : await this.database.entityByName(tx, getEntityName(entity)); + // Find a matching (by uid, or by compound name, depending on the given + // entity) existing entity, to know whether to update or add + const existing = entity.metadata.uid + ? await this.database.entityByUid(tx, entity.metadata.uid) + : await this.database.entityByName(tx, getEntityName(entity)); - // If it's an update, run the algorithm for annotation merging, updating - // etag/generation, etc. - let response: DbEntityResponse; - if (existing) { - const updated = generateUpdatedEntity(existing.entity, entity); - response = await this.database.updateEntity( - tx, - { locationId, entity: updated }, - existing.entity.metadata.etag, - existing.entity.metadata.generation, - ); - } else { - const added = await this.database.addEntities(tx, [ - { locationId, entity }, - ]); - response = added[0]; - } + // If it's an update, run the algorithm for annotation merging, updating + // etag/generation, etc. + let response: DbEntityResponse; + if (existing) { + const updated = generateUpdatedEntity(existing.entity, entity); + response = await this.database.updateEntity( + tx, + { locationId, entity: updated }, + existing.entity.metadata.etag, + existing.entity.metadata.generation, + ); + } else { + const added = await this.database.addEntities(tx, [ + { locationId, entity }, + ]); + response = added[0]; + } - return response.entity; - }); + return response.entity; } async removeEntityByUid(uid: string): Promise { @@ -143,14 +141,41 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { * Writes a number of entities efficiently to storage. * * @param entities Some entities - * @param locationId The location that they all belong to + * @param options.locationId The location that they all belong to + * @param options.tx A database transaction to execute the queries in */ async batchAddOrUpdateEntities( requests: EntityUpsertRequest[], - options?: { locationId?: string; tx?: Transaction }, + options?: { + locationId?: string; + tx?: Transaction; + }, ): Promise { - // TODO: How to work with the transaction here? + const locationId = options?.locationId; + if (options?.tx) { + return this.batchAddOrUpdateEntitiesInTransaction( + requests, + options.tx, + locationId, + ); + } + + return await this.database.transaction( + async tx => + await this.batchAddOrUpdateEntitiesInTransaction( + requests, + tx, + locationId, + ), + ); + } + + private async batchAddOrUpdateEntitiesInTransaction( + requests: EntityUpsertRequest[], + tx: Transaction, + locationId?: string, + ): Promise { // Group the entities by unique kind+namespace combinations const entitiesByKindAndNamespace = groupBy(requests, ({ entity }) => { const name = getEntityName(entity); @@ -180,29 +205,30 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { const context = { kind, namespace, - locationId: options?.locationId, + locationId, }; for (let attempt = 1; attempt <= BATCH_ATTEMPTS; ++attempt) { try { const { toAdd, toUpdate, toIgnore } = await this.analyzeBatch( batch, context, + tx, ); if (toAdd.length) { modifiedEntityIds.push( - ...(await this.batchAdd(toAdd, context)), + ...(await this.batchAdd(toAdd, context, tx)), ); } if (toUpdate.length) { modifiedEntityIds.push( - ...(await this.batchUpdate(toUpdate, context)), + ...(await this.batchUpdate(toUpdate, context, tx)), ); } // TODO(Rugvip): We currently always update relations, but we // likely want to figure out a way to avoid that for (const { entity, relations } of toIgnore) { const entityId = entity.metadata.uid!; - await this.setRelations(entityId, relations); + await this.setRelations(entityId, relations, tx); modifiedEntityIds.push({ entityId }); } @@ -231,10 +257,9 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { private async setRelations( originatingEntityId: string, relations: EntityRelationSpec[], + tx: Transaction, ): Promise { - return await this.database.transaction(tx => - this.database.setRelations(tx, originatingEntityId, relations), - ); + await this.database.setRelations(tx, originatingEntityId, relations); } // Given a batch of entities that were just read from a location, take them @@ -244,6 +269,7 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { private async analyzeBatch( requests: EntityUpsertRequest[], { kind, namespace }: BatchContext, + tx: Transaction, ): Promise<{ toAdd: EntityUpsertRequest[]; toUpdate: EntityUpsertRequest[]; @@ -260,6 +286,7 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { 'metadata.name': names, }, ], + tx, }); const oldEntitiesByName = new Map( @@ -299,15 +326,13 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { private async batchAdd( requests: EntityUpsertRequest[], { locationId }: BatchContext, + tx: Transaction, ): Promise { const markTimestamp = process.hrtime(); - const res = await this.database.transaction( - async tx => - await this.database.addEntities( - tx, - requests.map(({ entity }) => ({ locationId, entity })), - ), + const res = await this.database.addEntities( + tx, + requests.map(({ entity }) => ({ locationId, entity })), ); const entityIds = res.map(({ entity }) => ({ @@ -315,7 +340,7 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { })); for (const [index, { entityId }] of entityIds.entries()) { - await this.setRelations(entityId, requests[index].relations); + await this.setRelations(entityId, requests[index].relations, tx); } this.logger.debug( @@ -330,15 +355,16 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { private async batchUpdate( requests: EntityUpsertRequest[], { locationId }: BatchContext, + tx: Transaction, ): Promise { const markTimestamp = process.hrtime(); const responseIds: EntityUpsertResponse[] = []; // TODO(freben): Still not batched for (const entity of requests) { - const res = await this.addOrUpdateEntity(entity.entity, locationId); + const res = await this.addOrUpdateEntity(entity.entity, tx, locationId); const entityId = res.metadata.uid!; responseIds.push({ entityId }); - await this.setRelations(entityId, entity.relations); + await this.setRelations(entityId, entity.relations, tx); } this.logger.debug( diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index 1ac06618b5..6f2977619a 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -45,7 +45,10 @@ export type EntitiesCatalog = { */ batchAddOrUpdateEntities( entities: EntityUpsertRequest[], - options?: { locationId?: string; tx?: Transaction }, + options?: { + tx?: Transaction; + locationId?: string; + }, ): Promise; }; diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts index 174428799d..da722e56bc 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.test.ts @@ -63,6 +63,7 @@ describe('HigherOrderOperations', () => { entityByName: jest.fn(), entityByUid: jest.fn(), removeEntityByUid: jest.fn(), + setRelations: jest.fn(), addLocation: jest.fn(), removeLocation: jest.fn(), location: jest.fn(), @@ -81,6 +82,7 @@ describe('HigherOrderOperations', () => { beforeEach(() => { jest.resetAllMocks(); + database.transaction.mockImplementation(async f => f(transaction)); }); describe('addLocation', () => { @@ -89,7 +91,6 @@ describe('HigherOrderOperations', () => { type: 'a', target: 'b', }; - database.transaction.mockImplementation(x => x(transaction)); locationsCatalog.addLocation.mockImplementation(x => Promise.resolve(x)); locationsCatalog.locations.mockResolvedValue([]); locationReader.read.mockResolvedValue({ @@ -121,6 +122,62 @@ describe('HigherOrderOperations', () => { expect(transaction.rollback).not.toBeCalled(); }); + it('insert the location and its entities', async () => { + const spec = { + type: 'a', + target: 'b', + }; + const location: LocationSpec = { type: '', target: '' }; + const entity: Entity = { + apiVersion: 'a', + kind: 'b', + metadata: { name: 'n' }, + }; + locationsCatalog.addLocation.mockImplementation(x => Promise.resolve(x)); + locationsCatalog.locations.mockResolvedValue([]); + locationsCatalog.locations.mockResolvedValue([]); + entitiesCatalog.batchAddOrUpdateEntities.mockResolvedValue([ + { + entityId: 'id', + }, + ]); + entitiesCatalog.entities.mockResolvedValue([entity]); + locationReader.read.mockResolvedValue({ + entities: [ + { + location, + entity, + relations: [], + }, + ], + errors: [], + }); + + const result = await higherOrderOperation.addLocation(spec); + + expect(result.location).toEqual( + expect.objectContaining({ + id: expect.anything(), + ...spec, + }), + ); + expect(result.entities).toEqual([entity]); + expect(locationsCatalog.locations).toBeCalledTimes(1); + expect(locationsCatalog.addLocation).toBeCalledTimes(1); + expect(locationsCatalog.addLocation).toBeCalledWith( + expect.objectContaining({ + id: expect.anything(), + ...spec, + }), + { tx: transaction }, + ); + expect(locationReader.read).toBeCalledTimes(1); + expect(locationReader.read).toBeCalledWith({ type: 'a', target: 'b' }); + expect(entitiesCatalog.entities).toBeCalledTimes(1); + expect(entitiesCatalog.batchAddOrUpdateEntities).toBeCalledTimes(1); + expect(transaction.rollback).not.toBeCalled(); + }); + it('reuses the location if a match already existed', async () => { const spec = { type: 'a', @@ -131,7 +188,6 @@ describe('HigherOrderOperations', () => { ...spec, }; - database.transaction.mockImplementation(x => x(transaction)); locationsCatalog.locations.mockResolvedValue([ { currentStatus: { timestamp: '', status: '', message: '' }, @@ -167,7 +223,6 @@ describe('HigherOrderOperations', () => { metadata: { name: 'n' }, }; - database.transaction.mockImplementation(x => x(transaction)); locationsCatalog.locations.mockResolvedValue([]); locationReader.read.mockResolvedValue({ entities: [{ entity, location, relations: [] }], @@ -188,10 +243,31 @@ describe('HigherOrderOperations', () => { type: 'a', target: 'b', }; - database.transaction.mockImplementation(x => x(transaction)); + const location: LocationSpec = { type: '', target: '' }; + const entity: Entity = { + apiVersion: 'a', + kind: 'b', + metadata: { name: 'n' }, + }; locationsCatalog.addLocation.mockImplementation(x => Promise.resolve(x)); locationsCatalog.locations.mockResolvedValue([]); - locationReader.read.mockResolvedValue({ entities: [], errors: [] }); + locationsCatalog.locations.mockResolvedValue([]); + entitiesCatalog.batchAddOrUpdateEntities.mockResolvedValue([ + { + entityId: 'id', + }, + ]); + entitiesCatalog.entities.mockResolvedValue([entity]); + locationReader.read.mockResolvedValue({ + entities: [ + { + location, + entity, + relations: [], + }, + ], + errors: [], + }); const result = await higherOrderOperation.addLocation(spec, { dryRun: true, @@ -203,11 +279,8 @@ describe('HigherOrderOperations', () => { ...spec, }), ); - expect(result.entities).toEqual([]); + expect(result.entities).toEqual([entity]); expect(locationsCatalog.locations).toBeCalledTimes(1); - expect(locationReader.read).toBeCalledTimes(1); - expect(locationReader.read).toBeCalledWith({ type: 'a', target: 'b' }); - expect(entitiesCatalog.addOrUpdateEntity).not.toBeCalled(); expect(locationsCatalog.addLocation).toBeCalledTimes(1); expect(locationsCatalog.addLocation).toBeCalledWith( expect.objectContaining({ @@ -216,6 +289,10 @@ describe('HigherOrderOperations', () => { }), { tx: transaction }, ); + expect(locationReader.read).toBeCalledTimes(1); + expect(locationReader.read).toBeCalledWith({ type: 'a', target: 'b' }); + expect(entitiesCatalog.entities).toBeCalledTimes(1); + expect(entitiesCatalog.batchAddOrUpdateEntities).toBeCalledTimes(1); expect(transaction.rollback).toBeCalled(); }); }); @@ -281,7 +358,9 @@ describe('HigherOrderOperations', () => { relations: [], }), ], - '123', + { + locationId: '123', + }, ); }); diff --git a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts index 2ca79c60b4..ac21fc6155 100644 --- a/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts +++ b/plugins/catalog-backend/src/ingestion/HigherOrderOperations.ts @@ -90,21 +90,18 @@ export class HigherOrderOperations implements HigherOrderOperation { await this.locationsCatalog.addLocation(location, { tx }); } if (readerOutput.entities.length === 0) { - return { location, entities: [] }; + return []; } const writtenEntities = await this.entitiesCatalog.batchAddOrUpdateEntities( readerOutput.entities, - { - locationId: location.id, - tx, - }, + { locationId: location.id, tx }, ); - const outputEntities = await this.entitiesCatalog.entities( - [{ 'metadata.uid': writtenEntities.map(e => e.entityId) }], - { tx }, - ); + const outputEntities = await this.entitiesCatalog.entities({ + filters: [{ 'metadata.uid': writtenEntities.map(e => e.entityId) }], + tx, + }); if (dryRun) { // If this is only a dry run, cancel the database transaction even if it was successful. diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index 3a43ac96ad..95dc8e82d1 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -23,8 +23,6 @@ import { LocationResponse } from '../catalog/types'; import { HigherOrderOperation } from '../ingestion/types'; import { createRouter } from './router'; -// TODO: ... - describe('createRouter', () => { let entitiesCatalog: jest.Mocked; let locationsCatalog: jest.Mocked; diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index 40622da60a..85fd44cf01 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -95,9 +95,12 @@ export class CatalogClient implements CatalogApi { async addLocation({ type = 'url', target, + dryRun, }: AddLocationRequest): Promise { const response = await fetch( - `${await this.discoveryApi.getBaseUrl('catalog')}/locations`, + `${await this.discoveryApi.getBaseUrl('catalog')}/locations${ + dryRun ? '?dryRun=true' : '' + }`, { headers: { 'Content-Type': 'application/json', diff --git a/plugins/catalog/src/api/types.ts b/plugins/catalog/src/api/types.ts index 324a8d43fd..06030e6ac5 100644 --- a/plugins/catalog/src/api/types.ts +++ b/plugins/catalog/src/api/types.ts @@ -43,6 +43,7 @@ export interface CatalogApi { export type AddLocationRequest = { type?: string; target: string; + dryRun?: boolean; }; export type AddLocationResponse = { diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx index 875b1f8660..fae5d649b9 100644 --- a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx +++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.test.tsx @@ -20,17 +20,18 @@ import React from 'react'; import { RegisterComponentForm } from './RegisterComponentForm'; describe('RegisterComponentForm', () => { - it('should initially render a disabled button', async () => { + it('should initially render disabled buttons', async () => { render(); expect( await screen.findByText(/Enter the full path to the catalog-info.yaml/), ).toBeInTheDocument(); - expect(screen.getByText('Submit').closest('button')).toBeDisabled(); + expect(screen.getByText('Validate').closest('button')).toBeDisabled(); + expect(screen.getByText('Register').closest('button')).toBeDisabled(); }); - it('should enable a submit button when the target url is set', async () => { + it('should enable the submit buttons when the target url is set', async () => { render(); await act(async () => { @@ -40,7 +41,8 @@ describe('RegisterComponentForm', () => { ); }); - expect(screen.getByText('Submit').closest('button')).not.toBeDisabled(); + expect(screen.getByText('Validate').closest('button')).not.toBeDisabled(); + expect(screen.getByText('Register').closest('button')).not.toBeDisabled(); }); it('should show spinner while submitting', async () => { diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx index 56dd50ea67..de183869f7 100644 --- a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx +++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx @@ -33,8 +33,11 @@ const useStyles = makeStyles(theme => ({ display: 'flex', flexFlow: 'column nowrap', }, - submit: { - marginTop: theme.spacing(1), + buttonSpacing: { + marginLeft: theme.spacing(1), + }, + buttons: { + marginTop: theme.spacing(2), }, select: { minWidth: 120, @@ -54,12 +57,21 @@ export const RegisterComponentForm = ({ onSubmit, submitting }: Props) => { const hasErrors = !!errors.entityLocation; const dirty = formState?.isDirty; + const onSubmitValidate = handleSubmit(data => { + data.mode = 'validate'; + onSubmit(data); + }); + + const onSubmitRegister = handleSubmit(data => { + data.mode = 'register'; + onSubmit(data); + }); + return submitting ? ( ) : (
@@ -87,15 +99,27 @@ export const RegisterComponentForm = ({ onSubmit, submitting }: Props) => { )} - +
+ + +
); }; diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx index c7f94e2060..d0b56598b2 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.tsx @@ -75,27 +75,30 @@ export const RegisterComponentPage = ({ location: Location; } | null; error: null | Error; + dryRun: boolean; }>({ data: null, error: null, + dryRun: false, }); const handleSubmit = async (formData: Record) => { setFormState(FormStates.Submitting); - const { entityLocation: target } = formData; + const { entityLocation: target, mode } = formData; + const dryRun = mode === 'validate'; try { - const data = await catalogApi.addLocation({ target }); + const data = await catalogApi.addLocation({ target, dryRun }); if (!isMounted()) return; - setResult({ error: null, data }); + setResult({ error: null, data, dryRun }); setFormState(FormStates.Success); } catch (e) { errorApi.post(e); if (!isMounted()) return; - setResult({ error: e, data: null }); + setResult({ error: e, data: null, dryRun }); setFormState(FormStates.Idle); } }; @@ -124,6 +127,7 @@ export const RegisterComponentPage = ({ {formState === FormStates.Success && ( setFormState(FormStates.Idle)} classes={{ paper: classes.dialogPaper }} catalogRouteRef={catalogRouteRef} diff --git a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx index 9631865ffb..2b57050661 100644 --- a/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx +++ b/plugins/register-component/src/components/RegisterComponentResultDialog/RegisterComponentResultDialog.tsx @@ -29,7 +29,7 @@ import { List, ListItem, } from '@material-ui/core'; -import React from 'react'; +import React, { useState } from 'react'; import { generatePath, resolvePath } from 'react-router'; import { Link as RouterLink } from 'react-router-dom'; @@ -37,6 +37,7 @@ type Props = { onClose: () => void; classes?: Record; entities: Entity[]; + dryRun?: boolean; catalogRouteRef: RouteRef; }; @@ -64,49 +65,71 @@ export const RegisterComponentResultDialog = ({ onClose, classes, entities, + dryRun, catalogRouteRef, -}: Props) => ( - - Registration Result - - - The following entities have been successfully created: - - - {entities.map((entity: any, index: number) => { - const entityPath = getEntityCatalogPath({ entity, catalogRouteRef }); - return ( - - - - {entityPath} - - ), - }} - /> - - {index < entities.length - 1 && } - - ); - })} - - - - - - -); +}: Props) => { + const [open, setOpen] = useState(true); + const handleClose = () => { + setOpen(false); + }; + + return ( + + + {dryRun ? 'Validation Result' : 'Registration Result'} + + + + {dryRun + ? 'The following entities would be created:' + : 'The following entities have been successfully created:'} + + + {entities.map((entity: any, index: number) => { + const entityPath = getEntityCatalogPath({ + entity, + catalogRouteRef, + }); + return ( + + + + {entityPath} + + ), + }} + /> + + {index < entities.length - 1 && } + + ); + })} + + + + {dryRun && ( + + )} + {!dryRun && ( + + )} + + + ); +}; From a6da3d1c06b33dcdf8b3588868646caaf9bbaeda Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 29 Oct 2020 11:10:11 +0100 Subject: [PATCH 065/198] fix: upgrade knex to 0.21.6 This reduces a overly noisy warning during tests: https://github.com/knex/knex/pull/3936 --- packages/backend-common/package.json | 2 +- packages/backend/package.json | 2 +- .../packages/backend/package.json.hbs | 2 +- plugins/auth-backend/package.json | 2 +- plugins/catalog-backend/package.json | 2 +- plugins/techdocs-backend/package.json | 2 +- yarn.lock | 22 +++++++++---------- 7 files changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index dea2f16bae..076e644afd 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -43,7 +43,7 @@ "express-promise-router": "^3.0.3", "git-url-parse": "^11.4.0", "helmet": "^4.0.0", - "knex": "^0.21.1", + "knex": "^0.21.6", "lodash": "^4.17.15", "logform": "^2.1.1", "minimist": "^1.2.5", diff --git a/packages/backend/package.json b/packages/backend/package.json index 9f8e66bc61..0d066d1139 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -38,7 +38,7 @@ "example-app": "^0.1.1-alpha.26", "express": "^4.17.1", "express-promise-router": "^3.0.3", - "knex": "^0.21.1", + "knex": "^0.21.6", "pg": "^8.3.0", "pg-connection-string": "^2.3.0", "sqlite3": "^5.0.0", diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index dde04868b9..bf887dc014 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -31,7 +31,7 @@ "dockerode": "^3.2.0", "express": "^4.17.1", "express-promise-router": "^3.0.3", - "knex": "^0.21.1", + "knex": "^0.21.6", {{#if dbTypePG}} "pg": "^8.3.0", {{/if}} diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 6489c8547d..caa20e361b 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -35,7 +35,7 @@ "helmet": "^4.0.0", "jose": "^1.27.1", "jwt-decode": "2.2.0", - "knex": "^0.21.1", + "knex": "^0.21.6", "moment": "^2.26.0", "morgan": "^1.10.0", "passport": "^0.4.1", diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index cd8e363901..d15779bf4e 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -32,7 +32,7 @@ "express-promise-router": "^3.0.3", "fs-extra": "^9.0.0", "git-url-parse": "^11.4.0", - "knex": "^0.21.1", + "knex": "^0.21.6", "ldapjs": "^2.2.0", "lodash": "^4.17.15", "morgan": "^1.10.0", diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 5bdc7ba282..50eb0253cc 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -32,7 +32,7 @@ "express-promise-router": "^3.0.3", "fs-extra": "^9.0.1", "git-url-parse": "^11.4.0", - "knex": "^0.21.1", + "knex": "^0.21.6", "nodegit": "^0.27.0", "winston": "^3.2.1" }, diff --git a/yarn.lock b/yarn.lock index b2b87be5e0..70b224a600 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13469,7 +13469,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -15194,23 +15194,21 @@ kleur@^3.0.3: resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -knex@^0.21.1: - version "0.21.5" - resolved "https://registry.npmjs.org/knex/-/knex-0.21.5.tgz#c4be1958488f348aed3510aa4b7115639ee1bd01" - integrity sha512-cQj7F2D/fu03eTr6ZzYCYKdB9w7fPYlvTiU/f2OeXay52Pq5PwD+NAkcf40WDnppt/4/4KukROwlMOaE7WArcA== +knex@^0.21.6: + version "0.21.8" + resolved "https://registry.npmjs.org/knex/-/knex-0.21.8.tgz#e5c07af61ee6aa006d3468e10e3a69351deb0c26" + integrity sha512-ziUu4vAlIGA8j2l0S4xcD1d3XdpJA4HYGhwHEhgAgefGCmB1OLSjUGCs/ebkJal42fSvAkyZaB0tcOtTXKgS5g== dependencies: colorette "1.2.1" commander "^5.1.0" debug "4.1.1" esm "^3.2.25" getopts "2.2.5" - inherits "~2.0.4" interpret "^2.2.0" liftoff "3.1.0" lodash "^4.17.20" - mkdirp "^1.0.4" pg-connection-string "2.3.0" - tarn "^3.0.0" + tarn "^3.0.1" tildify "2.0.0" uuid "^7.0.3" v8flags "^3.2.0" @@ -22264,10 +22262,10 @@ tar@^6.0.1, tar@^6.0.2: mkdirp "^1.0.3" yallist "^4.0.0" -tarn@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/tarn/-/tarn-3.0.0.tgz#a4082405216c0cce182b8b4cb2639c52c1e870d4" - integrity sha512-PKUnlDFODZueoA8owLehl8vLcgtA8u4dRuVbZc92tspDYZixjJL6TqYOmryf/PfP/EBX+2rgNcrj96NO+RPkdQ== +tarn@^3.0.1: + version "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" From 4d66ae31c266b9b1de6b1523306a57c14a58f046 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 29 Oct 2020 14:08:16 +0100 Subject: [PATCH 066/198] feat: upgrade ts-jest to 26.3.0 This adds support for typescript 4.0 and resolves a warning during testing. --- packages/cli/package.json | 2 +- yarn.lock | 110 ++++++++++++++++++++++++++++++-------- 2 files changed, 90 insertions(+), 22 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 668ec5e000..00bf463018 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -96,7 +96,7 @@ "sucrase": "^3.16.0", "tar": "^6.0.1", "terser-webpack-plugin": "^1.4.3", - "ts-jest": "^26.0.0", + "ts-jest": "^26.3.0", "ts-loader": "^7.0.4", "typescript": "^4.0.3", "url-loader": "^4.1.0", diff --git a/yarn.lock b/yarn.lock index 70b224a600..52f797f241 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2315,6 +2315,11 @@ slash "^3.0.0" strip-ansi "^6.0.0" +"@jest/create-cache-key-function@^26.5.0": + version "26.5.0" + 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" @@ -2461,6 +2466,17 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@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== + 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" @@ -5242,6 +5258,14 @@ jest-diff "^25.2.1" pretty-format "^25.2.1" +"@types/jest@26.x": + version "26.0.15" + resolved "https://registry.npmjs.org/@types/jest/-/jest-26.0.15.tgz#12e02c0372ad0548e07b9f4e19132b834cb1effe" + integrity sha512-s2VMReFXRg9XXxV+CW9e5Nz8fH2K1aEhwgjUqPPbQd7g95T0laAcvLv032EhFHIa5GHsZ8W7iJEQVaJq6k3Gog== + dependencies: + jest-diff "^26.0.0" + pretty-format "^26.0.0" + "@types/jquery@^3.3.34": version "3.5.1" resolved "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.1.tgz#cebb057acf5071c40e439f30e840c57a30d406c3" @@ -14350,6 +14374,16 @@ jest-diff@^25.2.1: jest-get-type "^25.2.6" pretty-format "^25.5.0" +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== + dependencies: + chalk "^4.0.0" + diff-sequences "^26.5.0" + jest-get-type "^26.3.0" + pretty-format "^26.6.1" + jest-diff@^26.5.2: version "26.5.2" resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-26.5.2.tgz#8e26cb32dc598e8b8a1b9deff55316f8313c8053" @@ -14627,6 +14661,18 @@ jest-snapshot@^26.5.3: pretty-format "^26.5.2" semver "^7.3.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== + dependencies: + "@jest/types" "^26.6.1" + "@types/node" "*" + chalk "^4.0.0" + graceful-fs "^4.2.4" + is-ci "^2.0.0" + micromatch "^4.0.2" + jest-util@^26.5.2: version "26.5.2" resolved "https://registry.npmjs.org/jest-util/-/jest-util-26.5.2.tgz#8403f75677902cc52a1b2140f568e91f8ed4f4d7" @@ -16198,14 +16244,6 @@ microevent.ts@~0.1.1: resolved "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0" integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g== -micromatch@4.x, micromatch@^4.0.0, micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== - dependencies: - braces "^3.0.1" - picomatch "^2.0.5" - micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -16225,6 +16263,14 @@ micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" +micromatch@^4.0.0, micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -18811,6 +18857,16 @@ pretty-format@^25.2.1, pretty-format@^25.5.0: ansi-styles "^4.0.0" react-is "^16.12.0" +pretty-format@^26.0.0, 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== + dependencies: + "@jest/types" "^26.6.1" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^17.0.1" + pretty-format@^26.4.2: version "26.4.2" resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-26.4.2.tgz#d081d032b398e801e2012af2df1214ef75a81237" @@ -19496,6 +19552,11 @@ react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-i resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^17.0.1: + version "17.0.1" + resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" + integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== + react-lazylog@^4.5.2, react-lazylog@^4.5.3: version "4.5.3" resolved "https://registry.npmjs.org/react-lazylog/-/react-lazylog-4.5.3.tgz#289e24995b5599e75943556ac63f5e2c04d0001e" @@ -22739,21 +22800,23 @@ ts-invariant@^0.4.0: dependencies: tslib "^1.9.3" -ts-jest@^26.0.0: - version "26.1.1" - resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-26.1.1.tgz#b98569b8a4d4025d966b3d40c81986dd1c510f8d" - integrity sha512-Lk/357quLg5jJFyBQLnSbhycnB3FPe+e9i7ahxokyXxAYoB0q1pPmqxxRPYr4smJic1Rjcf7MXDBhZWgxlli0A== +ts-jest@^26.3.0: + version "26.4.3" + resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-26.4.3.tgz#d153a616033e7ec8544b97ddbe2638cbe38d53db" + integrity sha512-pFDkOKFGY+nL9v5pkhm+BIFpoAuno96ff7GMnIYr/3L6slFOS365SI0fGEVYx2RKGji5M2elxhWjDMPVcOCdSw== dependencies: + "@jest/create-cache-key-function" "^26.5.0" + "@types/jest" "26.x" bs-logger "0.x" buffer-from "1.x" fast-json-stable-stringify "2.x" + jest-util "^26.1.0" json5 "2.x" lodash.memoize "4.x" make-error "1.x" - micromatch "4.x" mkdirp "1.x" semver "7.x" - yargs-parser "18.x" + yargs-parser "20.x" ts-loader@^7.0.4: version "7.0.4" @@ -24214,13 +24277,10 @@ yaml@*, yaml@^1.10.0, yaml@^1.7.2, yaml@^1.9.2: resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== -yargs-parser@18.x, yargs-parser@^18.1.2: - version "18.1.3" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" +yargs-parser@20.x: + version "20.2.3" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.3.tgz#92419ba867b858c868acf8bae9bf74af0dd0ce26" + integrity sha512-emOFRT9WVHw03QSvN5qor9QQT9+sw5vwxfYweivSMHTcAXPefwVae2FjO7JJjj8hCE4CzPOPeFM83VwT29HCww== yargs-parser@^10.0.0: version "10.1.0" @@ -24245,6 +24305,14 @@ yargs-parser@^15.0.1: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs-parser@^20.0.0: version "20.2.1" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.1.tgz#28f3773c546cdd8a69ddae68116b48a5da328e77" From 3f5ef4ac1617c152daf3312d2280f34bf8f738ac Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Thu, 29 Oct 2020 15:52:18 -0400 Subject: [PATCH 067/198] formatting --- .../src/api/ExampleCostInsightsClient.ts | 5 +---- .../components/CostGrowth/CostGrowth.test.tsx | 8 ++++---- .../ProjectGrowthAlertCard.tsx | 5 +++-- plugins/cost-insights/src/utils/alerts.tsx | 16 ++++++++++++++++ plugins/cost-insights/src/utils/formatters.ts | 5 +---- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/plugins/cost-insights/src/api/ExampleCostInsightsClient.ts b/plugins/cost-insights/src/api/ExampleCostInsightsClient.ts index 70695beb25..8cc66e626f 100644 --- a/plugins/cost-insights/src/api/ExampleCostInsightsClient.ts +++ b/plugins/cost-insights/src/api/ExampleCostInsightsClient.ts @@ -33,10 +33,7 @@ import { Trendline, UnlabeledDataflowData, } from '../types'; -import { - ProjectGrowthAlert, - UnlabeledDataflowAlert -} from '../utils/alerts'; +import { ProjectGrowthAlert, UnlabeledDataflowAlert } from '../utils/alerts'; import { DEFAULT_DATE_FORMAT, exclusiveEndDateOf, diff --git a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx index 046394ca7a..9c76d6d998 100644 --- a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx +++ b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx @@ -37,10 +37,10 @@ const MockContext = ({ currency: Currency; engineerCost: number; }>) => ( - - {children} - - ); + + {children} + +); describe.each` engineerCost | ratio | amount | expected diff --git a/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.tsx b/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.tsx index 18f7fa151d..4311bcc1bf 100644 --- a/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.tsx +++ b/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.tsx @@ -31,8 +31,9 @@ export const ProjectGrowthAlertCard = ({ alert }: ProjectGrowthAlertProps) => { const [costStart, costEnd] = alert.aggregation; const subheader = ` - ${alert.products.length} ${pluralOf(alert.products.length, 'product')}${alert.products.length > 1 ? ', sorted by cost' : '' - }`; + ${alert.products.length} ${pluralOf(alert.products.length, 'product')}${ + alert.products.length > 1 ? ', sorted by cost' : '' + }`; const previousName = moment(alert.periodStart, 'YYYY-[Q]Q').format( '[Q]Q YYYY', ); diff --git a/plugins/cost-insights/src/utils/alerts.tsx b/plugins/cost-insights/src/utils/alerts.tsx index fcb41f1e3b..fbb148f38a 100644 --- a/plugins/cost-insights/src/utils/alerts.tsx +++ b/plugins/cost-insights/src/utils/alerts.tsx @@ -1,3 +1,19 @@ +/* + * 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, UnlabeledDataflowData, ProjectGrowthData } from '../types'; import { UnlabeledDataflowAlertCard } from '../components/UnlabeledDataflowAlertCard'; diff --git a/plugins/cost-insights/src/utils/formatters.ts b/plugins/cost-insights/src/utils/formatters.ts index fc563bf836..bf8e26ef5f 100644 --- a/plugins/cost-insights/src/utils/formatters.ts +++ b/plugins/cost-insights/src/utils/formatters.ts @@ -16,10 +16,7 @@ import moment from 'moment'; import { Duration } from '../types'; -import { - inclusiveEndDateOf, - inclusiveStartDateOf, -} from '../utils/duration'; +import { inclusiveEndDateOf, inclusiveStartDateOf } from '../utils/duration'; import { pluralOf } from '../utils/grammar'; export type Period = { From 8fc93d46967e6f15c48b9c402058a09d619583eb Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Thu, 29 Oct 2020 16:57:16 -0400 Subject: [PATCH 068/198] Gitlab Enterprise documentation Most importantly documents the Gitlab callback URL but also the various scopes that seem appropriate. --- plugins/auth-backend/README.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/plugins/auth-backend/README.md b/plugins/auth-backend/README.md index 355fcc1536..4f5a9ef6c7 100644 --- a/plugins/auth-backend/README.md +++ b/plugins/auth-backend/README.md @@ -52,9 +52,27 @@ export AUTH_GITHUB_ENTERPRISE_INSTANCE_URL=https://x ### Gitlab +#### Creating a Gitlab Enterprise OAuth application + +Follow this link, substituting `gitlab.example.com` for your Gitlab enterprise domain, https://gitlab.example.com/profile/applications + +1. Set Application Name to `backstage-dev` or something along those lines. +1. The Authorization Callback URL should match the redirect URI set in Backstage. + 1. Set this to `http://localhost:7000/api/auth/gitlab/handler/frame` for local development. + 1. Set this to `http://{APP_FQDN}:{APP_BACKEND_PORT}/api/auth/gitlab/handler/frame` for non-local deployments. + 1. Select the checked scopes from this list: + - [ ] `api` Grants complete read/write access to the API, including all groups and projects. + - [x] `read_user` Grants read-only access to the authenticated user's profile through the /user API endpoint, which includes username, public email, and full name. Also grants access to read-only API endpoints under /users. + - [x] `read_repository` Grants read-only access to repositories on private projects using Git-over-HTTP (not using the API). + - [x] `write_repository` Grants read-write access to repositories on private projects using Git-over-HTTP (not using the API). + - [ ] `sudo` Grants permission to perform API actions as any user in the system, when authenticated as an admin user. + - [x] `openid` Grants permission to authenticate with GitLab using OpenID Connect. Also gives read-only access to the user's profile and group memberships. + - [x] `profile` Grants read-only access to the user's profile data using OpenID Connect. + - [x] `email` Grants read-only access to the user's primary email address using OpenID Connect. + ```bash -export GITLAB_BASE_URL=x # default is https://gitlab.com -export AUTH_GITLAB_CLIENT_ID=x +export GITLAB_BASE_URL=https://gitlab.example.com # for GitLab Enterprise. default is https://gitlab.com +export AUTH_GITLAB_CLIENT_ID=x # Gitlab calls this the Application ID export AUTH_GITLAB_CLIENT_SECRET=x ``` From a40536a24ba9deba68c784a63cf094790fd68ff7 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Thu, 29 Oct 2020 17:31:05 -0400 Subject: [PATCH 069/198] move example client out of src to break import cycles --- plugins/cost-insights/dev/index.tsx | 4 ++-- plugins/cost-insights/src/api/index.ts | 1 - .../src/{api/ExampleCostInsightsClient.ts => client.ts} | 8 ++++---- plugins/cost-insights/src/index.ts | 1 + 4 files changed, 7 insertions(+), 7 deletions(-) rename plugins/cost-insights/src/{api/ExampleCostInsightsClient.ts => client.ts} (97%) diff --git a/plugins/cost-insights/dev/index.tsx b/plugins/cost-insights/dev/index.tsx index 836aea0edb..fe208d5bb8 100644 --- a/plugins/cost-insights/dev/index.tsx +++ b/plugins/cost-insights/dev/index.tsx @@ -15,8 +15,8 @@ */ import { createDevApp } from '@backstage/dev-utils'; import { createPlugin, createApiFactory } from '@backstage/core'; -import { ExampleCostInsightsClient } from '../src/api'; -import { costInsightsApiRef } from '../src'; +import { costInsightsApiRef } from '../src/api'; +import { ExampleCostInsightsClient } from '../src/client'; import { pluginConfig } from '../src/plugin'; const devPlugin = createPlugin({ diff --git a/plugins/cost-insights/src/api/index.ts b/plugins/cost-insights/src/api/index.ts index a367df9487..d231570e9b 100644 --- a/plugins/cost-insights/src/api/index.ts +++ b/plugins/cost-insights/src/api/index.ts @@ -15,4 +15,3 @@ */ export * from './CostInsightsApi'; -export * from './ExampleCostInsightsClient'; diff --git a/plugins/cost-insights/src/api/ExampleCostInsightsClient.ts b/plugins/cost-insights/src/client.ts similarity index 97% rename from plugins/cost-insights/src/api/ExampleCostInsightsClient.ts rename to plugins/cost-insights/src/client.ts index 8cc66e626f..b73e824031 100644 --- a/plugins/cost-insights/src/api/ExampleCostInsightsClient.ts +++ b/plugins/cost-insights/src/client.ts @@ -17,7 +17,7 @@ import dayjs from 'dayjs'; import regression, { DataPoint } from 'regression'; -import { CostInsightsApi } from './CostInsightsApi'; +import { CostInsightsApi } from './api'; import { Alert, ChangeStatistic, @@ -32,13 +32,13 @@ import { ProjectGrowthData, Trendline, UnlabeledDataflowData, -} from '../types'; -import { ProjectGrowthAlert, UnlabeledDataflowAlert } from '../utils/alerts'; +} from './types'; +import { ProjectGrowthAlert, UnlabeledDataflowAlert } from './utils/alerts'; import { DEFAULT_DATE_FORMAT, exclusiveEndDateOf, inclusiveStartDateOf, -} from '../utils/duration'; +} from './utils/duration'; type IntervalFields = { duration: Duration; diff --git a/plugins/cost-insights/src/index.ts b/plugins/cost-insights/src/index.ts index 48b2425946..795d4e14a6 100644 --- a/plugins/cost-insights/src/index.ts +++ b/plugins/cost-insights/src/index.ts @@ -15,6 +15,7 @@ */ export { plugin } from './plugin'; +export * from './ExampleCostInsightsClient'; export * from './api'; export * from './components'; export { useCurrency } from './hooks'; From f454f0eecca9dd35f60b4f47f9ccc1b30a5020f1 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Thu, 29 Oct 2020 17:34:01 -0400 Subject: [PATCH 070/198] make thresholds and growth enums public --- .../src/components/CostGrowth/CostGrowth.tsx | 4 ++-- plugins/cost-insights/src/types/ChangeStatistic.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx index 3f5e0f55eb..da2f3e3c1b 100644 --- a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx +++ b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx @@ -16,9 +16,9 @@ import React from 'react'; import classnames from 'classnames'; -import { ChangeStatistic, CurrencyType, Duration } from '../../types'; +import { ChangeStatistic, CurrencyType, Duration, EngineerThreshold, GrowthType } from '../../types'; import { rateOf } from '../../utils/currency'; -import { growthOf, GrowthType, EngineerThreshold } from '../../utils/change'; +import { growthOf } from '../../utils/change'; import { useCostGrowthStyles as useStyles } from '../../utils/styles'; import { formatPercent, formatCurrency } from '../../utils/formatters'; import { indefiniteArticleOf } from '../../utils/grammar'; diff --git a/plugins/cost-insights/src/types/ChangeStatistic.ts b/plugins/cost-insights/src/types/ChangeStatistic.ts index e60d47a0e6..a47640411a 100644 --- a/plugins/cost-insights/src/types/ChangeStatistic.ts +++ b/plugins/cost-insights/src/types/ChangeStatistic.ts @@ -20,3 +20,16 @@ export interface ChangeStatistic { // The actual USD change between time periods (can be negative if costs decreased) amount: number; } + +export const EngineerThreshold = 0.5; + +export enum ChangeThreshold { + upper = 0.05, + lower = -0.05, +} + +export enum GrowthType { + Negligible, + Savings, + Excess, +} From 24b8f78d5fdd8d71096f6c0427942fee052fec1b Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Thu, 29 Oct 2020 17:36:52 -0400 Subject: [PATCH 071/198] make client public --- plugins/cost-insights/src/client.ts | 8 ++++---- plugins/cost-insights/src/index.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/cost-insights/src/client.ts b/plugins/cost-insights/src/client.ts index b73e824031..979a7a44ef 100644 --- a/plugins/cost-insights/src/client.ts +++ b/plugins/cost-insights/src/client.ts @@ -17,7 +17,7 @@ import dayjs from 'dayjs'; import regression, { DataPoint } from 'regression'; -import { CostInsightsApi } from './api'; +import { CostInsightsApi } from '../src/api'; import { Alert, ChangeStatistic, @@ -32,13 +32,13 @@ import { ProjectGrowthData, Trendline, UnlabeledDataflowData, -} from './types'; -import { ProjectGrowthAlert, UnlabeledDataflowAlert } from './utils/alerts'; +} from '../src/types'; +import { ProjectGrowthAlert, UnlabeledDataflowAlert } from '../src/utils/alerts'; import { DEFAULT_DATE_FORMAT, exclusiveEndDateOf, inclusiveStartDateOf, -} from './utils/duration'; +} from '../src/utils/duration'; type IntervalFields = { duration: Duration; diff --git a/plugins/cost-insights/src/index.ts b/plugins/cost-insights/src/index.ts index 795d4e14a6..1e6172d568 100644 --- a/plugins/cost-insights/src/index.ts +++ b/plugins/cost-insights/src/index.ts @@ -15,7 +15,7 @@ */ export { plugin } from './plugin'; -export * from './ExampleCostInsightsClient'; +export * from './client'; export * from './api'; export * from './components'; export { useCurrency } from './hooks'; From c894503435f36712d10da018d582f1ec1ae0c2d3 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Thu, 29 Oct 2020 17:38:24 -0400 Subject: [PATCH 072/198] revert order --- .../ResourceGrowthBarChartLegend.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.tsx b/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.tsx index 2e8e0f07a1..6b5ce17c95 100644 --- a/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.tsx +++ b/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.tsx @@ -19,7 +19,7 @@ import { Box, useTheme } from '@material-ui/core'; import { LegendItem } from '../LegendItem'; import { CostGrowth } from '../CostGrowth'; import { currencyFormatter } from '../../utils/formatters'; -import { ChangeStatistic, Duration, CostInsightsTheme } from '../../types'; +import { ChangeStatistic, CostInsightsTheme, Duration } from '../../types'; export type ResourceGrowthBarChartLegendProps = { change: ChangeStatistic; From 7926f07735514f2fe6d76f5dad8f09597742ea61 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Thu, 29 Oct 2020 17:39:02 -0400 Subject: [PATCH 073/198] make default date format public --- plugins/cost-insights/src/types/Duration.ts | 2 ++ plugins/cost-insights/src/utils/duration.ts | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/cost-insights/src/types/Duration.ts b/plugins/cost-insights/src/types/Duration.ts index a1eb9aa31e..acf707dd1e 100644 --- a/plugins/cost-insights/src/types/Duration.ts +++ b/plugins/cost-insights/src/types/Duration.ts @@ -26,3 +26,5 @@ export enum Duration { P1M = 'P1M', P3M = 'P3M', } + +export const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD'; diff --git a/plugins/cost-insights/src/utils/duration.ts b/plugins/cost-insights/src/utils/duration.ts index d90e78dc5f..6a880b382c 100644 --- a/plugins/cost-insights/src/utils/duration.ts +++ b/plugins/cost-insights/src/utils/duration.ts @@ -15,11 +15,9 @@ */ import moment from 'moment'; -import { Duration } from '../types'; +import { Duration, DEFAULT_DATE_FORMAT } from '../types'; import { assertNever } from './assert'; -export const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD'; - /** * Derive the start date of a given period, assuming two repeating intervals. * From ce0e0f12c5d9cf991e980d91ec6da7ab50424b2c Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Thu, 29 Oct 2020 17:41:38 -0400 Subject: [PATCH 074/198] formatting --- plugins/cost-insights/src/client.ts | 5 ++++- .../src/components/CostGrowth/CostGrowth.tsx | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/plugins/cost-insights/src/client.ts b/plugins/cost-insights/src/client.ts index 979a7a44ef..914fdb7b64 100644 --- a/plugins/cost-insights/src/client.ts +++ b/plugins/cost-insights/src/client.ts @@ -33,7 +33,10 @@ import { Trendline, UnlabeledDataflowData, } from '../src/types'; -import { ProjectGrowthAlert, UnlabeledDataflowAlert } from '../src/utils/alerts'; +import { + ProjectGrowthAlert, + UnlabeledDataflowAlert, +} from '../src/utils/alerts'; import { DEFAULT_DATE_FORMAT, exclusiveEndDateOf, diff --git a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx index da2f3e3c1b..89e44ee185 100644 --- a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx +++ b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx @@ -16,7 +16,13 @@ import React from 'react'; import classnames from 'classnames'; -import { ChangeStatistic, CurrencyType, Duration, EngineerThreshold, GrowthType } from '../../types'; +import { + ChangeStatistic, + CurrencyType, + Duration, + EngineerThreshold, + GrowthType, +} from '../../types'; import { rateOf } from '../../utils/currency'; import { growthOf } from '../../utils/change'; import { useCostGrowthStyles as useStyles } from '../../utils/styles'; From e9c222605bb1ae1fccfd0990a645ad576124796f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 29 Oct 2020 22:55:05 +0100 Subject: [PATCH 075/198] cli: fix create-plugin tmp dir creation --- .../create-plugin/createPlugin.test.ts | 27 +---------------- .../commands/create-plugin/createPlugin.ts | 29 ++++++------------- 2 files changed, 10 insertions(+), 46 deletions(-) diff --git a/packages/cli/src/commands/create-plugin/createPlugin.test.ts b/packages/cli/src/commands/create-plugin/createPlugin.test.ts index a08bb0da87..660caec189 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.test.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.test.ts @@ -17,9 +17,7 @@ import fs from 'fs-extra'; import path from 'path'; import mockFs from 'mock-fs'; -import os from 'os'; -import del from 'del'; -import { createTemporaryPluginFolder, movePlugin } from './createPlugin'; +import { movePlugin } from './createPlugin'; const id = 'testPluginMock'; @@ -28,29 +26,6 @@ describe('createPlugin', () => { mockFs.restore(); }); - describe('createPluginFolder', () => { - it('should create a temporary plugin directory in the correct place', async () => { - const tempDir = path.join(os.tmpdir(), id); - try { - await createTemporaryPluginFolder(tempDir); - await expect(fs.pathExists(tempDir)).resolves.toBe(true); - expect(tempDir).toMatch(id); - } finally { - await del(tempDir, { force: true }); - } - }); - - it('should not create a temporary plugin directory if it already exists', async () => { - mockFs({ - [id]: {}, - }); - - await expect(createTemporaryPluginFolder(id)).rejects.toThrow( - /Failed to create temporary plugin directory/, - ); - }); - }); - describe('movePlugin', () => { it('should move the temporary plugin directory to its final place', async () => { mockFs({ diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index f54a62f613..c9f8b3a81d 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -19,7 +19,7 @@ import { promisify } from 'util'; import chalk from 'chalk'; import inquirer, { Answers, Question } from 'inquirer'; import { exec as execCb } from 'child_process'; -import { resolve as resolvePath } from 'path'; +import { resolve as resolvePath, join as joinPath } from 'path'; import os from 'os'; import { Command } from 'commander'; import { @@ -46,18 +46,6 @@ async function checkExists(destination: string) { }); } -export async function createTemporaryPluginFolder(tempDir: string) { - await Task.forItem('creating', 'temporary directory', async () => { - try { - await fs.mkdir(tempDir); - } catch (error) { - throw new Error( - `Failed to create temporary plugin directory: ${error.message}`, - ); - } - }); -} - const sortObjectByKeys = (obj: { [name in string]: string }) => { return Object.keys(obj) .sort() @@ -238,7 +226,6 @@ export default async (cmd: Command) => { ? 'templates/default-backend-plugin' : 'templates/default-plugin', ); - const tempDir = resolvePath(os.tmpdir(), pluginId); const pluginDir = isMonoRepo ? paths.resolveTargetRoot('plugins', pluginId) : paths.resolveTargetRoot(pluginId); @@ -250,13 +237,15 @@ export default async (cmd: Command) => { Task.log(); Task.log('Creating the plugin...'); + Task.section('Checking if the plugin ID is available'); + await checkExists(pluginDir); + + Task.section('Creating a temporary plugin directory'); + const tempDir = await fs.mkdtemp( + joinPath(os.tmpdir(), `backstage-plugin-${pluginId}`), + ); + try { - Task.section('Checking if the plugin ID is available'); - await checkExists(pluginDir); - - Task.section('Creating a temporary plugin directory'); - await createTemporaryPluginFolder(tempDir); - Task.section('Preparing files'); await templatingTask( From 21ba4d38a0f6359e8a08649b6c74bd098c457d23 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Thu, 29 Oct 2020 18:00:56 -0400 Subject: [PATCH 076/198] (docs) Clarifications to documentation & UI chapter rename (#3159) * Minor wording tweaks * Rename section --- docs/overview/adopting.md | 12 ++++++------ docs/overview/architecture-overview.md | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/overview/adopting.md b/docs/overview/adopting.md index 422adfb288..0d404d5cb4 100644 --- a/docs/overview/adopting.md +++ b/docs/overview/adopting.md @@ -42,7 +42,7 @@ infra/platform teams and end-users: ![pop](../assets/pop.png) While anyone at your company can contribute to the platform, the vast majority -of work will be done by teams that also has internal engineers as their +of work will be done by teams that also have internal engineers as their customers. The central team should treat these _contributing teams_ as customers of the platform as well. @@ -51,9 +51,9 @@ customers. This is done primarily by building [plugins](../plugins/index.md). Contributing teams should themselves treat their plugins as, or part of, the products they maintain. -> Case study: Inside Spotify we have a team that owns our CI platform. They -> don't only maintain the pipelines and build servers, but also expose their -> product in Backstage through a plugin. Since they also +> Case study: Inside Spotify we have a team that owns our CI platform. They not +> only maintain the pipelines and build servers, but also expose their product +> in Backstage through a plugin. Since they also > [maintain their own API](../plugins/call-existing-api.md), they can improve > their product by iterating on API and UI in lockstep. Because the plugin > follows our [platform design guidelines](../dls/design.md) their customers get @@ -123,14 +123,14 @@ successful impact on your software development process: engineer is someone that is able to contribute to different domains of engineering. Teams with T-shaped people have fewer bottlenecks and can therefore deliver more consistently. Backstage makes it easier to be T-shaped - since tools and infrastructure is consistent between domains, and information + since tools and infrastructure are consistent between domains, and information is available centrally. - **eNPS** Surveys asking about how productive people feel, how easy it is to find information and overall satisfaction with internal tools. - **Fragmentation** _(Experimental)_ Backstage - [Software Templates](../features/software-templates/index.md) helps drive + [Software Templates](../features/software-templates/index.md) help drive standardization in your software ecosystem. By measuring the variance in technology between different software components it is possible to get a sense of the overall fragmentation in your ecosystem. Examples could include: diff --git a/docs/overview/architecture-overview.md b/docs/overview/architecture-overview.md index ee2d3c6097..6f3c6b1a71 100644 --- a/docs/overview/architecture-overview.md +++ b/docs/overview/architecture-overview.md @@ -39,7 +39,7 @@ this. ![The architecture of a basic Backstage application](../assets/architecture-overview/backstage-typical-architecture.png) -## The UI +## User Interface The UI is a thin, client-side wrapper around a set of plugins. It provides some core UI components and libraries for shared activities such as config From 94d351d605ae26cf06aa9c4e3646bf9d85eca48c Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Thu, 29 Oct 2020 18:46:17 -0400 Subject: [PATCH 077/198] fix imports --- plugins/cost-insights/src/client.ts | 2 +- .../CostOverviewCard/CostOverviewTooltip.tsx | 2 +- plugins/cost-insights/src/utils/change.ts | 22 +++++++------------ 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/plugins/cost-insights/src/client.ts b/plugins/cost-insights/src/client.ts index 914fdb7b64..f7671bcce0 100644 --- a/plugins/cost-insights/src/client.ts +++ b/plugins/cost-insights/src/client.ts @@ -23,6 +23,7 @@ import { ChangeStatistic, Cost, DateAggregation, + DEFAULT_DATE_FORMAT, Duration, Group, Maybe, @@ -38,7 +39,6 @@ import { UnlabeledDataflowAlert, } from '../src/utils/alerts'; import { - DEFAULT_DATE_FORMAT, exclusiveEndDateOf, inclusiveStartDateOf, } from '../src/utils/duration'; diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewTooltip.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewTooltip.tsx index 51f2f12bdb..e0a9b9a9ea 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewTooltip.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewTooltip.tsx @@ -17,7 +17,7 @@ import React from 'react'; import moment from 'moment'; import { TooltipPayload, TooltipProps } from 'recharts'; import { Tooltip, TooltipItemProps } from '../../components/Tooltip'; -import { DEFAULT_DATE_FORMAT } from '../../utils/duration'; +import { DEFAULT_DATE_FORMAT } from '../../types'; export type CostOverviewTooltipProps = TooltipProps & { dataKeys: Array; diff --git a/plugins/cost-insights/src/utils/change.ts b/plugins/cost-insights/src/utils/change.ts index f5c6570b36..1fe3662537 100644 --- a/plugins/cost-insights/src/utils/change.ts +++ b/plugins/cost-insights/src/utils/change.ts @@ -14,22 +14,16 @@ * limitations under the License. */ -import { Cost, ChangeStatistic, MetricData } from '../types'; +import { + Cost, + ChangeStatistic, + ChangeThreshold, + EngineerThreshold, + GrowthType, + MetricData, +} from '../types'; import { aggregationSort } from '../utils/sort'; -export const EngineerThreshold = 0.5; - -export enum ChangeThreshold { - upper = 0.05, - lower = -0.05, -} - -export enum GrowthType { - Negligible, - Savings, - Excess, -} - // Used by for displaying status colors export function growthOf(amount: number, ratio: number) { if (amount >= EngineerThreshold && ratio >= ChangeThreshold.upper) { From 1ae2a687f08045e230dc604281e42b42649856f9 Mon Sep 17 00:00:00 2001 From: Mayursinh Sarvaiya Date: Fri, 30 Oct 2020 12:06:55 +0530 Subject: [PATCH 078/198] chore: better position for job stage loader (#3170) --- .../src/components/JobStage/JobStage.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/components/JobStage/JobStage.tsx b/plugins/scaffolder/src/components/JobStage/JobStage.tsx index 5128a761ec..8b3fc04de0 100644 --- a/plugins/scaffolder/src/components/JobStage/JobStage.tsx +++ b/plugins/scaffolder/src/components/JobStage/JobStage.tsx @@ -73,6 +73,18 @@ const useStyles = makeStyles(theme => ({ boxShadow: `inset 4px 0px 0px ${theme.palette.success.main}`, }, }, + jobStatusTitle: { + display: 'flex', + width: '100%', + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'space-between', + [theme.breakpoints.down('xs')]: { + flexDirection: 'column', + alignItems: 'flex-start', + justifyContent: 'flex-start', + }, + }, })); type Props = { @@ -118,7 +130,7 @@ export const JobStage = ({ endedAt, startedAt, name, log, status }: Props) => { className: classes.button, }} > - + {name} {timeElapsed && `(${timeElapsed})`}{' '} {startedAt && !endedAt && } From 16aefb099ed2892eba3919e798c65c7102e6da00 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 29 Oct 2020 20:58:03 +0100 Subject: [PATCH 079/198] Revert "feat: upgrade ts-jest to 26.3.0" This reverts commit 4d66ae31c266b9b1de6b1523306a57c14a58f046. --- packages/cli/package.json | 2 +- yarn.lock | 110 ++++++++------------------------------ 2 files changed, 22 insertions(+), 90 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 00bf463018..668ec5e000 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -96,7 +96,7 @@ "sucrase": "^3.16.0", "tar": "^6.0.1", "terser-webpack-plugin": "^1.4.3", - "ts-jest": "^26.3.0", + "ts-jest": "^26.0.0", "ts-loader": "^7.0.4", "typescript": "^4.0.3", "url-loader": "^4.1.0", diff --git a/yarn.lock b/yarn.lock index 52f797f241..70b224a600 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2315,11 +2315,6 @@ slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/create-cache-key-function@^26.5.0": - version "26.5.0" - 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" @@ -2466,17 +2461,6 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@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== - 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" @@ -5258,14 +5242,6 @@ jest-diff "^25.2.1" pretty-format "^25.2.1" -"@types/jest@26.x": - version "26.0.15" - resolved "https://registry.npmjs.org/@types/jest/-/jest-26.0.15.tgz#12e02c0372ad0548e07b9f4e19132b834cb1effe" - integrity sha512-s2VMReFXRg9XXxV+CW9e5Nz8fH2K1aEhwgjUqPPbQd7g95T0laAcvLv032EhFHIa5GHsZ8W7iJEQVaJq6k3Gog== - dependencies: - jest-diff "^26.0.0" - pretty-format "^26.0.0" - "@types/jquery@^3.3.34": version "3.5.1" resolved "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.1.tgz#cebb057acf5071c40e439f30e840c57a30d406c3" @@ -14374,16 +14350,6 @@ jest-diff@^25.2.1: jest-get-type "^25.2.6" pretty-format "^25.5.0" -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== - dependencies: - chalk "^4.0.0" - diff-sequences "^26.5.0" - jest-get-type "^26.3.0" - pretty-format "^26.6.1" - jest-diff@^26.5.2: version "26.5.2" resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-26.5.2.tgz#8e26cb32dc598e8b8a1b9deff55316f8313c8053" @@ -14661,18 +14627,6 @@ jest-snapshot@^26.5.3: pretty-format "^26.5.2" semver "^7.3.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== - dependencies: - "@jest/types" "^26.6.1" - "@types/node" "*" - chalk "^4.0.0" - graceful-fs "^4.2.4" - is-ci "^2.0.0" - micromatch "^4.0.2" - jest-util@^26.5.2: version "26.5.2" resolved "https://registry.npmjs.org/jest-util/-/jest-util-26.5.2.tgz#8403f75677902cc52a1b2140f568e91f8ed4f4d7" @@ -16244,6 +16198,14 @@ microevent.ts@~0.1.1: resolved "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0" integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g== +micromatch@4.x, micromatch@^4.0.0, micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -16263,14 +16225,6 @@ micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.0, micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== - dependencies: - braces "^3.0.1" - picomatch "^2.0.5" - miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -18857,16 +18811,6 @@ pretty-format@^25.2.1, pretty-format@^25.5.0: ansi-styles "^4.0.0" react-is "^16.12.0" -pretty-format@^26.0.0, 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== - dependencies: - "@jest/types" "^26.6.1" - ansi-regex "^5.0.0" - ansi-styles "^4.0.0" - react-is "^17.0.1" - pretty-format@^26.4.2: version "26.4.2" resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-26.4.2.tgz#d081d032b398e801e2012af2df1214ef75a81237" @@ -19552,11 +19496,6 @@ react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-i resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.1: - version "17.0.1" - resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" - integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== - react-lazylog@^4.5.2, react-lazylog@^4.5.3: version "4.5.3" resolved "https://registry.npmjs.org/react-lazylog/-/react-lazylog-4.5.3.tgz#289e24995b5599e75943556ac63f5e2c04d0001e" @@ -22800,23 +22739,21 @@ ts-invariant@^0.4.0: dependencies: tslib "^1.9.3" -ts-jest@^26.3.0: - version "26.4.3" - resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-26.4.3.tgz#d153a616033e7ec8544b97ddbe2638cbe38d53db" - integrity sha512-pFDkOKFGY+nL9v5pkhm+BIFpoAuno96ff7GMnIYr/3L6slFOS365SI0fGEVYx2RKGji5M2elxhWjDMPVcOCdSw== +ts-jest@^26.0.0: + version "26.1.1" + resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-26.1.1.tgz#b98569b8a4d4025d966b3d40c81986dd1c510f8d" + integrity sha512-Lk/357quLg5jJFyBQLnSbhycnB3FPe+e9i7ahxokyXxAYoB0q1pPmqxxRPYr4smJic1Rjcf7MXDBhZWgxlli0A== dependencies: - "@jest/create-cache-key-function" "^26.5.0" - "@types/jest" "26.x" bs-logger "0.x" buffer-from "1.x" fast-json-stable-stringify "2.x" - jest-util "^26.1.0" json5 "2.x" lodash.memoize "4.x" make-error "1.x" + micromatch "4.x" mkdirp "1.x" semver "7.x" - yargs-parser "20.x" + yargs-parser "18.x" ts-loader@^7.0.4: version "7.0.4" @@ -24277,10 +24214,13 @@ yaml@*, yaml@^1.10.0, yaml@^1.7.2, yaml@^1.9.2: resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== -yargs-parser@20.x: - version "20.2.3" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.3.tgz#92419ba867b858c868acf8bae9bf74af0dd0ce26" - integrity sha512-emOFRT9WVHw03QSvN5qor9QQT9+sw5vwxfYweivSMHTcAXPefwVae2FjO7JJjj8hCE4CzPOPeFM83VwT29HCww== +yargs-parser@18.x, yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" yargs-parser@^10.0.0: version "10.1.0" @@ -24305,14 +24245,6 @@ yargs-parser@^15.0.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^18.1.2: - version "18.1.3" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - yargs-parser@^20.0.0: version "20.2.1" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.1.tgz#28f3773c546cdd8a69ddae68116b48a5da328e77" From 16d90dca247d8fcaf2ebd04c01f84dc317e2fc7c Mon Sep 17 00:00:00 2001 From: Mayursinh Sarvaiya Date: Fri, 30 Oct 2020 13:40:59 +0530 Subject: [PATCH 080/198] fix: failing windows tests (#3171) --- .../src/scaffolder/stages/prepare/azure.test.ts | 4 ++-- .../src/scaffolder/stages/prepare/github.test.ts | 4 ++-- .../src/scaffolder/stages/prepare/gitlab.test.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts index 76dd842368..db3a110384 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/azure.test.ts @@ -152,8 +152,8 @@ describe('AzurePreparer', () => { workingDirectory: '/workDir', }); - expect(response).toBe( - '/workDir/graphql-starter-static/template/test/1/2/3', + expect(response.split('\\').join('/')).toMatch( + /\/workDir\/graphql-starter-static\/template\/test\/1\/2\/3$/, ); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index af3932d71a..86a870330e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -121,8 +121,8 @@ describe('GitHubPreparer', () => { workingDirectory: '/workDir', }); - expect(response).toBe( - '/workDir/graphql-starter-static/template/test/1/2/3', + expect(response.split('\\').join('/')).toMatch( + /\/workDir\/graphql-starter-static\/template\/test\/1\/2\/3$/, ); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts index 6be3f790f5..a86a51e0dc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.test.ts @@ -156,8 +156,8 @@ describe('GitLabPreparer', () => { workingDirectory: '/workDir', }); - expect(response).toBe( - '/workDir/graphql-starter-static/template/test/1/2/3', + expect(response.split('\\').join('/')).toMatch( + /\/workDir\/graphql-starter-static\/template\/test\/1\/2\/3$/, ); }); }); From efff67c205aed75997a1badca2e91385f236e662 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Fri, 30 Oct 2020 09:49:53 +0100 Subject: [PATCH 081/198] TechDocs: Update docs around docker-in-docker installation Fix python package name to mkdocs-techdocs-core Refer to the original Dockerfile where users can seek inspiration from --- docs/features/techdocs/getting-started.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index 72efb6eee0..288ddb74ae 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -73,10 +73,10 @@ required. ### Disable Docker in Docker situation (Optional) -The TechDocs backend plugin runs a docker container with mkdocs to generate the -frontend of the docs from source files (Markdown). If you are deploying -Backstage using Docker, this will mean that your Backstage Docker container will -try to run another Docker container for TechDocs backend. +The TechDocs backend plugin runs a docker container with mkdocs installed to +generate the frontend of the docs from source files (Markdown). If you are +deploying Backstage using Docker, this will mean that your Backstage Docker +container will try to run another Docker container for TechDocs backend. To avoid this problem, we have a configuration available. You can set a value in your `app-config.yaml` that tells the techdocs generator if it should run the @@ -90,10 +90,16 @@ techdocs: ``` Setting `generators.techdocs` to `local` means you will have to make sure your -environment is compatible with techdocs. You will have to install the -`mkdocs-techdocs-container` and 'mkdocs' package from pip, as well as graphviz -and plantuml from your package manager. This has only been tested with python -3.7 and python 3.8. +environment is compatible with techdocs. + +You will have to install the `mkdocs-techdocs-core` and `mkdocs` package from +pip, as well as `graphviz` and `plantuml` from your OS package manager (e.g. +apt). See our +[Dockerfile](https://github.com/spotify/backstage/blob/master/packages/techdocs-container/Dockerfile) +for the latest requirements. You should be trying to match your Dockerfile with +this one. + +It is worth mentioning that we recommend Python version 3.7 or higher. ## Run Backstage locally From c0d5242a0dc9315f635447f675ae4dbebe1db238 Mon Sep 17 00:00:00 2001 From: Giuliano Varriale Date: Fri, 30 Oct 2020 09:50:18 +0100 Subject: [PATCH 082/198] fix(StructuredMetadataTable): proper render boolean values (#3162) --- .changeset/popular-ghosts-remain.md | 5 +++++ .../StructuredMetadataTable.stories.tsx | 2 ++ .../StructuredMetadataTable.test.tsx | 22 ++++++++++++++++++- .../StructuredMetadataTable.tsx | 6 ++++- 4 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 .changeset/popular-ghosts-remain.md diff --git a/.changeset/popular-ghosts-remain.md b/.changeset/popular-ghosts-remain.md new file mode 100644 index 0000000000..4a170c7618 --- /dev/null +++ b/.changeset/popular-ghosts-remain.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Proper render boolean values on StructuredMetadataTable component diff --git a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx index 97591ebab3..fcaa92457f 100644 --- a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx +++ b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.stories.tsx @@ -24,6 +24,8 @@ const metadata = { description: 'This is a long description of what this is doing (and some additional info too). \n It has new lines and extra text to make it especially annoying to render. But it just ignores them.', something: 'Yes', + 'true value': true, + 'false value': false, owner: 'squad', 'longer key name': ['v1', 'v2', 'v3'], rules: { diff --git a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx index 68d4262c3e..0844710876 100644 --- a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx +++ b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { render } from '@testing-library/react'; +import { render, within } from '@testing-library/react'; import { startCase } from 'lodash'; import React from 'react'; import { StructuredMetadataTable } from './StructuredMetadataTable'; @@ -70,6 +70,26 @@ describe('', () => { }); }); + it('Supports boolean values', () => { + const metadata = { foo: true, bar: false }; + const expectedValues = [ + ['Foo', '✅'], + ['Bar', '❌'], + ]; + + const { getAllByRole } = render( + , + ); + + getAllByRole('row').forEach((row, index) => { + const [firstCell, secondCell] = within(row).getAllByRole('cell'); + const [expectedKey, expectedValue] = expectedValues[index]; + + expect(firstCell).toHaveTextContent(expectedKey); + expect(secondCell).toHaveTextContent(expectedValue); + }); + }); + it('Supports react elements', () => { const metadata = { react:
field
}; const { getByText } = render( diff --git a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx index 52ad9a320a..4868af54bd 100644 --- a/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx +++ b/packages/core/src/components/StructuredMetadataTable/StructuredMetadataTable.tsx @@ -78,7 +78,7 @@ function renderMap( } function toValue( - value: ReactElement | object | Array, + value: ReactElement | object | Array | boolean, options?: any, nested?: boolean, ) { @@ -94,6 +94,10 @@ function toValue( return renderList(value, nested); } + if (typeof value === 'boolean') { + return {value ? '✅' : '❌'}; + } + return {value}; } From da0a79c3b8c64b97bc08fa3db797486e81684fa8 Mon Sep 17 00:00:00 2001 From: Dominik Henneke Date: Fri, 30 Oct 2020 09:52:39 +0100 Subject: [PATCH 083/198] feat: add sonarqube plugin (#3160) --- .../well-known-annotations.md | 16 ++ microsite/data/plugins/sonarqube.yaml | 9 + microsite/static/img/sonarqube-icon.svg | 41 ++++ plugins/sonarqube/.eslintrc.js | 3 + plugins/sonarqube/README.md | 95 ++++++++ plugins/sonarqube/dev/index.tsx | 20 ++ plugins/sonarqube/docs/sonar-card.png | Bin 0 -> 75595 bytes plugins/sonarqube/package.json | 52 ++++ plugins/sonarqube/src/api/index.test.ts | 161 ++++++++++++ plugins/sonarqube/src/api/index.ts | 110 +++++++++ plugins/sonarqube/src/api/types.ts | 55 +++++ .../components/SonarQubeCard/Percentage.tsx | 45 ++++ .../src/components/SonarQubeCard/Rating.tsx | 112 +++++++++ .../components/SonarQubeCard/RatingCard.tsx | 78 ++++++ .../SonarQubeCard/SonarQubeCard.tsx | 230 ++++++++++++++++++ .../src/components/SonarQubeCard/Value.tsx | 34 +++ .../src/components/SonarQubeCard/index.ts | 17 ++ plugins/sonarqube/src/components/index.ts | 17 ++ .../sonarqube/src/components/useProjectKey.ts | 23 ++ plugins/sonarqube/src/index.ts | 18 ++ plugins/sonarqube/src/plugin.test.ts | 23 ++ plugins/sonarqube/src/plugin.ts | 38 +++ plugins/sonarqube/src/setupTests.ts | 17 ++ 23 files changed, 1214 insertions(+) create mode 100644 microsite/data/plugins/sonarqube.yaml create mode 100644 microsite/static/img/sonarqube-icon.svg create mode 100644 plugins/sonarqube/.eslintrc.js create mode 100644 plugins/sonarqube/README.md create mode 100644 plugins/sonarqube/dev/index.tsx create mode 100644 plugins/sonarqube/docs/sonar-card.png create mode 100644 plugins/sonarqube/package.json create mode 100644 plugins/sonarqube/src/api/index.test.ts create mode 100644 plugins/sonarqube/src/api/index.ts create mode 100644 plugins/sonarqube/src/api/types.ts create mode 100644 plugins/sonarqube/src/components/SonarQubeCard/Percentage.tsx create mode 100644 plugins/sonarqube/src/components/SonarQubeCard/Rating.tsx create mode 100644 plugins/sonarqube/src/components/SonarQubeCard/RatingCard.tsx create mode 100644 plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx create mode 100644 plugins/sonarqube/src/components/SonarQubeCard/Value.tsx create mode 100644 plugins/sonarqube/src/components/SonarQubeCard/index.ts create mode 100644 plugins/sonarqube/src/components/index.ts create mode 100644 plugins/sonarqube/src/components/useProjectKey.ts create mode 100644 plugins/sonarqube/src/index.ts create mode 100644 plugins/sonarqube/src/plugin.test.ts create mode 100644 plugins/sonarqube/src/plugin.ts create mode 100644 plugins/sonarqube/src/setupTests.ts diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index 40a4ca3ea1..47b1d5885d 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -173,6 +173,22 @@ The value of these annotations are the corresponding attributes that were found when ingestion the entity from LDAP. Not all of them may be present, depending on what attributes that the server presented at ingestion time. +### sonarqube.org/project-key + +```yaml +# Example: +metadata: + annotations: + sonarqube.org/project-key: pump-station +``` + +The value of this annotation is the project key of a +[SonarQube](https://sonarqube.org) or [SonarCloud](https://sonarcloud.io) +project within your organization. + +Specifying this annotation may enable SonarQube related features in Backstage +for that entity. + ## Deprecated Annotations The following annotations are deprecated, and only listed here to aid in diff --git a/microsite/data/plugins/sonarqube.yaml b/microsite/data/plugins/sonarqube.yaml new file mode 100644 index 0000000000..a2982a4a58 --- /dev/null +++ b/microsite/data/plugins/sonarqube.yaml @@ -0,0 +1,9 @@ +--- +title: SonarQube +author: SDA SE +authorUrl: https://sda.se/ +category: Quality +description: Components to display code quality metrics from SonarCloud and SonarQube. +documentation: https://github.com/spotify/backstage/blob/master/plugins/sonarqube/README.md +iconUrl: img/sonarqube-icon.svg +npmPackageName: '@backstage/plugin-sonarqube' diff --git a/microsite/static/img/sonarqube-icon.svg b/microsite/static/img/sonarqube-icon.svg new file mode 100644 index 0000000000..d3de342b38 --- /dev/null +++ b/microsite/static/img/sonarqube-icon.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/sonarqube/.eslintrc.js b/plugins/sonarqube/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/sonarqube/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/sonarqube/README.md b/plugins/sonarqube/README.md new file mode 100644 index 0000000000..3d3c1f725b --- /dev/null +++ b/plugins/sonarqube/README.md @@ -0,0 +1,95 @@ +# SonarQube Plugin + +The SonarQube Plugin displays code statistics from [SonarCloud](https://sonarcloud.io) or [SonarQube](https://sonarqube.com). + +![Sonar Card](./docs/sonar-card.png) + +## Getting Started + +1. Install the SonarQube Plugin: + +```bash +yarn add @backstage/plugin-sonarqube +``` + +2. Add plugin to the app: + +```js +// packages/app/src/plugins.ts + +export { plugin as SonarQube } from '@backstage/plugin-sonarqube'; +``` + +3. Add the `SonarQubeCard` to the EntityPage: + +```jsx +// packages/app/src/components/catalog/EntityPage.tsx + +import { SonarQubeCard } from '@backstage/plugin-sonarqube'; + +const OverviewContent = ({ entity }: { entity: Entity }) => ( + + // ... + + + + // ... + +); +``` + +4. Add the proxy config: + +**SonarCloud** + +```yaml +// app-config.yaml + +proxy: + '/sonarqube': + target: https://sonarcloud.io/api + allowedMethods: ['GET'] + headers: + Authorization: + # Content: 'Basic base64(":")' <-- note the trailing ':' + # Example: Basic bXktYXBpLWtleTo= + $env: SONARQUBE_AUTH_HEADER +``` + +**SonarQube** + +```yaml +// app-config.yaml + +proxy: + '/sonarqube': + target: https://your.sonarqube.instance.com/api + allowedMethods: ['GET'] + headers: + Authorization: + # Content: 'Basic base64(":")' <-- note the trailing ':' + # Example: Basic bXktYXBpLWtleTo= + $env: SONARQUBE_AUTH_HEADER + +sonarQube: + baseUrl: https://your.sonarqube.instance.com +``` + +5. Get and provide `SONARQUBE_AUTH_HEADER` as env variable (https://sonarcloud.io/account/security or https://docs.sonarqube.org/latest/user-guide/user-token/) + +6. Add the `sonarqube.org/project-key` annotation to your component-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: + sonarqube.org/project-key: YOUR_PROJECT_KEY +spec: + type: library + owner: Spotify + lifecycle: experimental +``` diff --git a/plugins/sonarqube/dev/index.tsx b/plugins/sonarqube/dev/index.tsx new file mode 100644 index 0000000000..812a5585d4 --- /dev/null +++ b/plugins/sonarqube/dev/index.tsx @@ -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 { createDevApp } from '@backstage/dev-utils'; +import { plugin } from '../src/plugin'; + +createDevApp().registerPlugin(plugin).render(); diff --git a/plugins/sonarqube/docs/sonar-card.png b/plugins/sonarqube/docs/sonar-card.png new file mode 100644 index 0000000000000000000000000000000000000000..cbab02edd63b1598b05981bdc0d42e506b97569a GIT binary patch literal 75595 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�&QZgZR|qRWnyVNSL~NZ7*3m}bwjs4*t?tWPY1?i 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 { + msw.setupDefaultHandlers(server); + + const mockBaseUrl = 'http://backstage:9191/api/proxy'; + const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); + + const setupHandlers = () => { + server.use( + rest.get(`${mockBaseUrl}/sonarqube/components/show`, (req, res, ctx) => { + expect(req.url.searchParams.toString()).toBe('component=our-service'); + return res( + ctx.json({ + component: { + analysisDate: '2020-01-01T00:00:00Z', + }, + } as ComponentWrapper), + ); + }), + ); + + server.use( + rest.get(`${mockBaseUrl}/sonarqube/measures/search`, (req, res, ctx) => { + expect(req.url.searchParams.toString()).toBe( + 'projectKeys=our-service&metricKeys=alert_status%2Cbugs%2Creliability_rating%2Cvulnerabilities%2Csecurity_rating%2Ccode_smells%2Csqale_rating%2Ccoverage%2Cduplicated_lines_density', + ); + return res( + ctx.json({ + measures: [ + { + metric: 'alert_status', + value: 'OK', + component: 'our-service', + }, + { + metric: 'alert_status', + value: 'ERROR', + component: 'other-service', + }, + { + metric: 'bugs', + value: '2', + component: 'our-service', + }, + { + metric: 'reliability_rating', + value: '3.0', + component: 'our-service', + }, + { + metric: 'vulnerabilities', + value: '4', + component: 'our-service', + }, + { + metric: 'security_rating', + value: '1.0', + component: 'our-service', + }, + { + metric: 'code_smells', + value: '100', + component: 'our-service', + }, + { + metric: 'sqale_rating', + value: '2.0', + component: 'our-service', + }, + { + metric: 'coverage', + value: '55.5', + component: 'our-service', + }, + { + metric: 'duplicated_lines_density', + value: '1.0', + component: 'our-service', + }, + ], + } as MeasuresWrapper), + ); + }), + ); + }; + + it('should report finding summary', async () => { + setupHandlers(); + + const client = new SonarQubeApi({ discoveryApi }); + + const summary = await client.getFindingSummary('our-service'); + + expect(summary).toEqual({ + lastAnalysis: '2020-01-01T00:00:00Z', + metrics: { + alert_status: 'OK', + bugs: '2', + reliability_rating: '3.0', + vulnerabilities: '4', + security_rating: '1.0', + code_smells: '100', + sqale_rating: '2.0', + coverage: '55.5', + duplicated_lines_density: '1.0', + }, + projectUrl: 'https://sonarcloud.io/dashboard?id=our-service', + } as FindingSummary); + }); + + it('should report finding summary (custom baseUrl)', async () => { + setupHandlers(); + + const client = new SonarQubeApi({ + discoveryApi, + baseUrl: 'http://a.instance.local', + }); + + const summary = await client.getFindingSummary('our-service'); + + expect(summary).toEqual({ + lastAnalysis: '2020-01-01T00:00:00Z', + metrics: { + alert_status: 'OK', + bugs: '2', + reliability_rating: '3.0', + vulnerabilities: '4', + security_rating: '1.0', + code_smells: '100', + sqale_rating: '2.0', + coverage: '55.5', + duplicated_lines_density: '1.0', + }, + projectUrl: 'http://a.instance.local/dashboard?id=our-service', + } as FindingSummary); + }); +}); diff --git a/plugins/sonarqube/src/api/index.ts b/plugins/sonarqube/src/api/index.ts new file mode 100644 index 0000000000..01f6f74fe3 --- /dev/null +++ b/plugins/sonarqube/src/api/index.ts @@ -0,0 +1,110 @@ +/* + * 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, DiscoveryApi } from '@backstage/core'; +import fetch from 'cross-fetch'; +import { ComponentWrapper, MeasuresWrapper, MetricKey } 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; +} + +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}`, + }; + } +} diff --git a/plugins/sonarqube/src/api/types.ts b/plugins/sonarqube/src/api/types.ts new file mode 100644 index 0000000000..17539070cb --- /dev/null +++ b/plugins/sonarqube/src/api/types.ts @@ -0,0 +1,55 @@ +/* + * 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 interface ComponentWrapper { + component: Component; +} + +export interface Component { + analysisDate: string; +} + +export interface MeasuresWrapper { + measures: Measure[]; +} + +export type MetricKey = + // alert status + | 'alert_status' + + // bugs and rating (-> reliability) + | 'bugs' + | 'reliability_rating' + + // vulnerabilities and rating (-> security) + | 'vulnerabilities' + | 'security_rating' + + // code smells and rating (-> maintainability) + | 'code_smells' + | 'sqale_rating' + + // coverage + | 'coverage' + + // duplicated lines + | 'duplicated_lines_density'; + +export interface Measure { + metric: MetricKey; + value: string; + component: string; +} diff --git a/plugins/sonarqube/src/components/SonarQubeCard/Percentage.tsx b/plugins/sonarqube/src/components/SonarQubeCard/Percentage.tsx new file mode 100644 index 0000000000..9d1fc415ff --- /dev/null +++ b/plugins/sonarqube/src/components/SonarQubeCard/Percentage.tsx @@ -0,0 +1,45 @@ +/* + * 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 { BackstageTheme } from '@backstage/theme'; +import { makeStyles } from '@material-ui/core/styles'; +import { useTheme } from '@material-ui/styles'; +import { Circle } from 'rc-progress'; +import React from 'react'; + +const useStyles = makeStyles((theme: BackstageTheme) => ({ + root: { + height: theme.spacing(3), + width: theme.spacing(3), + }, +})); + +export const Percentage = ({ value }: { value?: string }) => { + const classes = useStyles(); + const theme = useTheme(); + + return ( + + ); +}; diff --git a/plugins/sonarqube/src/components/SonarQubeCard/Rating.tsx b/plugins/sonarqube/src/components/SonarQubeCard/Rating.tsx new file mode 100644 index 0000000000..df90e2b821 --- /dev/null +++ b/plugins/sonarqube/src/components/SonarQubeCard/Rating.tsx @@ -0,0 +1,112 @@ +/* + * 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 { BackstageTheme } from '@backstage/theme'; +import { Avatar } from '@material-ui/core'; +import { lighten, makeStyles } from '@material-ui/core/styles'; +import { CSSProperties } from '@material-ui/styles'; +import React, { useMemo } from 'react'; + +const useStyles = makeStyles((theme: BackstageTheme) => { + const commonCardRating: CSSProperties = { + height: theme.spacing(3), + width: theme.spacing(3), + color: theme.palette.common.white, + }; + + return { + ratingDefault: { + ...commonCardRating, + background: theme.palette.status.aborted, + }, + ratingA: { + ...commonCardRating, + background: theme.palette.status.ok, + }, + ratingB: { + ...commonCardRating, + background: lighten(theme.palette.status.ok, 0.5), + }, + ratingC: { + ...commonCardRating, + background: theme.palette.status.pending, + }, + ratingD: { + ...commonCardRating, + background: theme.palette.status.warning, + }, + ratingE: { + ...commonCardRating, + background: theme.palette.error.main, + }, + }; +}); + +export const Rating = ({ + rating, + hideValue, +}: { + rating?: string; + hideValue?: boolean; +}) => { + const classes = useStyles(); + + const ratingProp = useMemo(() => { + switch (rating) { + case '1.0': + return { + name: 'A', + className: classes.ratingA, + }; + + case '2.0': + return { + name: 'B', + className: classes.ratingB, + }; + + case '3.0': + return { + name: 'C', + className: classes.ratingC, + }; + + case '4.0': + return { + name: 'D', + className: classes.ratingD, + }; + + case '5.0': + return { + name: 'E', + className: classes.ratingE, + }; + + default: + return { + name: '', + className: classes.ratingDefault, + }; + } + }, [classes, rating]); + + return ( + + {!hideValue && ratingProp.name} + + ); +}; diff --git a/plugins/sonarqube/src/components/SonarQubeCard/RatingCard.tsx b/plugins/sonarqube/src/components/SonarQubeCard/RatingCard.tsx new file mode 100644 index 0000000000..cd678b7366 --- /dev/null +++ b/plugins/sonarqube/src/components/SonarQubeCard/RatingCard.tsx @@ -0,0 +1,78 @@ +/* + * 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 { Grid, Typography } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import React, { ReactNode } from 'react'; + +const useStyles = makeStyles(theme => { + return { + root: { + margin: theme.spacing(1, 0), + }, + upper: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, + cardTitle: { + textAlign: 'center', + }, + wrapIcon: { + display: 'inline-flex', + verticalAlign: 'baseline', + }, + left: { + display: 'flex', + }, + right: { + display: 'flex', + marginLeft: theme.spacing(0.5), + }, + }; +}); + +export const RatingCard = ({ + leftSlot, + rightSlot, + title, + titleIcon, +}: { + leftSlot: ReactNode; + rightSlot: ReactNode; + title: string; + titleIcon?: ReactNode; +}) => { + const classes = useStyles(); + + return ( + + + + {leftSlot} + + + {rightSlot} + + + + + {titleIcon} {title} + + + + ); +}; diff --git a/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx b/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx new file mode 100644 index 0000000000..8e6cc817a1 --- /dev/null +++ b/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx @@ -0,0 +1,230 @@ +/* + * 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 { + EmptyState, + InfoCard, + MissingAnnotationEmptyState, + Progress, + useApi, +} from '@backstage/core'; +import { Chip, Grid } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import BugReport from '@material-ui/icons/BugReport'; +import LockOpen from '@material-ui/icons/LockOpen'; +import SentimentVeryDissatisfied from '@material-ui/icons/SentimentVeryDissatisfied'; +import React, { useMemo } from 'react'; +import { useAsync } from 'react-use'; +import { sonarQubeApiRef } from '../../api'; +import { + SONARQUBE_PROJECT_KEY_ANNOTATION, + useProjectKey, +} from '../useProjectKey'; +import { Percentage } from './Percentage'; +import { Rating } from './Rating'; +import { RatingCard } from './RatingCard'; +import { Value } from './Value'; + +const useStyles = makeStyles(theme => ({ + badgeLabel: { + color: theme.palette.common.white, + }, + badgeError: { + margin: 0, + backgroundColor: theme.palette.error.main, + }, + badgeSuccess: { + margin: 0, + backgroundColor: theme.palette.success.main, + }, + header: { + padding: theme.spacing(2, 2, 2, 2.5), + }, + action: { + margin: 0, + }, + lastAnalyzed: { + color: theme.palette.text.secondary, + }, + disabled: { + backgroundColor: theme.palette.background.default, + }, +})); + +interface DuplicationRating { + greaterThan: number; + rating: '1.0' | '2.0' | '3.0' | '4.0' | '5.0'; +} + +const defaultDuplicationRatings: DuplicationRating[] = [ + { greaterThan: 0, rating: '1.0' }, + { greaterThan: 3, rating: '2.0' }, + { greaterThan: 5, rating: '3.0' }, + { greaterThan: 10, rating: '4.0' }, + { greaterThan: 20, rating: '5.0' }, +]; + +export const SonarQubeCard = ({ + entity, + variant = 'gridItem', + duplicationRatings = defaultDuplicationRatings, +}: { + entity: Entity; + variant?: string; + duplicationRatings?: DuplicationRating[]; +}) => { + const sonarQubeApi = useApi(sonarQubeApiRef); + + const projectTitle = useProjectKey(entity); + + const { value, loading } = useAsync( + async () => sonarQubeApi.getFindingSummary(projectTitle), + [sonarQubeApi, projectTitle], + ); + + const deepLink = + !loading && value + ? { + title: 'View more', + link: value.projectUrl, + } + : undefined; + + const gatePassed = value && value.metrics.alert_status === 'OK'; + + const classes = useStyles(); + + const qualityBadge = !loading && value && ( + + ); + + const duplicationRating = useMemo(() => { + if (loading || !value || !value.metrics.duplicated_lines_density) { + return ''; + } + + let rating = ''; + + for (const r of duplicationRatings) { + if (+value.metrics.duplicated_lines_density >= r.greaterThan) { + rating = r.rating; + } + } + + return rating; + }, [loading, value, duplicationRatings]); + + return ( + + {loading && } + + {!loading && !projectTitle && ( + + )} + + {!loading && projectTitle && !value && ( + + )} + + {!loading && value && ( + <> + + + } + title="Bugs" + leftSlot={} + rightSlot={} + /> + } + title="Vulnerabilities" + leftSlot={} + rightSlot={} + /> + } + title="Code Smells" + leftSlot={} + rightSlot={} + /> +
+ } + rightSlot={} + /> + } + rightSlot={ + + } + /> + + + Last analyzed on{' '} + {new Date(value.lastAnalysis).toLocaleString('en-US', { + timeZone: 'UTC', + day: 'numeric', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: false, + })} + + + + )} + + ); +}; diff --git a/plugins/sonarqube/src/components/SonarQubeCard/Value.tsx b/plugins/sonarqube/src/components/SonarQubeCard/Value.tsx new file mode 100644 index 0000000000..e7a5492e68 --- /dev/null +++ b/plugins/sonarqube/src/components/SonarQubeCard/Value.tsx @@ -0,0 +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 { BackstageTheme } from '@backstage/theme'; +import { makeStyles } from '@material-ui/core/styles'; +import React from 'react'; + +const useStyles = makeStyles((theme: BackstageTheme) => { + return { + value: { + fontSize: '1.5rem', + fontWeight: theme.typography.fontWeightMedium, + }, + }; +}); + +export const Value = ({ value }: { value?: string }) => { + const classes = useStyles(); + + return {value}; +}; diff --git a/plugins/sonarqube/src/components/SonarQubeCard/index.ts b/plugins/sonarqube/src/components/SonarQubeCard/index.ts new file mode 100644 index 0000000000..e349f1e1f9 --- /dev/null +++ b/plugins/sonarqube/src/components/SonarQubeCard/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 { SonarQubeCard } from './SonarQubeCard'; diff --git a/plugins/sonarqube/src/components/index.ts b/plugins/sonarqube/src/components/index.ts new file mode 100644 index 0000000000..8f0786bb8e --- /dev/null +++ b/plugins/sonarqube/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 './SonarQubeCard'; diff --git a/plugins/sonarqube/src/components/useProjectKey.ts b/plugins/sonarqube/src/components/useProjectKey.ts new file mode 100644 index 0000000000..dcff79d972 --- /dev/null +++ b/plugins/sonarqube/src/components/useProjectKey.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 SONARQUBE_PROJECT_KEY_ANNOTATION = 'sonarqube.org/project-key'; + +export const useProjectKey = (entity: Entity) => { + return entity?.metadata.annotations?.[SONARQUBE_PROJECT_KEY_ANNOTATION] ?? ''; +}; diff --git a/plugins/sonarqube/src/index.ts b/plugins/sonarqube/src/index.ts new file mode 100644 index 0000000000..f09aeb1038 --- /dev/null +++ b/plugins/sonarqube/src/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 './components'; +export { plugin } from './plugin'; diff --git a/plugins/sonarqube/src/plugin.test.ts b/plugins/sonarqube/src/plugin.test.ts new file mode 100644 index 0000000000..32730b64c3 --- /dev/null +++ b/plugins/sonarqube/src/plugin.test.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 { plugin } from './plugin'; + +describe('sonarqube', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/plugins/sonarqube/src/plugin.ts b/plugins/sonarqube/src/plugin.ts new file mode 100644 index 0000000000..154d4ee3b8 --- /dev/null +++ b/plugins/sonarqube/src/plugin.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + configApiRef, + createApiFactory, + createPlugin, + discoveryApiRef, +} from '@backstage/core'; +import { SonarQubeApi, sonarQubeApiRef } from './api'; + +export const plugin = createPlugin({ + id: 'sonarqube', + apis: [ + createApiFactory({ + api: sonarQubeApiRef, + deps: { configApi: configApiRef, discoveryApi: discoveryApiRef }, + factory: ({ configApi, discoveryApi }) => + new SonarQubeApi({ + discoveryApi, + baseUrl: configApi.getOptionalString('sonarQube.baseUrl'), + }), + }), + ], +}); diff --git a/plugins/sonarqube/src/setupTests.ts b/plugins/sonarqube/src/setupTests.ts new file mode 100644 index 0000000000..825bcd4115 --- /dev/null +++ b/plugins/sonarqube/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'; From 801e5e55cc2668fd7979a8b885ab404804a9da6a Mon Sep 17 00:00:00 2001 From: Abhishek Jakhar Date: Fri, 30 Oct 2020 14:46:53 +0530 Subject: [PATCH 084/198] chore: remove redundant functions from microsite footer (#3174) --- microsite/core/Footer.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/microsite/core/Footer.js b/microsite/core/Footer.js index 1bebf2f399..d41b15b74c 100644 --- a/microsite/core/Footer.js +++ b/microsite/core/Footer.js @@ -17,19 +17,6 @@ const React = require('react'); class Footer extends React.Component { - docUrl(doc, language) { - const baseUrl = this.props.config.baseUrl; - const docsUrl = this.props.config.docsUrl; - const docsPart = `${docsUrl ? `${docsUrl}/` : ''}`; - const langPart = `${language ? `${language}/` : ''}`; - return `${baseUrl}${docsPart}${langPart}${doc}`; - } - - pageUrl(doc, language) { - const baseUrl = this.props.config.baseUrl; - return baseUrl + (language ? `${language}/` : '') + doc; - } - render() { return (