From f9a82d452e53ed8488019a09b765e2e1c30278a1 Mon Sep 17 00:00:00 2001 From: Steven Lougheed Date: Mon, 14 Feb 2022 09:44:12 -0500 Subject: [PATCH 001/353] Fixed path for loading cookiecutter.json Signed-off-by: slougheed --- .../src/actions/fetch/cookiecutter.ts | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts index c4d7f1e3e8..efc8b6215e 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts @@ -62,25 +62,34 @@ export class CookiecutterRunner { values: JsonObject; logStream: Writable; }): Promise { - const templateDir = path.join(workspacePath, 'template'); const intermediateDir = path.join(workspacePath, 'intermediate'); await fs.ensureDir(intermediateDir); const resultDir = path.join(workspacePath, 'result'); + const { + templateContentsDir, + templateDir, + imageName, + ...valuesForCookieCutterJson + } = values; // First lets grab the default cookiecutter.json file - const cookieCutterJson = await this.fetchTemplateCookieCutter(templateDir); + const cookieCutterJson = await this.fetchTemplateCookieCutter( + templateContentsDir as string, + ); - const { imageName, ...valuesForCookieCutterJson } = values; const cookieInfo = { ...cookieCutterJson, ...valuesForCookieCutterJson, }; - await fs.writeJSON(path.join(templateDir, 'cookiecutter.json'), cookieInfo); + await fs.writeJSON( + path.join(templateDir as string, 'cookiecutter.json'), + cookieInfo, + ); // Directories to bind on container const mountDirs = { - [templateDir]: '/input', + [templateDir as string]: '/input', [intermediateDir]: '/output', }; @@ -91,7 +100,13 @@ export class CookiecutterRunner { if (cookieCutterInstalled) { await runCommand({ command: 'cookiecutter', - args: ['--no-input', '-o', intermediateDir, templateDir, '--verbose'], + args: [ + '--no-input', + '-o', + intermediateDir, + templateDir as string, + '--verbose', + ], logStream, }); } else { @@ -233,6 +248,8 @@ export function createFetchCookiecutterAction(options: { _copy_without_render: ctx.input.copyWithoutRender, _extensions: ctx.input.extensions, imageName: ctx.input.imageName, + templateDir: templateDir, + templateContentsDir: templateContentsDir, }; // Will execute the template in ./template and put the result in ./result From 2a865343c2bcaa6c70fe123b0eff32debde42a0d Mon Sep 17 00:00:00 2001 From: Nikolas Skoufis Date: Wed, 16 Feb 2022 16:42:33 +1100 Subject: [PATCH 002/353] Add a new interface: DocsBuildStrategy This adds a new interface called DocsBuildStrategy. This strategy allows for different answers to the question: should the TechDocs backend perform a build of the given entity? The default implementation replicates the existing functionality, in that if the techdocs.builder config value is set to local, then we do trigger builds, and if the value is set to anything else, we don't trigger builds. However the strategy has access to the entity, and so more complex strategies are possible. This strategy is added as an optional parameter on the RouterOptions types, and defaults to the DefaultDocsBuildStrategy if unspecified, allowing for backwards compatibility. TODO: rename some of the config options, and reword errors to reflect the new interface Signed-off-by: Nikolas Skoufis --- .../src/service/DocsBuildStrategy.test.ts | 63 +++++++++++++++++++ .../src/service/DocsBuildStrategy.ts | 33 ++++++++++ .../src/service/router.test.ts | 30 +++++---- .../techdocs-backend/src/service/router.ts | 11 +++- 4 files changed, 124 insertions(+), 13 deletions(-) create mode 100644 plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts create mode 100644 plugins/techdocs-backend/src/service/DocsBuildStrategy.ts diff --git a/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts b/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts new file mode 100644 index 0000000000..71e5377a03 --- /dev/null +++ b/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { DefaultDocsBuildStrategy } from './DocsBuildStrategy'; +import { ConfigReader } from '@backstage/config'; + +const MockedConfigReader = ConfigReader as jest.MockedClass< + typeof ConfigReader +>; + +jest.mock('@backstage/config'); + +describe('DefaultDocsBuildStrategy', () => { + const entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + uid: '0', + name: 'test', + }, + }; + + const config = new ConfigReader({}); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('shouldBuild', () => { + it('should return true when techdocs.build is set to local', async () => { + const defaultDocsBuildStrategy = new DefaultDocsBuildStrategy(config); + + MockedConfigReader.prototype.getString.mockReturnValue('local'); + + const result = await defaultDocsBuildStrategy.shouldBuild(entity); + + expect(result).toBe(true); + }); + + it('should return false when techdocs.build is set to external', async () => { + const defaultDocsBuildStrategy = new DefaultDocsBuildStrategy(config); + + MockedConfigReader.prototype.getString.mockReturnValue('external'); + + const result = await defaultDocsBuildStrategy.shouldBuild(entity); + + expect(result).toBe(false); + }); + }); +}); diff --git a/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts b/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts new file mode 100644 index 0000000000..69b0474a85 --- /dev/null +++ b/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { Config } from '@backstage/config'; + +export interface DocsBuildStrategy { + shouldBuild(entity: Entity): Promise; +} + +export class DefaultDocsBuildStrategy { + private readonly config: Config; + + constructor(config: Config) { + this.config = config; + } + + async shouldBuild(_: Entity): Promise { + return this.config.getString('techdocs.builder') === 'local'; + } +} diff --git a/plugins/techdocs-backend/src/service/router.test.ts b/plugins/techdocs-backend/src/service/router.test.ts index 43e9641efb..521024252c 100644 --- a/plugins/techdocs-backend/src/service/router.test.ts +++ b/plugins/techdocs-backend/src/service/router.test.ts @@ -38,6 +38,7 @@ import { RouterOptions, } from './router'; import { TechDocsCache } from '../cache'; +import { DocsBuildStrategy } from './DocsBuildStrategy'; jest.mock('@backstage/catalog-client'); jest.mock('@backstage/config'); @@ -120,6 +121,9 @@ describe('createRouter', () => { const cache: jest.Mocked = { getClient: jest.fn(), }; + const docsBuildStrategy: jest.Mocked = { + shouldBuild: jest.fn(), + }; const outOfTheBoxOptions = { preparers, generators, @@ -128,6 +132,7 @@ describe('createRouter', () => { logger: getVoidLogger(), discovery, cache, + docsBuildStrategy, }; const recommendedOptions = { publisher, @@ -135,6 +140,7 @@ describe('createRouter', () => { logger: getVoidLogger(), discovery, cache, + docsBuildStrategy, }; beforeEach(() => { @@ -181,10 +187,10 @@ describe('createRouter', () => { expect(response.status).toBe(404); }); - it('should not check for an update without local builder', async () => { + it('should not check for an update when shouldBuild returns false', async () => { const app = await createApp(outOfTheBoxOptions); - MockedConfigReader.prototype.getString.mockReturnValue('external'); + docsBuildStrategy.shouldBuild.mockResolvedValue(false); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); MockDocsSynchronizer.prototype.doCacheSync.mockImplementation( async ({ responseHandler }) => @@ -198,10 +204,10 @@ describe('createRouter', () => { expect(response.status).toBe(304); }); - it('should error if missing builder', async () => { + it('should error if build is required and is missing preparer', async () => { const app = await createApp(recommendedOptions); - MockedConfigReader.prototype.getString.mockReturnValue('local'); + docsBuildStrategy.shouldBuild.mockResolvedValue(true); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); const response = await request(app) @@ -219,7 +225,7 @@ describe('createRouter', () => { it('should execute synchronization', async () => { const app = await createApp(outOfTheBoxOptions); - MockedConfigReader.prototype.getString.mockReturnValue('local'); + docsBuildStrategy.shouldBuild.mockResolvedValue(true); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); MockDocsSynchronizer.prototype.doSync.mockImplementation( async ({ responseHandler }) => @@ -244,7 +250,7 @@ describe('createRouter', () => { it('should return on updated', async () => { const app = await createApp(outOfTheBoxOptions); - MockedConfigReader.prototype.getString.mockReturnValue('local'); + docsBuildStrategy.shouldBuild.mockResolvedValue(true); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); MockDocsSynchronizer.prototype.doSync.mockImplementation( async ({ responseHandler }) => { @@ -297,10 +303,10 @@ describe('createRouter', () => { expect(response.status).toBe(404); }); - it('should not check for an update without local builder', async () => { + it('should not check for an update when shouldBuild returns false', async () => { const app = await createApp(outOfTheBoxOptions); - MockedConfigReader.prototype.getString.mockReturnValue('external'); + docsBuildStrategy.shouldBuild.mockResolvedValue(false); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); MockDocsSynchronizer.prototype.doCacheSync.mockImplementation( async ({ responseHandler }) => @@ -322,10 +328,10 @@ data: {"updated":false} ); }); - it('should error if missing builder', async () => { + it('should error if build is required and is missing preparer', async () => { const app = await createApp(recommendedOptions); - MockedConfigReader.prototype.getString.mockReturnValue('local'); + docsBuildStrategy.shouldBuild.mockResolvedValue(true); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); const response = await request(app) @@ -348,7 +354,7 @@ data: "Invalid configuration. 'techdocs.builder' was set to 'local' but no 'prep it('should execute synchronization', async () => { const app = await createApp(outOfTheBoxOptions); - MockedConfigReader.prototype.getString.mockReturnValue('local'); + docsBuildStrategy.shouldBuild.mockResolvedValue(true); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); MockDocsSynchronizer.prototype.doSync.mockImplementation( async ({ responseHandler }) => @@ -376,7 +382,7 @@ data: "Invalid configuration. 'techdocs.builder' was set to 'local' but no 'prep it('should return an event-stream', async () => { const app = await createApp(outOfTheBoxOptions); - MockedConfigReader.prototype.getString.mockReturnValue('local'); + docsBuildStrategy.shouldBuild.mockResolvedValue(true); MockCachedEntityLoader.prototype.load.mockResolvedValue(entity); MockDocsSynchronizer.prototype.doSync.mockImplementation( async ({ responseHandler }) => { diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index 141ce93f79..96916bdc9b 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -35,6 +35,10 @@ import { ScmIntegrations } from '@backstage/integration'; import { DocsSynchronizer, DocsSynchronizerSyncOpts } from './DocsSynchronizer'; import { createCacheMiddleware, TechDocsCache } from '../cache'; import { CachedEntityLoader } from './CachedEntityLoader'; +import { + DefaultDocsBuildStrategy, + DocsBuildStrategy, +} from './DocsBuildStrategy'; /** * All of the required dependencies for running TechDocs in the "out-of-the-box" @@ -49,6 +53,7 @@ export type OutOfTheBoxDeploymentOptions = { database?: Knex; // TODO: Make database required when we're implementing database stuff. config: Config; cache: PluginCacheManager; + docsBuildStrategy?: DocsBuildStrategy; }; /** @@ -61,6 +66,7 @@ export type RecommendedDeploymentOptions = { discovery: PluginEndpointDiscovery; config: Config; cache: PluginCacheManager; + docsBuildStrategy?: DocsBuildStrategy; }; /** @@ -86,6 +92,8 @@ export async function createRouter( const router = Router(); const { publisher, config, logger, discovery } = options; const catalogClient = new CatalogClient({ discoveryApi: discovery }); + const docsBuildStrategy = + options.docsBuildStrategy ?? new DefaultDocsBuildStrategy(config); // Entities are cached to optimize the /static/docs request path, which can be called many times // when loading a single techdocs page. @@ -200,7 +208,8 @@ export async function createRouter( // techdocs-backend will only try to build documentation for an entity if techdocs.builder is set to 'local' // If set to 'external', it will assume that an external process (e.g. CI/CD pipeline // of the repository) is responsible for building and publishing documentation to the storage provider - if (config.getString('techdocs.builder') !== 'local') { + const shouldBuild = await docsBuildStrategy.shouldBuild(entity); + if (!shouldBuild) { // However, if caching is enabled, take the opportunity to check and // invalidate stale cache entries. if (cache) { From 0d1c461780fad011bf48d46420f12b2be473e65f Mon Sep 17 00:00:00 2001 From: su-gupta Date: Tue, 22 Feb 2022 18:11:14 -0500 Subject: [PATCH 003/353] adding Confluence search functionality upstream v1 Signed-off-by: su-gupta --- contrib/search/README.md | 3 + contrib/search/confluence/ConfluenceCollator | 87 +++++++++++++++++++ .../confluence/ConfluenceResultListItem | 50 +++++++++++ contrib/search/confluence/README.md | 6 ++ 4 files changed, 146 insertions(+) create mode 100644 contrib/search/README.md create mode 100644 contrib/search/confluence/ConfluenceCollator create mode 100644 contrib/search/confluence/ConfluenceResultListItem create mode 100644 contrib/search/confluence/README.md diff --git a/contrib/search/README.md b/contrib/search/README.md new file mode 100644 index 0000000000..9a43a05c4c --- /dev/null +++ b/contrib/search/README.md @@ -0,0 +1,3 @@ +# Search + +Contributions/extensions to the Search plugin diff --git a/contrib/search/confluence/ConfluenceCollator b/contrib/search/confluence/ConfluenceCollator new file mode 100644 index 0000000000..5d6668b953 --- /dev/null +++ b/contrib/search/confluence/ConfluenceCollator @@ -0,0 +1,87 @@ +import { DocumentCollator } from '@backstage/search-common'; +import fetch from 'cross-fetch' + +export class ConfluenceCollator implements DocumentCollator { + public readonly type: string = 'confluence'; + + async execute() { + + const ConfluenceUrlBase = 'https://{CONFLUENCE-ORG-NAME}.atlassian.net/wiki/rest/api' + + async function getConfluenceData(requestUrl: string) { + var emptyJson = {} + try { + const res = await fetch(requestUrl, { + method: 'get', + headers: { + 'Authorization': `Basic ${process.env.CONFLUENCE_TOKEN}` + }, + }); + if (res.ok) { + return await res.json(); + } + } catch (err) { + console.error(err); + } + return emptyJson + } + + async function getSpaces(): Promise { + const data = await getConfluenceData(`${ConfluenceUrlBase}/space?&limit=1000&type=global&status=current`); + let spacesList = [] + if (data["results"]) { + const results = data["results"]; + for (const result of results) { + spacesList.push(result["key"]) + } + } + return spacesList + } + + async function getDocumentsFromSpaces(spaces: string[]): Promise { + let documentsList = [] + for (var space of spaces) { + let next = true + let requestUrl = `${ConfluenceUrlBase}/content?limit=1000&status=current&spaceKey=${space}` + while (next) { + const data = await getConfluenceData(requestUrl) + if (data["results"]) { + const results = data["results"] + for (const result of results) { + documentsList.push(result["_links"]["self"]) + } + if (data["_links"]["next"]) { + requestUrl = data["_links"]["base"] + data["_links"]["next"] + } else { + next = false + } + } else { + break + } + } + } + return documentsList + } + + async function getDocumentInfo(documents: string[]) { + let documentInfo = [] + for (var documentUrl of documents) { + const data = await getConfluenceData(documentUrl + '?expand=body.storage') + if (data["status"] && data["status"]=="current") { + const documentMetaData = { + title: data["title"], + text: data["body"]["storage"]["value"], + location: data["_links"]["base"] + data["_links"]["webui"], + } + documentInfo.push(documentMetaData) + } + } + return documentInfo + } + + const spacesList = await getSpaces(); + const documentsList = await getDocumentsFromSpaces(spacesList); + const documentMetaDataList = await getDocumentInfo(documentsList); + return documentMetaDataList + } +} diff --git a/contrib/search/confluence/ConfluenceResultListItem b/contrib/search/confluence/ConfluenceResultListItem new file mode 100644 index 0000000000..cd6d13bdea --- /dev/null +++ b/contrib/search/confluence/ConfluenceResultListItem @@ -0,0 +1,50 @@ +import React from 'react'; +import { Link } from '@backstage/core-components'; +import { IndexableDocument } from '@backstage/search-common'; +import { + Divider, + ListItem, + ListItemIcon, + ListItemText, +} from '@material-ui/core'; + +type Props = { + result: IndexableDocument; +}; + +export const ConfluenceResultListItem = ({ result }: Props) => { + // Remove html tags from document text before displaying + const chars = []; + let isTag = false; + for (const c of result.text.substring(0, 500)) { + if (c === "<") { + isTag = true; + continue; + } + if (c === ">") { + isTag = false; + chars.push(" ") + continue; + } + if (!isTag) { + chars.push(c); + } + } + const excerpt = chars.join("").substring(0, 80) + (result.text.length > 80 ? "..." : ""); + + return ( + + + + + + + + + + ); +}; diff --git a/contrib/search/confluence/README.md b/contrib/search/confluence/README.md new file mode 100644 index 0000000000..5ebd275560 --- /dev/null +++ b/contrib/search/confluence/README.md @@ -0,0 +1,6 @@ +# Confluence + +These files help you add Confluence as a source to the Backstage Search plugin. +To do so, add both files in this directory under the packages/backend/src/plugins/search/ pathway in your Backstage app as TypeScript files. +Then, update your packages/app/src/components/search/SearchPage.tsx and packages/backend/src/plugins/search.ts +to include the new Search source. From 0ce1d75871954ef8d59e5e489cc1e478d9ace0b7 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 25 Feb 2022 09:26:56 +0100 Subject: [PATCH 004/353] core-plugin-api: Remove generic auth APIs Signed-off-by: Johan Haals --- packages/app-defaults/src/defaults/apis.ts | 66 ------------- packages/app/src/identityProviders.ts | 21 ---- packages/core-app-api/api-report.md | 7 -- .../implementations/auth/auth0/Auth0Auth.ts | 67 ------------- .../apis/implementations/auth/auth0/index.ts | 17 ---- .../src/apis/implementations/auth/index.ts | 1 - .../src/layout/SignInPage/auth0Provider.tsx | 95 ------------------- packages/core-plugin-api/api-report.md | 28 ------ .../src/apis/definitions/auth.ts | 61 ------------ .../test-utils/src/testUtils/defaultApis.ts | 66 ------------- .../AuthProviders/DefaultProviderSettings.tsx | 18 ---- 11 files changed, 447 deletions(-) delete mode 100644 packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts delete mode 100644 packages/core-app-api/src/apis/implementations/auth/auth0/index.ts delete mode 100644 packages/core-components/src/layout/SignInPage/auth0Provider.tsx diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index f26c298fb1..3f5cfc1c58 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -21,16 +21,13 @@ import { ErrorAlerter, GoogleAuth, GithubAuth, - OAuth2, OktaAuth, GitlabAuth, - Auth0Auth, MicrosoftAuth, BitbucketAuth, OAuthRequestManager, WebStorage, UrlPatternDiscovery, - SamlAuth, OneLoginAuth, UnhandledErrorForwarder, AtlassianAuth, @@ -49,16 +46,12 @@ import { oauthRequestApiRef, googleAuthApiRef, githubAuthApiRef, - oauth2ApiRef, oktaAuthApiRef, gitlabAuthApiRef, - auth0AuthApiRef, microsoftAuthApiRef, storageApiRef, configApiRef, - samlAuthApiRef, oneloginAuthApiRef, - oidcAuthApiRef, bitbucketAuthApiRef, atlassianAuthApiRef, } from '@backstage/core-plugin-api'; @@ -197,46 +190,6 @@ export const apis = [ environment: configApi.getOptionalString('auth.environment'), }), }), - createApiFactory({ - api: auth0AuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - Auth0Auth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: oauth2ApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: samlAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, configApi }) => - SamlAuth.create({ - discoveryApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), createApiFactory({ api: oneloginAuthApiRef, deps: { @@ -251,25 +204,6 @@ export const apis = [ environment: configApi.getOptionalString('auth.environment'), }), }), - createApiFactory({ - api: oidcAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider: { - id: 'oidc', - title: 'Your Identity Provider', - icon: () => null, - }, - environment: configApi.getOptionalString('auth.environment'), - }), - }), createApiFactory({ api: bitbucketAuthApiRef, deps: { diff --git a/packages/app/src/identityProviders.ts b/packages/app/src/identityProviders.ts index 49204d2b5e..5f8c0faf47 100644 --- a/packages/app/src/identityProviders.ts +++ b/packages/app/src/identityProviders.ts @@ -19,27 +19,12 @@ import { gitlabAuthApiRef, oktaAuthApiRef, githubAuthApiRef, - samlAuthApiRef, microsoftAuthApiRef, oneloginAuthApiRef, - oauth2ApiRef, - oidcAuthApiRef, bitbucketAuthApiRef, } from '@backstage/core-plugin-api'; export const providers = [ - { - id: 'oidc-auth-provider', - title: 'Oidc', - message: 'Sign In using OpenId Connect', - apiRef: oidcAuthApiRef, - }, - { - id: 'oauth2-auth-provider', - title: 'OAuth 2.0', - message: 'Sign In using OAuth 2.0', - apiRef: oauth2ApiRef, - }, { id: 'google-auth-provider', title: 'Google', @@ -70,12 +55,6 @@ export const providers = [ message: 'Sign In using Okta', apiRef: oktaAuthApiRef, }, - { - id: 'saml-auth-provider', - title: 'SAML', - message: 'Sign In using SAML', - apiRef: samlAuthApiRef, - }, { id: 'onelogin-auth-provider', title: 'OneLogin', diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 3b53813615..17a9592523 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -16,7 +16,6 @@ import { AppConfig } from '@backstage/config'; import { AppTheme } from '@backstage/core-plugin-api'; import { AppThemeApi } from '@backstage/core-plugin-api'; import { atlassianAuthApiRef } from '@backstage/core-plugin-api'; -import { auth0AuthApiRef } from '@backstage/core-plugin-api'; import { AuthProviderInfo } from '@backstage/core-plugin-api'; import { AuthRequestOptions } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; @@ -246,12 +245,6 @@ export class AtlassianAuth { static create(options: OAuthApiCreateOptions): typeof atlassianAuthApiRef.T; } -// @public @deprecated -export class Auth0Auth { - // (undocumented) - static create(options: OAuthApiCreateOptions): typeof auth0AuthApiRef.T; -} - // @public export type AuthApiCreateOptions = { discoveryApi: DiscoveryApi; diff --git a/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts b/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts deleted file mode 100644 index d8f942b94b..0000000000 --- a/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { auth0AuthApiRef } from '@backstage/core-plugin-api'; -import { OAuth2 } from '../oauth2'; -import { OAuthApiCreateOptions } from '../types'; - -const DEFAULT_PROVIDER = { - id: 'auth0', - title: 'Auth0', - icon: () => null, -}; - -/** - * Implements the OAuth flow to Auth0 products. - * - * @public - * @deprecated Use {@link OAuth2} instead - * - * @example - * - * ```ts - * OAuth2.create({ - * discoveryApi, - * oauthRequestApi, - * provider: { - * id: 'auth0', - * title: 'Auth0', - * icon: () => null, - * }, - * defaultScopes: ['openid', 'email', 'profile'], - * environment: configApi.getOptionalString('auth.environment'), - * }) - * ``` - */ -export default class Auth0Auth { - static create(options: OAuthApiCreateOptions): typeof auth0AuthApiRef.T { - const { - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = ['openid', `email`, `profile`], - } = options; - - return OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider, - environment, - defaultScopes, - }); - } -} diff --git a/packages/core-app-api/src/apis/implementations/auth/auth0/index.ts b/packages/core-app-api/src/apis/implementations/auth/auth0/index.ts deleted file mode 100644 index 9daed7d13e..0000000000 --- a/packages/core-app-api/src/apis/implementations/auth/auth0/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 Auth0Auth } from './Auth0Auth'; diff --git a/packages/core-app-api/src/apis/implementations/auth/index.ts b/packages/core-app-api/src/apis/implementations/auth/index.ts index 50333f07a0..c4cf520db5 100644 --- a/packages/core-app-api/src/apis/implementations/auth/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/index.ts @@ -20,7 +20,6 @@ export * from './google'; export * from './oauth2'; export * from './okta'; export * from './saml'; -export * from './auth0'; export * from './microsoft'; export * from './onelogin'; export * from './bitbucket'; diff --git a/packages/core-components/src/layout/SignInPage/auth0Provider.tsx b/packages/core-components/src/layout/SignInPage/auth0Provider.tsx deleted file mode 100644 index 739c3709a9..0000000000 --- a/packages/core-components/src/layout/SignInPage/auth0Provider.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 Grid from '@material-ui/core/Grid'; -import Typography from '@material-ui/core/Typography'; -import Button from '@material-ui/core/Button'; -import { InfoCard } from '../InfoCard/InfoCard'; -import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; -import { - useApi, - auth0AuthApiRef, - errorApiRef, -} from '@backstage/core-plugin-api'; -import { ForwardedError } from '@backstage/errors'; -import { UserIdentity } from './UserIdentity'; - -const Component: ProviderComponent = ({ onSignInSuccess }) => { - const auth0AuthApi = useApi(auth0AuthApiRef); - const errorApi = useApi(errorApiRef); - - const handleLogin = async () => { - try { - const identityResponse = await auth0AuthApi.getBackstageIdentity({ - instantPopup: true, - }); - if (!identityResponse) { - throw new Error( - 'The Auth0 provider is not configured to support sign-in', - ); - } - - const profile = await auth0AuthApi.getProfile(); - - onSignInSuccess( - UserIdentity.create({ - identity: identityResponse.identity, - authApi: auth0AuthApi, - profile, - }), - ); - } catch (error) { - errorApi.post(new ForwardedError('Auth0 login failed', error)); - } - }; - - return ( - - - Sign In - - } - > - Sign In using Auth0 - - - ); -}; - -const loader: ProviderLoader = async apis => { - const auth0AuthApi = apis.get(auth0AuthApiRef)!; - - const identityResponse = await auth0AuthApi.getBackstageIdentity({ - optional: true, - }); - - if (!identityResponse) { - return undefined; - } - - const profile = await auth0AuthApi.getProfile(); - return UserIdentity.create({ - identity: identityResponse.identity, - authApi: auth0AuthApi, - profile, - }); -}; - -export const auth0Provider: SignInProvider = { Component, loader }; diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 1647d1c2cb..2b0e061cce 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -184,11 +184,6 @@ export function attachComponentData

( data: unknown, ): void; -// @public @deprecated -export const auth0AuthApiRef: ApiRef< - OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi ->; - // @public export type AuthProviderInfo = { id: string; @@ -531,15 +526,6 @@ export const microsoftAuthApiRef: ApiRef< SessionApi >; -// @public @deprecated -export const oauth2ApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi ->; - // @public export type OAuthApi = { getAccessToken( @@ -575,15 +561,6 @@ export type OAuthRequesterOptions = { // @public export type OAuthScope = string | string[]; -// @public @deprecated -export const oidcAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi ->; - // @alpha export const oktaAuthApiRef: ApiRef< OAuthApi & @@ -684,11 +661,6 @@ export type RouteRef = { params: ParamKeys; }; -// @public @deprecated -export const samlAuthApiRef: ApiRef< - ProfileInfoApi & BackstageIdentityApi & SessionApi ->; - // @public export type SessionApi = { signIn(): Promise; diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index ed5a7a3264..497ce8233b 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -366,23 +366,6 @@ export const gitlabAuthApiRef: ApiRef< id: 'core.auth.gitlab', }); -/** - * Provides authentication towards Auth0 APIs. - * - * @remarks - * - * See {@link https://auth0.com/docs/scopes/current/oidc-scopes} - * for a full list of supported scopes. - * - * @public - * @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs - */ -export const auth0AuthApiRef: ApiRef< - OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'core.auth.auth0', -}); - /** * Provides authentication towards Microsoft APIs and identities. * @@ -404,50 +387,6 @@ export const microsoftAuthApiRef: ApiRef< id: 'core.auth.microsoft', }); -/** - * Provides authentication for custom identity providers. - * - * @public - * @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs - */ -export const oauth2ApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.oauth2', -}); - -/** - * Provides authentication for custom OpenID Connect identity providers. - * - * @public - * @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs - */ -export const oidcAuthApiRef: ApiRef< - OAuthApi & - OpenIdConnectApi & - ProfileInfoApi & - BackstageIdentityApi & - SessionApi -> = createApiRef({ - id: 'core.auth.oidc', -}); - -/** - * Provides authentication for SAML-based identity providers. - * - * @public - * @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs - */ -export const samlAuthApiRef: ApiRef< - ProfileInfoApi & BackstageIdentityApi & SessionApi -> = createApiRef({ - id: 'core.auth.saml', -}); - /** * Provides authentication towards OneLogin APIs. * diff --git a/packages/test-utils/src/testUtils/defaultApis.ts b/packages/test-utils/src/testUtils/defaultApis.ts index f06e1ba6b7..1ff10bfe6f 100644 --- a/packages/test-utils/src/testUtils/defaultApis.ts +++ b/packages/test-utils/src/testUtils/defaultApis.ts @@ -21,16 +21,13 @@ import { ErrorAlerter, GoogleAuth, GithubAuth, - OAuth2, OktaAuth, GitlabAuth, - Auth0Auth, MicrosoftAuth, BitbucketAuth, OAuthRequestManager, WebStorage, UrlPatternDiscovery, - SamlAuth, OneLoginAuth, UnhandledErrorForwarder, AtlassianAuth, @@ -45,16 +42,12 @@ import { oauthRequestApiRef, googleAuthApiRef, githubAuthApiRef, - oauth2ApiRef, oktaAuthApiRef, gitlabAuthApiRef, - auth0AuthApiRef, microsoftAuthApiRef, storageApiRef, configApiRef, - samlAuthApiRef, oneloginAuthApiRef, - oidcAuthApiRef, bitbucketAuthApiRef, atlassianAuthApiRef, } from '@backstage/core-plugin-api'; @@ -158,46 +151,6 @@ export const defaultApis = [ environment: configApi.getOptionalString('auth.environment'), }), }), - createApiFactory({ - api: auth0AuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - Auth0Auth.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: oauth2ApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), - createApiFactory({ - api: samlAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, configApi }) => - SamlAuth.create({ - discoveryApi, - environment: configApi.getOptionalString('auth.environment'), - }), - }), createApiFactory({ api: oneloginAuthApiRef, deps: { @@ -212,25 +165,6 @@ export const defaultApis = [ environment: configApi.getOptionalString('auth.environment'), }), }), - createApiFactory({ - api: oidcAuthApiRef, - deps: { - discoveryApi: discoveryApiRef, - oauthRequestApi: oauthRequestApiRef, - configApi: configApiRef, - }, - factory: ({ discoveryApi, oauthRequestApi, configApi }) => - OAuth2.create({ - discoveryApi, - oauthRequestApi, - provider: { - id: 'oidc', - title: 'Your Identity Provider', - icon: () => null, - }, - environment: configApi.getOptionalString('auth.environment'), - }), - }), createApiFactory({ api: bitbucketAuthApiRef, deps: { diff --git a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx index 8cfb3a7257..7025167658 100644 --- a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx +++ b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx @@ -17,11 +17,9 @@ import Star from '@material-ui/icons/Star'; import React from 'react'; import { ProviderSettingsItem } from './ProviderSettingsItem'; import { - auth0AuthApiRef, githubAuthApiRef, gitlabAuthApiRef, googleAuthApiRef, - oauth2ApiRef, oktaAuthApiRef, microsoftAuthApiRef, bitbucketAuthApiRef, @@ -66,14 +64,6 @@ export const DefaultProviderSettings = ({ configuredProviders }: Props) => ( icon={Star} /> )} - {configuredProviders.includes('auth0') && ( - - )} {configuredProviders.includes('okta') && ( ( icon={Star} /> )} - {configuredProviders.includes('oauth2') && ( - - )} ); From af5eaa87f43fa63c503fb64fe07b9ceacce47048 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 25 Feb 2022 09:43:51 +0100 Subject: [PATCH 005/353] add changeset Signed-off-by: Johan Haals --- .changeset/rare-insects-punch.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/rare-insects-punch.md diff --git a/.changeset/rare-insects-punch.md b/.changeset/rare-insects-punch.md new file mode 100644 index 0000000000..d22b851abd --- /dev/null +++ b/.changeset/rare-insects-punch.md @@ -0,0 +1,10 @@ +--- +'@backstage/app-defaults': minor +'@backstage/core-app-api': minor +'@backstage/core-components': minor +'@backstage/core-plugin-api': minor +'@backstage/test-utils': minor +'@backstage/plugin-user-settings': minor +--- + +**BREAKING**: Removed deprecated `auth0AuthApiRef`, `oauth2ApiRef`, `samlAuthApiRef` and `oidcAuthApiRef` as these APIs are too generic to be useful. Instructions for how to migrate can be found at [https://backstage.io/docs/api/deprecations#generic-auth-api-refs](https://backstage.io/docs/api/deprecations#generic-auth-api-refs). From 34af86517c8cc8caf0cae77692021ae867417e11 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Thu, 24 Feb 2022 12:45:04 +0100 Subject: [PATCH 006/353] feat(bitbucket): ensure apiBaseUrl, replace hardcoded cases Ensure presence of apiBaseUrl for bitbucket integrations for both cases Bitbucket Cloud and Bitbucket Server by setting the default for Bitbucket Server at the integration config, too. Replace hardcoded uses of the default apiBaseUrl with the use of the integration config's value. Signed-off-by: Patrick Jungermann --- .changeset/honest-students-clean.md | 7 +++++++ .../src/reading/BitbucketUrlReader.test.ts | 16 ---------------- .../src/reading/BitbucketUrlReader.ts | 9 ++------- packages/integration/api-report.md | 2 +- packages/integration/src/bitbucket/config.ts | 10 +++++----- packages/integration/src/helpers.test.ts | 10 ++++++++-- .../actions/builtin/publish/bitbucket.ts | 18 ++++++++++-------- 7 files changed, 33 insertions(+), 39 deletions(-) create mode 100644 .changeset/honest-students-clean.md diff --git a/.changeset/honest-students-clean.md b/.changeset/honest-students-clean.md new file mode 100644 index 0000000000..ca8db28343 --- /dev/null +++ b/.changeset/honest-students-clean.md @@ -0,0 +1,7 @@ +--- +'@backstage/integration': minor +'@backstage/backend-common': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +ensure `apiBaseUrl` being set for Bitbucket integrations, replace hardcoded defaults diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index ea8e4626be..371e11b364 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -270,22 +270,6 @@ describe('BitbucketUrlReader', () => { expect(response.etag).toBe('12ab34cd56ef'); }); - - it('should throw error when apiBaseUrl is missing', () => { - expect(() => { - /* eslint-disable no-new */ - new BitbucketUrlReader( - new BitbucketIntegration( - readBitbucketIntegrationConfig( - new ConfigReader({ - host: 'bitbucket.mycompany.net', - }), - ), - ), - { treeResponseFactory }, - ); - }).toThrowError('must configure an explicit apiBaseUrl'); - }); }); describe('search hosted', () => { diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index 2006637545..e22137ea69 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -62,14 +62,9 @@ export class BitbucketUrlReader implements UrlReader { private readonly integration: BitbucketIntegration, private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }, ) { - const { host, apiBaseUrl, token, username, appPassword } = - integration.config; + const { host, token, username, appPassword } = integration.config; - if (!apiBaseUrl) { - throw new Error( - `Bitbucket integration for '${host}' must configure an explicit apiBaseUrl`, - ); - } else if (!token && username && !appPassword) { + if (!token && username && !appPassword) { throw new Error( `Bitbucket integration for '${host}' has configured a username but is missing a required appPassword.`, ); diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 10da05784c..9a6ba03f8e 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -88,7 +88,7 @@ export class BitbucketIntegration implements ScmIntegration { // @public export type BitbucketIntegrationConfig = { host: string; - apiBaseUrl?: string; + apiBaseUrl: string; token?: string; username?: string; appPassword?: string; diff --git a/packages/integration/src/bitbucket/config.ts b/packages/integration/src/bitbucket/config.ts index 1cd911aad3..44a2f0cc1f 100644 --- a/packages/integration/src/bitbucket/config.ts +++ b/packages/integration/src/bitbucket/config.ts @@ -36,12 +36,10 @@ export type BitbucketIntegrationConfig = { * The base URL of the API of this provider, e.g. "https://api.bitbucket.org/2.0", * with no trailing slash. * - * May be omitted specifically for Bitbucket Cloud; then it will be deduced. - * - * The API will always be preferred if both its base URL and a token are - * present. + * Values omitted at the optional property at the app-config will be deduced + * from the "host" value. */ - apiBaseUrl?: string; + apiBaseUrl: string; /** * The authorization token to use for requests to a Bitbucket Server provider. @@ -90,6 +88,8 @@ export function readBitbucketIntegrationConfig( apiBaseUrl = trimEnd(apiBaseUrl, '/'); } else if (host === BITBUCKET_HOST) { apiBaseUrl = BITBUCKET_API_BASE_URL; + } else { + apiBaseUrl = `https://${host}/rest/api/1.0`; } return { diff --git a/packages/integration/src/helpers.test.ts b/packages/integration/src/helpers.test.ts index a69c7faec9..60a2789f05 100644 --- a/packages/integration/src/helpers.test.ts +++ b/packages/integration/src/helpers.test.ts @@ -24,7 +24,10 @@ import { describe('basicIntegrations', () => { describe('byUrl', () => { it('handles hosts without a port', () => { - const integration = new BitbucketIntegration({ host: 'host.com' }); + const integration = new BitbucketIntegration({ + host: 'host.com', + apiBaseUrl: 'a', + }); const integrations = basicIntegrations( [integration], i => i.config.host, @@ -33,7 +36,10 @@ describe('basicIntegrations', () => { expect(integrations.byUrl('https://host.com:8080/a')).toBeUndefined(); }); it('handles hosts with a port', () => { - const integration = new BitbucketIntegration({ host: 'host.com:8080' }); + const integration = new BitbucketIntegration({ + host: 'host.com:8080', + apiBaseUrl: 'a', + }); const integrations = basicIntegrations( [integration], i => i.config.host, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts index 47ebce0550..494fca1af9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts @@ -32,6 +32,7 @@ const createBitbucketCloudRepository = async (opts: { description?: string; repoVisibility: 'private' | 'public'; authorization: string; + apiBaseUrl: string; }) => { const { workspace, @@ -40,6 +41,7 @@ const createBitbucketCloudRepository = async (opts: { description, repoVisibility, authorization, + apiBaseUrl, } = opts; const options: RequestInit = { @@ -59,7 +61,7 @@ const createBitbucketCloudRepository = async (opts: { let response: Response; try { response = await fetch( - `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo}`, + `${apiBaseUrl}/repositories/${workspace}/${repo}`, options, ); } catch (e) { @@ -88,16 +90,14 @@ const createBitbucketCloudRepository = async (opts: { }; const createBitbucketServerRepository = async (opts: { - host: string; project: string; repo: string; description?: string; repoVisibility: 'private' | 'public'; authorization: string; - apiBaseUrl?: string; + apiBaseUrl: string; }) => { const { - host, project, repo, description, @@ -121,8 +121,7 @@ const createBitbucketServerRepository = async (opts: { }; try { - const baseUrl = apiBaseUrl ? apiBaseUrl : `https://${host}/rest/api/1.0`; - response = await fetch(`${baseUrl}/projects/${project}/repos`, options); + response = await fetch(`${apiBaseUrl}/projects/${project}/repos`, options); } catch (e) { throw new Error(`Unable to create repository, ${e}`); } @@ -306,7 +305,11 @@ export function createPublishBitbucketAction(options: { const authorization = getAuthorizationHeader( ctx.input.token - ? { host: integrationConfig.config.host, token: ctx.input.token } + ? { + host: integrationConfig.config.host, + apiBaseUrl: integrationConfig.config.apiBaseUrl, + token: ctx.input.token, + } : integrationConfig.config, ); @@ -319,7 +322,6 @@ export function createPublishBitbucketAction(options: { const { remoteUrl, repoContentsUrl } = await createMethod({ authorization, - host, workspace: workspace || '', project, repo, From e26fd1c7abb95a86dafd4b3957478d51c70bd2df Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 25 Feb 2022 15:41:53 +0100 Subject: [PATCH 007/353] catalog-react: mark useEntityPermission as alpha Signed-off-by: Johan Haals --- .changeset/polite-poems-nail.md | 5 +++++ plugins/catalog-react/api-report.md | 2 +- plugins/catalog-react/src/hooks/useEntityPermission.ts | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/polite-poems-nail.md diff --git a/.changeset/polite-poems-nail.md b/.changeset/polite-poems-nail.md new file mode 100644 index 0000000000..c3eb97fce0 --- /dev/null +++ b/.changeset/polite-poems-nail.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +Marked `useEntityPermission` as alpha since the underlying permission framework is under active development. diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index f117e8cd5c..dfd5f7d02b 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -564,7 +564,7 @@ export function useEntityOwnership(): { isOwnedEntity: (entity: Entity | EntityName) => boolean; }; -// @public +// @alpha export function useEntityPermission(permission: Permission): { loading: boolean; allowed: boolean; diff --git a/plugins/catalog-react/src/hooks/useEntityPermission.ts b/plugins/catalog-react/src/hooks/useEntityPermission.ts index c1f62cfc39..092114936a 100644 --- a/plugins/catalog-react/src/hooks/useEntityPermission.ts +++ b/plugins/catalog-react/src/hooks/useEntityPermission.ts @@ -28,7 +28,7 @@ import { useEntity } from './useEntity'; * Note: this hook blocks the permission request until the entity has loaded in * context. If you have the entityRef and need concurrent requests, use the * `usePermission` hook directly. - * @public + * @alpha */ export function useEntityPermission(permission: Permission): { loading: boolean; From 4ed5ce50610812779c33f233eab63e7b19f5c5c3 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 25 Feb 2022 15:54:28 +0100 Subject: [PATCH 008/353] publish alpha types Signed-off-by: Johan Haals --- plugins/catalog-react/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index c7e255f973..b80d1f5789 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -9,7 +9,8 @@ "publishConfig": { "access": "public", "main": "dist/index.esm.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "backstage": { "role": "web-library" From 4bc61a64e279b4438092124d3f733c563d29e841 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Fri, 25 Feb 2022 17:12:57 +0100 Subject: [PATCH 009/353] fix(msgraph): add missing docs for config options Adds documentation for the recently introduced config options - userGroupMemberSearch - groupSearch Closes: #9819 Signed-off-by: Patrick Jungermann --- .changeset/quick-mugs-pay.md | 5 +++++ plugins/catalog-backend-module-msgraph/README.md | 9 +++++++++ 2 files changed, 14 insertions(+) create mode 100644 .changeset/quick-mugs-pay.md diff --git a/.changeset/quick-mugs-pay.md b/.changeset/quick-mugs-pay.md new file mode 100644 index 0000000000..f2d3a00eb8 --- /dev/null +++ b/.changeset/quick-mugs-pay.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +add documentation for config options `userGroupMemberSearch` and `groupSearch` diff --git a/plugins/catalog-backend-module-msgraph/README.md b/plugins/catalog-backend-module-msgraph/README.md index 0e088d3a4e..9f879895d9 100644 --- a/plugins/catalog-backend-module-msgraph/README.md +++ b/plugins/catalog-backend-module-msgraph/README.md @@ -41,11 +41,20 @@ catalog: # This and userGroupMemberFilter are mutually exclusive, only one can be specified userFilter: accountEnabled eq true and userType eq 'member' # Optional filter for users, use group membership to get users. + # (Filtered groups and fetch their members.) # This and userFilter are mutually exclusive, only one can be specified + # See https://docs.microsoft.com/en-us/graph/search-query-parameter userGroupMemberFilter: "displayName eq 'Backstage Users'" + # Optional search for users, use group membership to get users. + # (Search for groups and fetch their members.) + # This and userFilter are mutually exclusive, only one can be specified + userGroupMemberSearch: '"description:One" AND ("displayName:Video" OR "displayName:Drive")' # Optional filter for group, see Microsoft Graph API for the syntax # See https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0#properties groupFilter: securityEnabled eq false and mailEnabled eq true and groupTypes/any(c:c+eq+'Unified') + # Optional search for groups, see Microsoft Graph API for the syntax + # See https://docs.microsoft.com/en-us/graph/search-query-parameter + groupSearch: '"description:One" AND ("displayName:Video" OR "displayName:Drive")' ``` `userFilter` and `userGroupMemberFilter` are mutually exclusive, only one can be provided. If both are provided, an error will be thrown. From f9bb6aa0aa59730001913fcf38059086938138b7 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Fri, 25 Feb 2022 20:33:07 +0100 Subject: [PATCH 010/353] feat(msgraph): add `userExpand` config option Previously, the `userExpand` option was added to the config schema, but not read or passed. This change will fully implement `userExpand` and additionally. making it work with `userGroupMember[...]` options. Relates-to: issue #9819 Relates-to: PR #9824 Relates-to: PR #9721 Signed-off-by: Patrick Jungermann --- .changeset/tidy-jokes-dream.md | 5 +++ .../catalog-backend-module-msgraph/README.md | 6 ++++ .../api-report.md | 11 ++++--- .../src/microsoftGraph/client.test.ts | 2 +- .../src/microsoftGraph/client.ts | 12 ++++--- .../src/microsoftGraph/config.test.ts | 2 ++ .../src/microsoftGraph/config.ts | 5 ++- .../src/microsoftGraph/read.test.ts | 32 +++++++++++++++---- .../src/microsoftGraph/read.ts | 9 ++++-- 9 files changed, 65 insertions(+), 19 deletions(-) create mode 100644 .changeset/tidy-jokes-dream.md diff --git a/.changeset/tidy-jokes-dream.md b/.changeset/tidy-jokes-dream.md new file mode 100644 index 0000000000..aa9c84fc4b --- /dev/null +++ b/.changeset/tidy-jokes-dream.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +add `userExpand` config option to allow expanding a single relationship diff --git a/plugins/catalog-backend-module-msgraph/README.md b/plugins/catalog-backend-module-msgraph/README.md index 0e088d3a4e..5608231405 100644 --- a/plugins/catalog-backend-module-msgraph/README.md +++ b/plugins/catalog-backend-module-msgraph/README.md @@ -35,6 +35,12 @@ catalog: # the App registration in the Microsoft Azure Portal. clientId: ${MICROSOFT_GRAPH_CLIENT_ID} clientSecret: ${MICROSOFT_GRAPH_CLIENT_SECRET_TOKEN} + # Optional parameter to include the expanded resource or collection referenced + # by a single relationship (navigation property) in your results. + # Only one relationship can be expanded in a single request. + # See https://docs.microsoft.com/en-us/graph/query-parameters#expand-parameter + # Can be combined with userGroupMember[...] instead of userFilter. + userExpand: manager # Optional filter for user, see Microsoft Graph API for the syntax # See https://docs.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0#properties # and for the syntax https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index 7ad603e6ca..574b948dea 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -78,7 +78,10 @@ export class MicrosoftGraphClient { userId: string, maxSize: number, ): Promise; - getUserProfile(userId: string): Promise; + getUserProfile( + userId: string, + query?: ODataQuery, + ): Promise; getUsers(query?: ODataQuery): AsyncIterable; requestApi( path: string, @@ -158,7 +161,7 @@ export type MicrosoftGraphProviderConfig = { clientId: string; clientSecret: string; userFilter?: string; - userExpand?: string[]; + userExpand?: string; userGroupMemberFilter?: string; userGroupMemberSearch?: string; groupFilter?: string; @@ -172,7 +175,7 @@ export function normalizeEntityName(name: string): string; export type ODataQuery = { search?: string; filter?: string; - expand?: string[]; + expand?: string; select?: string[]; }; @@ -191,7 +194,7 @@ export function readMicrosoftGraphOrg( client: MicrosoftGraphClient, tenantId: string, options: { - userExpand?: string[]; + userExpand?: string; userFilter?: string; userGroupMemberSearch?: string; userGroupMemberFilter?: string; diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts index 6413a97c15..94a126e1a7 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts @@ -85,7 +85,7 @@ describe('MicrosoftGraphClient', () => { const response = await client.requestApi('users', { filter: 'test eq true', - expand: ['children'], + expand: 'children', select: ['id', 'children'], }); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts index db749a536e..cba5d9f640 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts @@ -38,7 +38,7 @@ export type ODataQuery = { /** * specifies the related resources or media streams to be included in line with retrieved resources */ - expand?: string[]; + expand?: string; /** * request a specific set of properties for each entity or complex type */ @@ -155,7 +155,7 @@ export class MicrosoftGraphClient { $search: query?.search, $filter: query?.filter, $select: query?.select?.join(','), - $expand: query?.expand?.join(','), + $expand: query?.expand, }, { addQueryPrefix: true, @@ -203,10 +203,14 @@ export class MicrosoftGraphClient { * * @public * @param userId - The unique identifier for the `User` resource + * @param query - OData Query {@link ODataQuery} * */ - async getUserProfile(userId: string): Promise { - const response = await this.requestApi(`users/${userId}`); + async getUserProfile( + userId: string, + query?: ODataQuery, + ): Promise { + const response = await this.requestApi(`users/${userId}`, query); if (response.status !== 200) { await this.handleError('user profile', response); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts index efbbca1f5c..700e5cd8fe 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts @@ -53,6 +53,7 @@ describe('readMicrosoftGraphConfig', () => { clientId: 'clientId', clientSecret: 'clientSecret', authority: 'https://login.example.com/', + userExpand: 'manager', userFilter: 'accountEnabled eq true', groupFilter: 'securityEnabled eq false', }, @@ -66,6 +67,7 @@ describe('readMicrosoftGraphConfig', () => { clientId: 'clientId', clientSecret: 'clientSecret', authority: 'https://login.example.com', + userExpand: 'manager', userFilter: 'accountEnabled eq true', groupFilter: 'securityEnabled eq false', }, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts index 6809144f80..c2789d767a 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts @@ -57,7 +57,7 @@ export type MicrosoftGraphProviderConfig = { * * E.g. "manager" */ - userExpand?: string[]; + userExpand?: string; /** * The filter to apply to extract users by groups memberships. * @@ -106,6 +106,8 @@ export function readMicrosoftGraphConfig( const tenantId = providerConfig.getString('tenantId'); const clientId = providerConfig.getString('clientId'); const clientSecret = providerConfig.getString('clientSecret'); + + const userExpand = providerConfig.getOptionalString('userExpand'); const userFilter = providerConfig.getOptionalString('userFilter'); const userGroupMemberFilter = providerConfig.getOptionalString( 'userGroupMemberFilter', @@ -133,6 +135,7 @@ export function readMicrosoftGraphConfig( tenantId, clientId, clientSecret, + userExpand, userFilter, userGroupMemberFilter, userGroupMemberSearch, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts index b0273e1f58..dba3741c73 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts @@ -118,7 +118,7 @@ describe('read microsoft graph', () => { expect(client.getUserPhotoWithSizeLimit).toBeCalledWith('userid', 120); }); - it('should read users with custom transformer', async () => { + it('should read users with userExpand and custom transformer', async () => { async function* getExampleUsers() { yield { id: 'userid', @@ -133,6 +133,7 @@ describe('read microsoft graph', () => { ); const { users } = await readMicrosoftGraphUsers(client, { + userExpand: 'manager', userFilter: 'accountEnabled eq true', transformer: async () => ({ apiVersion: 'backstage.io/v1alpha1', @@ -154,6 +155,7 @@ describe('read microsoft graph', () => { expect(client.getUsers).toBeCalledTimes(1); expect(client.getUsers).toBeCalledWith({ + expand: 'manager', filter: 'accountEnabled eq true', }); expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1); @@ -227,12 +229,14 @@ describe('read microsoft graph', () => { expect(client.getGroupMembers).toBeCalledWith('groupid'); expect(client.getUserProfile).toBeCalledTimes(1); - expect(client.getUserProfile).toBeCalledWith('userid'); + expect(client.getUserProfile).toBeCalledWith('userid', { + expand: undefined, + }); expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toBeCalledWith('userid', 120); }); - it('should read users with custom transformer', async () => { + it('should read users with userExpand and custom transformer', async () => { async function* getExampleGroups() { yield { id: 'groupid', @@ -266,6 +270,7 @@ describe('read microsoft graph', () => { ); const { users } = await readMicrosoftGraphUsersInGroups(client, { + userExpand: 'manager', userGroupMemberFilter: 'securityEnabled eq true', transformer: async () => ({ apiVersion: 'backstage.io/v1alpha1', @@ -293,7 +298,9 @@ describe('read microsoft graph', () => { expect(client.getGroupMembers).toBeCalledWith('groupid'); expect(client.getUserProfile).toBeCalledTimes(1); - expect(client.getUserProfile).toBeCalledWith('userid'); + expect(client.getUserProfile).toBeCalledWith('userid', { + expand: 'manager', + }); expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toBeCalledWith('userid', 120); }); @@ -634,6 +641,14 @@ describe('read microsoft graph', () => { }; } + async function getExampleUserProfile(userId: string) { + return { + id: userId, + displayName: 'User Name', + mail: 'user.name@example.com', + }; + } + async function* getExampleGroups() { yield { id: 'groupid', @@ -686,7 +701,7 @@ describe('read microsoft graph', () => { }); }); - it('should read users using userFilter', async () => { + it('should read users using userExpand and userFilter', async () => { client.getOrganization.mockResolvedValue({ id: 'tenantid', displayName: 'Organization Name', @@ -705,12 +720,14 @@ describe('read microsoft graph', () => { await readMicrosoftGraphOrg(client, 'tenantid', { logger: getVoidLogger(), + userExpand: 'manager', userFilter: 'accountEnabled eq true', groupFilter: 'securityEnabled eq false', }); expect(client.getUsers).toBeCalledTimes(1); expect(client.getUsers).toBeCalledWith({ + expand: 'manager', filter: 'accountEnabled eq true', }); expect(client.getGroups).toBeCalledTimes(1); @@ -719,13 +736,14 @@ describe('read microsoft graph', () => { }); }); - it('should read users using userGroupMemberFilter', async () => { + it('should read users using userExpand and userGroupMemberFilter', async () => { client.getOrganization.mockResolvedValue({ id: 'tenantid', displayName: 'Organization Name', }); client.getUsers.mockImplementation(getExampleUsers); + client.getUserProfile.mockImplementation(getExampleUserProfile); client.getUserPhotoWithSizeLimit.mockResolvedValue( 'data:image/jpeg;base64,...', ); @@ -750,6 +768,8 @@ describe('read microsoft graph', () => { expect(client.getGroups).toBeCalledWith({ filter: 'securityEnabled eq false', }); + expect(client.getUserProfile).toBeCalledTimes(1); + expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1); }); }); }); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index f862fd5ace..333eaa6b0a 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -85,7 +85,7 @@ export async function readMicrosoftGraphUsers( client: MicrosoftGraphClient, options: { userFilter?: string; - userExpand?: string[]; + userExpand?: string; transformer?: UserTransformer; logger: Logger; }, @@ -137,6 +137,7 @@ export async function readMicrosoftGraphUsers( export async function readMicrosoftGraphUsersInGroups( client: MicrosoftGraphClient, options: { + userExpand?: string; userGroupMemberSearch?: string; userGroupMemberFilter?: string; transformer?: UserTransformer; @@ -186,7 +187,9 @@ export async function readMicrosoftGraphUsersInGroups( let user; let userPhoto; try { - user = await client.getUserProfile(userId); + user = await client.getUserProfile(userId, { + expand: options.userExpand, + }); } catch (e) { options.logger.warn(`Unable to load user for ${userId}`); } @@ -506,7 +509,7 @@ export async function readMicrosoftGraphOrg( client: MicrosoftGraphClient, tenantId: string, options: { - userExpand?: string[]; + userExpand?: string; userFilter?: string; userGroupMemberSearch?: string; userGroupMemberFilter?: string; From d741c97b9892ad829c7e557a90375eb1a37fe333 Mon Sep 17 00:00:00 2001 From: Andy Tan Date: Fri, 25 Feb 2022 15:52:04 -0800 Subject: [PATCH 011/353] Render markdown for description in software templates Signed-off-by: Andy Tan --- .changeset/mean-panthers-wonder.md | 5 +++++ .../src/components/TemplateCard/TemplateCard.tsx | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .changeset/mean-panthers-wonder.md diff --git a/.changeset/mean-panthers-wonder.md b/.changeset/mean-panthers-wonder.md new file mode 100644 index 0000000000..f055c4a064 --- /dev/null +++ b/.changeset/mean-panthers-wonder.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Render markdown for description in software templates diff --git a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx index 3495409dd6..aa895a35fa 100644 --- a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx @@ -45,7 +45,11 @@ import { generatePath } from 'react-router'; import { rootRouteRef } from '../../routes'; import { FavouriteTemplate } from '../FavouriteTemplate/FavouriteTemplate'; -import { Button, ItemCardHeader } from '@backstage/core-components'; +import { + Button, + ItemCardHeader, + MarkdownContent, +} from '@backstage/core-components'; import { useApi, useRouteRef } from '@backstage/core-plugin-api'; const useStyles = makeStyles(theme => ({ @@ -175,7 +179,7 @@ export const TemplateCard = ({ template, deprecated }: TemplateCardProps) => { Description - {templateProps.description} + From 766f969de973a8dbba12e62a964234c0ff596c07 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 26 Feb 2022 15:40:34 +0100 Subject: [PATCH 012/353] scripts/prepare-release: update to detect patch versions from patch branches Signed-off-by: Patrik Oldsberg --- docs/publishing.md | 9 -- scripts/prepare-release.js | 174 +++++++++++++++++++++++++++++-------- 2 files changed, 137 insertions(+), 46 deletions(-) diff --git a/docs/publishing.md b/docs/publishing.md index fbbef93493..a0d67b7d24 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -67,12 +67,3 @@ process is used to release an emergency fix as version `6.5.1` in the patch rele - [ ] The fix, which you can likely cherry-pick from your patch branch: `git cherry-pick origin/patch/v1.18.0^` - [ ] An updated `CHANGELOG.md` of all patched packages from the tip of the patch branch, `git checkout origin/patch/v1.18.0 -- {packages,plugins}/*/CHANGELOG.md`. - [ ] A changeset with the message "Applied the fix from version `6.5.1` of this package, which is part of the `v1.18.1` release of Backstage." - - [ ] An entry in `.changeset/patched.json` that sets the current release version to `6.5.1`: - - ```json - { - "currentReleaseVersion": { - "@backstage/plugin-foo": "6.5.1" - } - } - ``` diff --git a/scripts/prepare-release.js b/scripts/prepare-release.js index be82e19344..5cc888fd66 100755 --- a/scripts/prepare-release.js +++ b/scripts/prepare-release.js @@ -28,6 +28,10 @@ const execFile = promisify(execFileCb); // All of these are considered to be main-line release branches const MAIN_BRANCHES = ['master', 'origin/master', 'changeset-release/master']; +// This prefix is used for patch branches, followed by the release version WITH a 'v' prefix +// For example, `patch/v1.2.0` +const PATCH_BRANCH_PREFIX = 'patch/'; + const DEPENDENCY_TYPES = [ 'dependencies', 'devDependencies', @@ -35,25 +39,84 @@ const DEPENDENCY_TYPES = [ 'peerDependencies', ]; +/** + * Returns the most recent release version on the main branch that is not a pre-release. + */ +async function getPreviousReleaseVersion(repo) { + // TODO(Rugvip): Figure out which field to sort by to avoid manual sort after + const { stdout: tagsStr } = await execFile( + 'git', + ['tag', '--list', 'v*', '--merged=HEAD'], + { shell: true, cwd: repo.root.dir }, + ); + const tags = tagsStr.trim().split(/\r\n|\n/); + const [latestTag] = semver.rsort(tags).filter(t => !semver.prerelease(t)); + return latestTag; +} + +/** + * Finds the tip of the patch branch of a given release version. + * Returns undefined if no patch branch exists. + */ +async function findTipOfPatchBranch(repo, release) { + try { + await execFile('git', ['fetch', 'origin', PATCH_BRANCH_PREFIX + release], { + shell: true, + cwd: repo.root.dir, + }); + } catch (error) { + if (error.stderr?.match(/fatal: couldn't find remote ref/i)) { + return undefined; + } + throw error; + } + const { stdout: refStr } = await execFile('git', ['rev-parse', 'FETCH_HEAD']); + return refStr.trim(); +} + +/** + * Returns a map of packages to their versions for any package version + * in that does not match the current version in the working directory. + */ +async function detectPatchVersionsForRef(repo, ref) { + const patchVersions = new Map(); + + for (const pkg of repo.packages) { + const pkgJsonPath = path.join( + path.relative(repo.root.dir, pkg.dir), + 'package.json', + ); + const { stdout: pkgJsonStr } = await execFile('git', [ + 'show', + `${ref}:${pkgJsonPath}`, + ]); + if (pkgJsonStr) { + const releasePkgJson = JSON.parse(pkgJsonStr); + const pkgJson = pkg.packageJson; + if (releasePkgJson.name !== pkgJson.name) { + throw new Error( + `Mismatched package name at ${pkg.dir}, ${releasePkgJson.name} !== ${pkgJson.name}`, + ); + } + if (releasePkgJson.version !== pkgJson.version) { + patchVersions.set(pkgJson.name, releasePkgJson.version); + } + } + } + + return patchVersions; +} + /** * Bumps up the versions of packages to account for * the base versions that are set in .changeset/patched.json. * This may be needed when we have made emergency releases. */ -async function updatePatchVersions() { - const patchedJsonPath = path.resolve('.changeset', 'patched.json'); - const { currentReleaseVersion } = await fs.readJson(patchedJsonPath); - if (Object.keys(currentReleaseVersion).length === 0) { - console.log('No currentReleaseVersion overrides found, skipping.'); - return; - } - - const { packages } = await getPackages(path.resolve('.')); - +async function applyPatchVersions(repo, patchVersions) { const pendingVersionBumps = new Map(); - for (const [name, version] of Object.entries(currentReleaseVersion)) { - const pkg = packages.find(p => p.packageJson.name === name); + for (const [name, version] of patchVersions) { + const pkg = repo.packages.find(p => p.packageJson.name === name); if (!pkg) { throw new Error(`Package ${name} not found`); } @@ -81,7 +144,7 @@ async function updatePatchVersions() { }); } - for (const { dir, packageJson } of packages) { + for (const { dir, packageJson } of [repo.root, ...repo.packages]) { let hasChanges = false; if (pendingVersionBumps.has(packageJson.name)) { @@ -117,20 +180,44 @@ async function updatePatchVersions() { }); } } +} - await fs.writeJSON( - patchedJsonPath, - { currentReleaseVersion: {} }, - { spaces: 2, encoding: 'utf8' }, - ); +/** + * Detects any patched packages version since the most recent release on + * the main branch, and then bumps all packages in the repo accordingly. + */ +async function updatePackageVersions(repo) { + const previousRelease = await getPreviousReleaseVersion(repo); + console.log(`Found release version: ${previousRelease}`); + + const patchRef = await findTipOfPatchBranch(repo, previousRelease); + if (patchRef) { + console.log(`Tip of the patch branch: ${patchRef}`); + + const patchVersions = await detectPatchVersionsForRef(repo, patchRef); + if (patchVersions.size > 0) { + console.log( + `Found ${patchVersions.size} packages that were patched since the last release`, + ); + for (const [name, version] of patchVersions) { + console.log(` ${name}: ${version}`); + } + + await applyPatchVersions(repo, patchVersions); + } else { + console.log('No packages were patched since the last release'); + } + } else { + console.log('No patch branch found'); + } } /** * Returns the mode and tag that is currently set * in the .changeset/pre.json file */ -async function getPreInfo(rootPath) { - const pre = path.join(rootPath, '.changeset', 'pre.json'); +async function getPreInfo(repo) { + const pre = path.join(repo.root.dir, '.changeset', 'pre.json'); if (!(await fs.pathExists(pre))) { return { mode: undefined, tag: undefined }; } @@ -139,26 +226,30 @@ async function getPreInfo(rootPath) { return { mode, tag }; } +/** + * Returns the name of the current git branch + */ +async function getCurrentBranch(repo) { + const { stdout } = await execFile( + 'git', + ['rev-parse', '--abbrev-ref', 'HEAD'], + { cwd: repo.root.dir, shell: true }, + ); + return stdout.trim(); +} + /** * Bumps the release version in the root package.json. * * This takes into account whether we're in pre-release mode or on a patch branch. */ -async function updateBackstageReleaseVersion() { - const rootPath = path.resolve(__dirname, '..'); - const branchName = await execFile( - 'git', - ['rev-parse', '--abbrev-ref', 'HEAD'], - { shell: true }, - ).then(({ stdout }) => stdout.trim()); - const { mode: preMode, tag: preTag } = await getPreInfo(rootPath); +async function updateBackstageReleaseVersion(repo, type) { + const { mode: preMode, tag: preTag } = await getPreInfo(repo); - const packagePath = path.join(rootPath, 'package.json'); - const package = await fs.readJson(packagePath); - const { version: currentVersion } = package; + const { version: currentVersion } = repo.root.packageJson; let nextVersion; - if (MAIN_BRANCHES.includes(branchName)) { + if (type === 'minor') { if (preMode === 'pre') { if (semver.prerelease(currentVersion)) { nextVersion = semver.inc(currentVersion, 'pre', preTag); @@ -170,7 +261,7 @@ async function updateBackstageReleaseVersion() { } else { nextVersion = semver.inc(currentVersion, 'minor'); } - } else { + } else if (type === 'patch') { if (preMode) { throw new Error(`Unexpected pre mode ${preMode} on branch ${branchName}`); } @@ -178,9 +269,9 @@ async function updateBackstageReleaseVersion() { } await fs.writeJson( - packagePath, + path.join(repo.root.dir, 'package.json'), { - ...package, + ...repo.root.packageJson, version: nextVersion, }, { spaces: 2, encoding: 'utf8' }, @@ -188,8 +279,17 @@ async function updateBackstageReleaseVersion() { } async function main() { - await updatePatchVersions(); - await updateBackstageReleaseVersion(); + const repo = await getPackages(__dirname); + const branchName = await getCurrentBranch(repo); + const isMainBranch = MAIN_BRANCHES.includes(branchName); + + console.log(`Current branch: ${branchName}`); + if (isMainBranch) { + console.log('Main release, updating package versions'); + await updatePackageVersions(repo); + } + + await updateBackstageReleaseVersion(repo, isMainBranch ? 'minor' : 'patch'); } main().catch(error => { From 23e1c17bbaccfdbcbfac39f1d17902ec536aaf68 Mon Sep 17 00:00:00 2001 From: Hasan Oezdemir <21654050+nodify-at@users.noreply.github.com> Date: Sun, 27 Feb 2022 01:58:56 +0100 Subject: [PATCH 013/353] (feature): enable permissions in jenkins plugin. Actions (re-build) can be executed only by a user who owns the entity (or related group own the entity) Signed-off-by: Hasan Oezdemir <21654050+nodify-at@users.noreply.github.com> --- .changeset/gold-garlics-sing.md | 5 ++ .changeset/orange-cobras-shave.md | 6 +++ .changeset/ten-fireants-march.md | 5 ++ plugins/jenkins-backend/api-report.md | 46 +++++++++++++++++++ plugins/jenkins-backend/package.json | 3 ++ plugins/jenkins-backend/src/index.ts | 1 + .../src/permissions/conditionExports.ts | 27 +++++++++++ .../jenkins-backend/src/permissions/index.ts | 20 ++++++++ .../permissions/permission-router-factory.ts | 40 ++++++++++++++++ .../src/permissions/rules/index.ts | 30 ++++++++++++ plugins/jenkins-backend/src/service/router.ts | 23 +++++++++- plugins/jenkins-common/.eslintrc.js | 3 ++ plugins/jenkins-common/README.md | 3 ++ plugins/jenkins-common/api-report.md | 15 ++++++ plugins/jenkins-common/package.json | 33 +++++++++++++ plugins/jenkins-common/src/index.ts | 16 +++++++ plugins/jenkins-common/src/permissions.ts | 34 ++++++++++++++ plugins/jenkins/package.json | 1 + .../BuildsPage/lib/CITable/CITable.tsx | 8 +++- yarn.lock | 1 + 20 files changed, 317 insertions(+), 3 deletions(-) create mode 100644 .changeset/gold-garlics-sing.md create mode 100644 .changeset/orange-cobras-shave.md create mode 100644 .changeset/ten-fireants-march.md create mode 100644 plugins/jenkins-backend/src/permissions/conditionExports.ts create mode 100644 plugins/jenkins-backend/src/permissions/index.ts create mode 100644 plugins/jenkins-backend/src/permissions/permission-router-factory.ts create mode 100644 plugins/jenkins-backend/src/permissions/rules/index.ts create mode 100644 plugins/jenkins-common/.eslintrc.js create mode 100644 plugins/jenkins-common/README.md create mode 100644 plugins/jenkins-common/api-report.md create mode 100644 plugins/jenkins-common/package.json create mode 100644 plugins/jenkins-common/src/index.ts create mode 100644 plugins/jenkins-common/src/permissions.ts diff --git a/.changeset/gold-garlics-sing.md b/.changeset/gold-garlics-sing.md new file mode 100644 index 0000000000..f13229d003 --- /dev/null +++ b/.changeset/gold-garlics-sing.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-jenkins-common': major +--- + +Add a new common plugin for jenkins which provides shared isomorphic code for the jenkins plugin. diff --git a/.changeset/orange-cobras-shave.md b/.changeset/orange-cobras-shave.md new file mode 100644 index 0000000000..c753aecbc6 --- /dev/null +++ b/.changeset/orange-cobras-shave.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-jenkins-backend': minor +--- + +Add permissions support for jenkins backend and provide an integration router. You must pass a permission router +if you want to enable permissions in jenkins plugin. diff --git a/.changeset/ten-fireants-march.md b/.changeset/ten-fireants-march.md new file mode 100644 index 0000000000..9376292196 --- /dev/null +++ b/.changeset/ten-fireants-march.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-jenkins': minor +--- + +Integrated the permission plugin, if enabled, the actions can be executed only by owned user (or assigned to related group) diff --git a/plugins/jenkins-backend/api-report.md b/plugins/jenkins-backend/api-report.md index d437a5dd75..6f9ec10cf4 100644 --- a/plugins/jenkins-backend/api-report.md +++ b/plugins/jenkins-backend/api-report.md @@ -4,10 +4,25 @@ ```ts import { CatalogApi } from '@backstage/catalog-client'; +import { ConditionalPolicyDecision } from '@backstage/plugin-permission-node'; +import { Conditions } from '@backstage/plugin-permission-node'; import { Config } from '@backstage/config'; +import { EntitiesSearchFilter } from '@backstage/plugin-catalog-backend'; +import { Entity } from '@backstage/catalog-model'; import { EntityName } from '@backstage/catalog-model'; import express from 'express'; import { Logger as Logger_2 } from 'winston'; +import { PermissionCondition } from '@backstage/plugin-permission-common'; +import { PermissionCriteria } from '@backstage/plugin-permission-common'; +import { PermissionRule } from '@backstage/plugin-permission-node'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; + +// Warning: (ae-missing-release-tag) "createJenkinsPermissionPolicy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const createJenkinsPermissionPolicy: ( + conditions: PermissionCriteria>, +) => ConditionalPolicyDecision; // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -34,6 +49,17 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { static readonly OLD_JENKINS_ANNOTATION = 'jenkins.io/github-folder'; } +// Warning: (ae-missing-release-tag) "jenkinsConditions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const jenkinsConditions: Conditions<{ + isEntityOwner: PermissionRule< + Entity, + EntitiesSearchFilter, + [claims: string[]] + >; +}>; + // Warning: (ae-missing-release-tag) "JenkinsConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -86,10 +112,30 @@ export interface JenkinsInstanceConfig { username: string; } +// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// +// @public +export const jenkinsPermissionRules: { + isEntityOwner: PermissionRule< + Entity, + EntitiesSearchFilter, + [claims: string[]] + >; +}; + // Warning: (ae-missing-release-tag) "RouterOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export interface RouterOptions { + // (undocumented) + config?: Config; + // (undocumented) + discovery?: PluginEndpointDiscovery; + // (undocumented) + fetchApi?: { + fetch: typeof fetch; + }; // (undocumented) jenkinsInfoProvider: JenkinsInfoProvider; // (undocumented) diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index 66cbe5ba24..72ebc1ee2b 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -29,6 +29,9 @@ "@backstage/catalog-client": "^0.7.2", "@backstage/catalog-model": "^0.11.0", "@backstage/config": "^0.1.15", + "@backstage/plugin-catalog-backend": "^0.22.0", + "@backstage/plugin-jenkins-common": "^0.1.0", + "@backstage/plugin-permission-node": "^0.5.2", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", diff --git a/plugins/jenkins-backend/src/index.ts b/plugins/jenkins-backend/src/index.ts index c55335c52d..c584842e07 100644 --- a/plugins/jenkins-backend/src/index.ts +++ b/plugins/jenkins-backend/src/index.ts @@ -21,3 +21,4 @@ */ export * from './service'; +export * from './permissions'; diff --git a/plugins/jenkins-backend/src/permissions/conditionExports.ts b/plugins/jenkins-backend/src/permissions/conditionExports.ts new file mode 100644 index 0000000000..5266b1b091 --- /dev/null +++ b/plugins/jenkins-backend/src/permissions/conditionExports.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { jenkinsPermissionRules } from './rules'; +import { createConditionExports } from '@backstage/plugin-permission-node'; +import { RESOURCE_TYPE_JENKINS } from '@backstage/plugin-jenkins-common'; + +const { conditions, createPolicyDecision } = createConditionExports({ + pluginId: 'jenkins', + resourceType: RESOURCE_TYPE_JENKINS, + rules: jenkinsPermissionRules, +}); + +export const jenkinsConditions = conditions; +export const createJenkinsPermissionPolicy = createPolicyDecision; diff --git a/plugins/jenkins-backend/src/permissions/index.ts b/plugins/jenkins-backend/src/permissions/index.ts new file mode 100644 index 0000000000..522f40aeee --- /dev/null +++ b/plugins/jenkins-backend/src/permissions/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { + createJenkinsPermissionPolicy, + jenkinsConditions, +} from './conditionExports'; +export * from './rules'; diff --git a/plugins/jenkins-backend/src/permissions/permission-router-factory.ts b/plugins/jenkins-backend/src/permissions/permission-router-factory.ts new file mode 100644 index 0000000000..1959eb24a7 --- /dev/null +++ b/plugins/jenkins-backend/src/permissions/permission-router-factory.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; +import { jenkinsPermissionRules } from './rules'; +import { parseEntityRef } from '@backstage/catalog-model'; +import type { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { CatalogClient } from '@backstage/catalog-client'; +import { RESOURCE_TYPE_JENKINS } from '@backstage/plugin-jenkins-common'; + +export const jenkinsPermissionIntegrationRouterFactory = ( + discoveryApi: PluginEndpointDiscovery, + fetchApi?: { fetch: typeof fetch }, +) => { + const catalogApi = new CatalogClient({ discoveryApi, fetchApi: fetchApi }); + return createPermissionIntegrationRouter({ + resourceType: RESOURCE_TYPE_JENKINS, + rules: Object.values(jenkinsPermissionRules), + getResources: resourceRefs => { + const entities = resourceRefs.map(async resourceRef => { + const { kind, namespace, name } = parseEntityRef(resourceRef); + return catalogApi.getEntityByName({ kind, name, namespace }); + }); + // combine the promises to return entities as any array, getResources expects a single promise with all entities + return Promise.all(entities); + }, + }); +}; diff --git a/plugins/jenkins-backend/src/permissions/rules/index.ts b/plugins/jenkins-backend/src/permissions/rules/index.ts new file mode 100644 index 0000000000..0cd07b26a8 --- /dev/null +++ b/plugins/jenkins-backend/src/permissions/rules/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { permissionRules } from '@backstage/plugin-catalog-backend'; +/** + * + * Jenkins' permission rules can be used to defined different kind of rules to check if the user authorizes an action + * (or listing the jobs ever) + * + * Provided rules: + * {isEntityOwner} can be used to determine if a user or the group of the user + * owns an entity. + * + * @public + */ +export const jenkinsPermissionRules = { + isEntityOwner: permissionRules.isEntityOwner, +}; diff --git a/plugins/jenkins-backend/src/service/router.ts b/plugins/jenkins-backend/src/service/router.ts index 8c5852efa9..85320bb520 100644 --- a/plugins/jenkins-backend/src/service/router.ts +++ b/plugins/jenkins-backend/src/service/router.ts @@ -14,16 +14,24 @@ * limitations under the License. */ -import { errorHandler } from '@backstage/backend-common'; +import { + errorHandler, + PluginEndpointDiscovery, +} from '@backstage/backend-common'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import { JenkinsInfoProvider } from './jenkinsInfoProvider'; import { JenkinsApiImpl } from './jenkinsApi'; +import { Config } from '@backstage/config'; +import { jenkinsPermissionIntegrationRouterFactory } from '../permissions/permission-router-factory'; export interface RouterOptions { logger: Logger; jenkinsInfoProvider: JenkinsInfoProvider; + config?: Config; + discovery?: PluginEndpointDiscovery; + fetchApi?: { fetch: typeof fetch }; } export async function createRouter( @@ -117,7 +125,18 @@ export async function createRouter( response.json({}); }, ); - router.use(errorHandler()); + + if (options.config?.getOptionalBoolean('permission.enabled')) { + if (!options.discovery) { + throw new Error('Discovery API is required if permissions are enabled.'); + } + router.use( + jenkinsPermissionIntegrationRouterFactory( + options.discovery, + options.fetchApi, + ), + ); + } return router; } diff --git a/plugins/jenkins-common/.eslintrc.js b/plugins/jenkins-common/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/jenkins-common/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/jenkins-common/README.md b/plugins/jenkins-common/README.md new file mode 100644 index 0000000000..71e6db3a16 --- /dev/null +++ b/plugins/jenkins-common/README.md @@ -0,0 +1,3 @@ +# Jenkins Common + +Shared isomorphic code for the jenkins plugin. diff --git a/plugins/jenkins-common/api-report.md b/plugins/jenkins-common/api-report.md new file mode 100644 index 0000000000..b63cc94374 --- /dev/null +++ b/plugins/jenkins-common/api-report.md @@ -0,0 +1,15 @@ +## API Report File for "@backstage/plugin-jenkins-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Permission } from '@backstage/plugin-permission-common'; + +// @public +export const jenkinsExecutePermission: Permission; + +// @public (undocumented) +export const RESOURCE_TYPE_JENKINS = 'jenkins'; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/jenkins-common/package.json b/plugins/jenkins-common/package.json new file mode 100644 index 0000000000..5739794197 --- /dev/null +++ b/plugins/jenkins-common/package.json @@ -0,0 +1,33 @@ +{ + "name": "@backstage/plugin-jenkins-common", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "common-library" + }, + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" + }, + "dependencies": { + "@backstage/plugin-permission-common": "^0.5.1" + }, + "devDependencies": { + "@backstage/cli": "^0.14.1" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/jenkins-common/src/index.ts b/plugins/jenkins-common/src/index.ts new file mode 100644 index 0000000000..aff3b136bd --- /dev/null +++ b/plugins/jenkins-common/src/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 './permissions'; diff --git a/plugins/jenkins-common/src/permissions.ts b/plugins/jenkins-common/src/permissions.ts new file mode 100644 index 0000000000..f521e0454b --- /dev/null +++ b/plugins/jenkins-common/src/permissions.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { Permission } from '@backstage/plugin-permission-common'; + +/** + * @public + */ +export const RESOURCE_TYPE_JENKINS = 'jenkins'; + +/** + * This permission is used to determine if a user is allowed to execute an action in jenkins plugin + * + * @public + */ +export const jenkinsExecutePermission: Permission = { + name: 'jenkins.execute', + attributes: { + action: 'update', + }, + resourceType: RESOURCE_TYPE_JENKINS, +}; diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index ada061dba9..e9ea4e5b5c 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -40,6 +40,7 @@ "@backstage/core-plugin-api": "^0.7.0", "@backstage/errors": "^0.2.2", "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-jenkins-common": "^0.1.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", diff --git a/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx b/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx index 2c464491c2..c525e297be 100644 --- a/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx @@ -15,6 +15,7 @@ */ import { Link, Progress, Table, TableColumn } from '@backstage/core-components'; import { alertApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { useEntityPermission } from '@backstage/plugin-catalog-react'; import { Box, IconButton, Tooltip, Typography } from '@material-ui/core'; import RetryIcon from '@material-ui/icons/Replay'; import { default as React, useState } from 'react'; @@ -23,6 +24,7 @@ import JenkinsLogo from '../../../../assets/JenkinsLogo.svg'; import { buildRouteRef } from '../../../../plugin'; import { useBuilds } from '../../../useBuilds'; import { JenkinsRunStatus } from '../Status'; +import { jenkinsExecutePermission } from '@backstage/plugin-jenkins-common'; const FailCount = ({ count }: { count: number }): JSX.Element | null => { if (count !== 0) { @@ -174,6 +176,10 @@ const generatedColumns: TableColumn[] = [ render: (row: Partial) => { const ActionWrapper = () => { const [isLoadingRebuild, setIsLoadingRebuild] = useState(false); + const { allowed, loading } = useEntityPermission( + jenkinsExecutePermission, + ); + const alertApi = useApi(alertApiRef); const onRebuild = async () => { @@ -201,7 +207,7 @@ const generatedColumns: TableColumn[] = [ <> {isLoadingRebuild && } {!isLoadingRebuild && ( - + )} diff --git a/yarn.lock b/yarn.lock index 6cb525a71e..e0757c196f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11929,6 +11929,7 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: "@backstage/plugin-graphiql" "^0.2.32" "@backstage/plugin-home" "^0.4.16" "@backstage/plugin-jenkins" "^0.6.0" + "@backstage/plugin-jenkins-common" "^0.1.0" "@backstage/plugin-kafka" "^0.3.0" "@backstage/plugin-kubernetes" "^0.6.0" "@backstage/plugin-lighthouse" "^0.3.0" From ed2259afcbc94dfbc100a8aa7825e4cd24d76705 Mon Sep 17 00:00:00 2001 From: Hasan Oezdemir <21654050+nodify-at@users.noreply.github.com> Date: Sun, 27 Feb 2022 02:05:50 +0100 Subject: [PATCH 014/353] (feature): fixed typo. reported by Vale Signed-off-by: Hasan Oezdemir <21654050+nodify-at@users.noreply.github.com> --- .changeset/gold-garlics-sing.md | 2 +- .changeset/orange-cobras-shave.md | 4 ++-- plugins/jenkins-common/README.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/gold-garlics-sing.md b/.changeset/gold-garlics-sing.md index f13229d003..012d6f92fb 100644 --- a/.changeset/gold-garlics-sing.md +++ b/.changeset/gold-garlics-sing.md @@ -2,4 +2,4 @@ '@backstage/plugin-jenkins-common': major --- -Add a new common plugin for jenkins which provides shared isomorphic code for the jenkins plugin. +Add a new common plugin for Jenkins which provides shared isomorphic code for the Jenkins plugin. diff --git a/.changeset/orange-cobras-shave.md b/.changeset/orange-cobras-shave.md index c753aecbc6..2b840dab48 100644 --- a/.changeset/orange-cobras-shave.md +++ b/.changeset/orange-cobras-shave.md @@ -2,5 +2,5 @@ '@backstage/plugin-jenkins-backend': minor --- -Add permissions support for jenkins backend and provide an integration router. You must pass a permission router -if you want to enable permissions in jenkins plugin. +Add permissions support for Jenkins backend and provide an integration router. You must pass a permission router +if you want to enable permissions in Jenkins plugin. diff --git a/plugins/jenkins-common/README.md b/plugins/jenkins-common/README.md index 71e6db3a16..54a79fc287 100644 --- a/plugins/jenkins-common/README.md +++ b/plugins/jenkins-common/README.md @@ -1,3 +1,3 @@ # Jenkins Common -Shared isomorphic code for the jenkins plugin. +Shared isomorphic code for the Jenkins plugin. From 6537a601c7de369c8aa9a3601b8fe780db46aa2b Mon Sep 17 00:00:00 2001 From: Nikolas Skoufis Date: Sun, 27 Feb 2022 18:36:35 +1100 Subject: [PATCH 015/353] Add changeset for these changes Signed-off-by: Nikolas Skoufis --- .changeset/dull-months-knock.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dull-months-knock.md diff --git a/.changeset/dull-months-knock.md b/.changeset/dull-months-knock.md new file mode 100644 index 0000000000..3b957518c2 --- /dev/null +++ b/.changeset/dull-months-knock.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Added a new interface that allows for customization of when to build techdocs From f7e6a2fbc7cd1a7c24016b205a77117e77000b42 Mon Sep 17 00:00:00 2001 From: Nikolas Skoufis Date: Sun, 27 Feb 2022 18:49:59 +1100 Subject: [PATCH 016/353] Add updated api report Signed-off-by: Nikolas Skoufis --- plugins/techdocs-backend/api-report.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/techdocs-backend/api-report.md b/plugins/techdocs-backend/api-report.md index 5d106cfafb..28700b245b 100644 --- a/plugins/techdocs-backend/api-report.md +++ b/plugins/techdocs-backend/api-report.md @@ -6,6 +6,7 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { DocumentCollator } from '@backstage/search-common'; +import { Entity } from '@backstage/catalog-model'; import express from 'express'; import { GeneratorBuilder } from '@backstage/techdocs-common'; import { Knex } from 'knex'; @@ -51,6 +52,7 @@ export type OutOfTheBoxDeploymentOptions = { database?: Knex; config: Config; cache: PluginCacheManager; + docsBuildStrategy?: DocsBuildStrategy; }; // @public @@ -60,6 +62,7 @@ export type RecommendedDeploymentOptions = { discovery: PluginEndpointDiscovery; config: Config; cache: PluginCacheManager; + docsBuildStrategy?: DocsBuildStrategy; }; // @public @@ -81,4 +84,8 @@ export type TechDocsCollatorOptions = { export { TechDocsDocument }; export * from '@backstage/techdocs-common'; + +// Warnings were encountered during analysis: +// +// src/service/router.d.ts:24:5 - (ae-forgotten-export) The symbol "DocsBuildStrategy" needs to be exported by the entry point index.d.ts ``` From 0bedff8c9abecdc13730fe54b816e9cc6674306a Mon Sep 17 00:00:00 2001 From: Nikolas Skoufis Date: Sun, 27 Feb 2022 19:11:09 +1100 Subject: [PATCH 017/353] Add docs for build strategy stuff Signed-off-by: Nikolas Skoufis --- docs/features/techdocs/concepts.md | 18 +++++++++ docs/features/techdocs/configuration.md | 15 +++++--- docs/features/techdocs/how-to-guides.md | 50 +++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/docs/features/techdocs/concepts.md b/docs/features/techdocs/concepts.md index 408f092ebd..f8e813d89e 100644 --- a/docs/features/techdocs/concepts.md +++ b/docs/features/techdocs/concepts.md @@ -46,6 +46,24 @@ between `techdocs-backend` and the storage) [TechDocs Backend](https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend) +## TechDocs Build Strategy + +To accommodate more complex logic surrounding whether or not to build TechDocs, the TechDocs backend +supports selecting a Build Strategy. +The Build Strategy is responsible for deciding whether the documentation requested should be built locally +by the TechDocs backend or not. +Customization of the Build Strategy allows for more complex behaviour regarding whether the TechDocs backend +is responsible for building TechDocs, whether an external process is responsible, or whether a combination +of local builds and an external process is responsible, on an entity-by-entity basis. + +The default Build Strategy results in the TechDocs backend building documentation locally if the +`techdocs.builder` configuration option is set to `'local'`, and skipping any building otherwise. +However any logic that satisfies the Build Strategy interface can be implemented, using the Backstage +config as well as the entity being processed to make a decision. + +For an example of how the Build Strategy can be used to implement a 'hybrid' build model, refer to +the [How to implement a hybrid build strategy](./how-to-guides#how-to-implement-a-hybrid-build-strategy) guide. + ## TechDocs Container The TechDocs container is a Docker container available at diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index 6317ae365b..9509a9a545 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -38,11 +38,16 @@ techdocs: pullImage: true # techdocs.builder can be either 'local' or 'external. - # If builder is set to 'local' and you open a TechDocs page, techdocs-backend will try to generate the docs, publish to storage - # and show the generated docs afterwords. This is the "Basic" setup of the TechDocs Architecture. - # If builder is set to 'external', techdocs-backend will only fetch the docs and will NOT try to generate and publish. In this case of 'external', - # we assume that docs are being built by an external process (e.g. in the CI/CD pipeline of the repository). This is the "Recommended" setup of - # the architecture. Read more here https://backstage.io/docs/features/techdocs/architecture + # Using the default build strategy, if builder is set to 'local' and you open a TechDocs page, + # techdocs-backend will try to generate the docs, publish to storage and show the generated docs afterwords. + # This is the "Basic" setup of the TechDocs Architecture. + # Using the default build strategy, if builder is set to 'external' (or anything other than 'local'), techdocs-backend + # will only fetch the docs and will NOT try to generate and publish. + # In this case, we assume that docs are being built by an external process (e.g. in the CI/CD pipeline of the repository). + # This is the "Recommended" setup of the architecture. + # Note that custom build strategies may alter this behaviour. + # Read more about the "Basic" and "Recommended" setups here https://backstage.io/docs/features/techdocs/architecture + # Read more about build strategies here: https://backstage.io/docs/features/techdocs/concepts#techdocs-build-strategy builder: 'local' diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index 863b4b3ecb..5ec8aac307 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -538,3 +538,53 @@ Done! Now you have a support of the following diagrams along with mermaid: - `Vega` - `Vega-Lite` - `WaveDrom` + +## How to implement a hybrid build strategy + +One limitation of the [Recommended deployment](./architecture#recommended-deployment) is that +the experience for users requires modifying their CI/CD process to publish +their TechDocs. For some users, this may be unnecessary, and provides a barrier +to entry for onboarding users to Backstage. However, a purely local TechDocs +build restricts TechDocs creators to using the tooling provided in Backstage, +as well as the plugins and features provided in the Backstage-included `mkdocs` +installation. + +To accommodate both of these use-cases, users can implement a custom [Build Strategy](./concepts#techdocs-build-strategy) +with logic to encode which TechDocs should be built locally, and which will be +built externally. + +To achieve this hybrid build model: + +1. In your Backstage instance's `app-config.yaml`, set `techdocs.builder` to + `'local'`. This ensures that Backstage will build docs for users who want the + 'out-of-the-box' experience. +2. Configure external storage of TechDocs as normal for a production deployment. + This allows Backstage to publish documentation to your storage, as well as + allowing other users to publish documentation from their CI/CD pipelines. +3. Create a custom build strategy, that implements the `DocsBuildStrategy` interface, + and which implements your custom logic for determining whether to build docs for + a given entity. + For example, to only build docs when an entity has the `company.com/techdocs-builder` + annotation set to `'local'`: + ```typescript + export class AnnotationBasedBuildStrategy { + private readonly config: Config; + + constructor(config: Config) { + this.config = config; + } + + async shouldBuild(_: Entity): Promise { + return this.entity.metadata?.annotations?.["company.com/techdocs-builder"] === 'local' + } + } + ``` +4. Pass an instance of this Build Strategy as the `docsBuildStrategy` parameter of the + TechDocs backend `createRouter` method. + +Users should now be able to choose to have their documentation built and published by +the TechDocs backend by adding the `company.com/techdocs-builder` annotation to their +entity. If the value of this annotation is `'local'`, the TechDocs backend will build +and publish the documentation for them. If the value of the `company.com/techdocs-builder` +annotation is anything other than `'local'`, the user is responsible for publishing +documentation to the appropriate location in the TechDocs external storage. From 9731e500688c598dfb6c19b7d752775202cadd2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Feb 2022 04:11:23 +0000 Subject: [PATCH 018/353] chore(deps): bump eslint from 8.7.0 to 8.10.0 Bumps [eslint](https://github.com/eslint/eslint) from 8.7.0 to 8.10.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v8.7.0...v8.10.0) --- updated-dependencies: - dependency-name: eslint dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/yarn.lock b/yarn.lock index 997e55c9e9..2e89e7b0d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1808,14 +1808,14 @@ ts-node "^9" tslib "^2" -"@eslint/eslintrc@^1.0.5": - version "1.0.5" - resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.5.tgz#33f1b838dbf1f923bfa517e008362b78ddbbf318" - integrity sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ== +"@eslint/eslintrc@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.0.tgz#7ce1547a5c46dfe56e1e45c3c9ed18038c721c6a" + integrity sha512-igm9SjJHNEJRiUnecP/1R5T3wKLEJ7pL6e2P+GUSfCd0dGjPYYZve08uzw8L2J8foVHFz+NGu12JxRcU2gGo6w== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.2.0" + espree "^9.3.1" globals "^13.9.0" ignore "^4.0.6" import-fresh "^3.2.1" @@ -11688,10 +11688,10 @@ eslint-scope@5.1.1, eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.1.0: - version "7.1.0" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz#c1f6ea30ac583031f203d65c73e723b01298f153" - integrity sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg== +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" @@ -11708,10 +11708,10 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== -eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.1.0, eslint-visitor-keys@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz#6fbb166a6798ee5991358bc2daa1ba76cc1254a1" - integrity sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ== +eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== eslint-webpack-plugin@^2.6.0: version "2.6.0" @@ -11726,11 +11726,11 @@ eslint-webpack-plugin@^2.6.0: schema-utils "^3.1.1" eslint@^8.6.0: - version "8.7.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-8.7.0.tgz#22e036842ee5b7cf87b03fe237731675b4d3633c" - integrity sha512-ifHYzkBGrzS2iDU7KjhCAVMGCvF6M3Xfs8X8b37cgrUlDt6bWRTpRh6T/gtSXv1HJ/BUGgmjvNvOEGu85Iif7w== + version "8.10.0" + resolved "https://registry.npmjs.org/eslint/-/eslint-8.10.0.tgz#931be395eb60f900c01658b278e05b6dae47199d" + integrity sha512-tcI1D9lfVec+R4LE1mNDnzoJ/f71Kl/9Cv4nG47jOueCMBrCCKYXr4AUVS7go6mWYGFD4+EoN6+eXSrEbRzXVw== dependencies: - "@eslint/eslintrc" "^1.0.5" + "@eslint/eslintrc" "^1.2.0" "@humanwhocodes/config-array" "^0.9.2" ajv "^6.10.0" chalk "^4.0.0" @@ -11738,10 +11738,10 @@ eslint@^8.6.0: debug "^4.3.2" doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.1.0" + eslint-scope "^7.1.1" eslint-utils "^3.0.0" - eslint-visitor-keys "^3.2.0" - espree "^9.3.0" + eslint-visitor-keys "^3.3.0" + espree "^9.3.1" esquery "^1.4.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -11771,14 +11771,14 @@ esm@^3.2.25: resolved "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== -espree@^9.2.0, espree@^9.3.0: - version "9.3.0" - resolved "https://registry.npmjs.org/espree/-/espree-9.3.0.tgz#c1240d79183b72aaee6ccfa5a90bc9111df085a8" - integrity sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ== +espree@^9.3.1: + version "9.3.1" + resolved "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz#8793b4bc27ea4c778c19908e0719e7b8f4115bcd" + integrity sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ== dependencies: acorn "^8.7.0" acorn-jsx "^5.3.1" - eslint-visitor-keys "^3.1.0" + eslint-visitor-keys "^3.3.0" esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: version "4.0.1" From 2d858b5c2a2dc4f6e3e74ebb6e99f34ab40937e8 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 28 Feb 2022 10:48:09 +0100 Subject: [PATCH 019/353] use experimental type build, include alpha types Signed-off-by: Johan Haals --- plugins/catalog-react/package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index b80d1f5789..119c3f19a4 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -25,7 +25,7 @@ "backstage" ], "scripts": { - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -77,6 +77,7 @@ "react-test-renderer": "^16.13.1" }, "files": [ - "dist" + "dist", + "alpha" ] } From d88b32361fc54250694706fd45ab941fe00db43e Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Mon, 28 Feb 2022 11:21:30 +0000 Subject: [PATCH 020/353] Update the Airbrake installation instructions Signed-off-by: Karan Shah --- plugins/airbrake/README.md | 64 ++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/plugins/airbrake/README.md b/plugins/airbrake/README.md index d5ea9bb006..28c230fb05 100644 --- a/plugins/airbrake/README.md +++ b/plugins/airbrake/README.md @@ -20,7 +20,7 @@ The Airbrake plugin provides connectivity between Backstage and Airbrake (https: yarn add @backstage/plugin-airbrake-backend ``` -3. Add the `EntityAirbrakeContent` to `packages/app/src/components/catalog/EntityPage.tsx`: +3. Add the `EntityAirbrakeContent` to `packages/app/src/components/catalog/EntityPage.tsx` for all entity pages you want Airbrake to be in: ```typescript jsx import { EntityAirbrakeContent } from '@backstage/plugin-airbrake'; @@ -32,40 +32,78 @@ The Airbrake plugin provides connectivity between Backstage and Airbrake (https: ); + + const websiteEntityPage = ( + + + + + + ); + + const defaultEntityPage = ( + + + + + + ); ``` -4. Setup the Backend code in `packages/backend/src/index.ts`: +4. Create `packages/backend/src/plugins/airbrake.ts` with these contents: ```typescript + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; import { - createRouter as createAirbrakeRouter, + createRouter, extractAirbrakeConfig, } from '@backstage/plugin-airbrake-backend'; - async function main() { - //... After const config = await loadBackendConfig({ ... - - const airbrakeRouter = await createAirbrakeRouter({ + export default async function createPlugin({ + logger, + config, + }: PluginEnvironment): Promise { + return createRouter({ logger, airbrakeConfig: extractAirbrakeConfig(config), }); - - const service = createServiceBuilder(module) - // ... Add the airbrakeRouter here - .addRouter('/api/airbrake', airbrakeRouter); } ``` -5. Add this config as a top level section in your `app-config.yaml`: +5. Setup the Backend code in `packages/backend/src/index.ts`: + + ```typescript + import airbrake from './plugins/airbrake'; + + async function main() { + //... After const createEnv = makeCreateEnv(config) ... + + const airbrakeEnv = useHotMemoize(module, () => createEnv('airbrake')); + + //... After const apiRouter = Router() ... + apiRouter.use('/airbrake', await airbrake(airbrakeEnv)); + } + ``` + +6. Add this config as a top level section in your `app-config.yaml`: ```yaml airbrake: apiKey: ${AIRBRAKE_API_KEY} ``` -6. Set an environment variable `AIRBRAKE_API_KEY` with your [API key](https://airbrake.io/docs/api/#authentication) +7. Set an environment variable `AIRBRAKE_API_KEY` with your [API key](https://airbrake.io/docs/api/#authentication) before starting Backstage backend. +8. Add the following annotation to the `catalog-info.yaml` for a repo you want to link to an Airbrake project: + + ```yaml + metadata: + annotations: + airbrake.io/project-id: '123456' + ``` + ## Local Development Start this plugin in standalone mode by running `yarn start` inside the plugin directory. This method of serving the plugin provides quicker From 71353b0fe0cbe53dd1775c217d1fffb2ad58370f Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Mon, 28 Feb 2022 11:25:16 +0000 Subject: [PATCH 021/353] Small grammar improvement Signed-off-by: Karan Shah --- plugins/airbrake/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/airbrake/README.md b/plugins/airbrake/README.md index 28c230fb05..bc830c326d 100644 --- a/plugins/airbrake/README.md +++ b/plugins/airbrake/README.md @@ -20,7 +20,7 @@ The Airbrake plugin provides connectivity between Backstage and Airbrake (https: yarn add @backstage/plugin-airbrake-backend ``` -3. Add the `EntityAirbrakeContent` to `packages/app/src/components/catalog/EntityPage.tsx` for all entity pages you want Airbrake to be in: +3. Add the `EntityAirbrakeContent` to `packages/app/src/components/catalog/EntityPage.tsx` for all the entity pages you want Airbrake to be in: ```typescript jsx import { EntityAirbrakeContent } from '@backstage/plugin-airbrake'; From ed1083a12fea50a9c59d4ba113e173fb01f19a89 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Mon, 28 Feb 2022 11:35:09 +0000 Subject: [PATCH 022/353] Improve the backend README as well. Signed-off-by: Karan Shah --- plugins/airbrake-backend/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/airbrake-backend/README.md b/plugins/airbrake-backend/README.md index 9c9b56ade4..21056229de 100644 --- a/plugins/airbrake-backend/README.md +++ b/plugins/airbrake-backend/README.md @@ -24,7 +24,7 @@ This method of serving the plugin provides quicker iteration speed and a faster 3. Go into the plugin's directory and run it in standalone mode by running `yarn start`. -Access it from http://localhost:7007/api/airbrake. Or use the Airbrake plugin which will talk to it automatically. +Access it from http://localhost:7007/api/airbrake. Or use the [Airbrake plugin in standalone mode](../airbrake/README.md#local-development) which will talk to it automatically. Here are some example endpoints: From 2a8d9ff6350362a0a2e9adc02cfc22c204c63d38 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 28 Feb 2022 13:16:42 +0100 Subject: [PATCH 023/353] storybook: remove usage of oauth2ApiRef & auth0AuthApiRef Signed-off-by: Johan Haals --- storybook/.storybook/apis.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/storybook/.storybook/apis.js b/storybook/.storybook/apis.js index 4c0813a8e9..37cc8eb605 100644 --- a/storybook/.storybook/apis.js +++ b/storybook/.storybook/apis.js @@ -5,10 +5,8 @@ import { GithubAuth, GitlabAuth, GoogleAuth, - OAuth2, OAuthRequestManager, OktaAuth, - Auth0Auth, ConfigReader, LocalStorageFeatureFlags, } from '@backstage/core-app-api'; @@ -20,10 +18,8 @@ import { gitlabAuthApiRef, googleAuthApiRef, identityApiRef, - oauth2ApiRef, oauthRequestApiRef, oktaAuthApiRef, - auth0AuthApiRef, configApiRef, featureFlagsApiRef, } from '@backstage/core-plugin-api'; @@ -59,16 +55,6 @@ const oktaAuthApi = OktaAuth.create({ basePath: '/auth/', oauthRequestApi, }); -const auth0AuthApi = Auth0Auth.create({ - apiOrigin: 'http://localhost:7007', - basePath: '/auth/', - oauthRequestApi, -}); -const oauth2Api = OAuth2.create({ - apiOrigin: 'http://localhost:7007', - basePath: '/auth/', - oauthRequestApi, -}); export const apis = [ [configApiRef, configApi], @@ -81,6 +67,4 @@ export const apis = [ [githubAuthApiRef, githubAuthApi], [gitlabAuthApiRef, gitlabAuthApi], [oktaAuthApiRef, oktaAuthApi], - [auth0AuthApiRef, auth0AuthApi], - [oauth2ApiRef, oauth2Api], ]; From 03ec06bf7f62daa648c34eea70080881d0b00eef Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 18 Feb 2022 17:05:28 +0100 Subject: [PATCH 024/353] catalog-react: refactor and fixes for DefaultStarredEntitiesApi Signed-off-by: Patrik Oldsberg --- .changeset/silver-boxes-flash.md | 7 + plugins/catalog-react/api-report.md | 2 - .../DefaultStarredEntitiesApi.test.ts | 120 ++++++++---------- .../DefaultStarredEntitiesApi.ts | 8 +- 4 files changed, 65 insertions(+), 72 deletions(-) create mode 100644 .changeset/silver-boxes-flash.md diff --git a/.changeset/silver-boxes-flash.md b/.changeset/silver-boxes-flash.md new file mode 100644 index 0000000000..4d3d70259a --- /dev/null +++ b/.changeset/silver-boxes-flash.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Fixed a risky behavior where `DefaultStarredEntitiesApi` forwarded values to observers that were later mutated. + +Removed the `isStarred` method from `DefaultStarredEntitiesApi`, as it is not part of the `StarredEntitiesApi`. diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index e78ec75daa..add76a7c1c 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -142,8 +142,6 @@ export type DefaultEntityFilters = { export class DefaultStarredEntitiesApi implements StarredEntitiesApi { constructor(opts: { storageApi: StorageApi }); // (undocumented) - isStarred(entityRef: string): boolean; - // (undocumented) starredEntitie$(): Observable>; // (undocumented) toggleStarred(entityRef: string): Promise; diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts b/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts index 55b992fadf..2a3c4d83ed 100644 --- a/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts +++ b/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts @@ -14,33 +14,27 @@ * limitations under the License. */ -import { stringifyEntityRef } from '@backstage/catalog-model'; -import { StorageApi } from '@backstage/core-plugin-api'; import { MockStorageApi } from '@backstage/test-utils'; import { DefaultStarredEntitiesApi } from './DefaultStarredEntitiesApi'; import { performMigrationToTheNewBucket } from './migration'; jest.mock('./migration'); -describe('DefaultStarredEntitiesApi', () => { - let mockStorage: StorageApi; - let starredEntitiesApi: DefaultStarredEntitiesApi; - - const mockEntityRef = stringifyEntityRef({ - apiVersion: '1', - kind: 'Component', - metadata: { - name: 'mock', - }, +function getStarred(api: DefaultStarredEntitiesApi) { + return new Promise((resolve, reject) => { + const subscription = api.starredEntitie$().subscribe({ + next(starred) { + resolve(starred); + subscription.unsubscribe(); + }, + error: reject, + }); }); +} +describe('DefaultStarredEntitiesApi', () => { beforeEach(() => { (performMigrationToTheNewBucket as jest.Mock).mockResolvedValue(undefined); - - mockStorage = MockStorageApi.create(); - starredEntitiesApi = new DefaultStarredEntitiesApi({ - storageApi: mockStorage, - }); }); afterEach(() => { @@ -49,55 +43,53 @@ describe('DefaultStarredEntitiesApi', () => { describe('constructor', () => { it('should call migration', () => { - expect(performMigrationToTheNewBucket).toBeCalledTimes(1); - }); - }); - - describe('toggleStarred', () => { - it('should star unstarred entity', async () => { - expect(starredEntitiesApi.isStarred(mockEntityRef)).toBe(false); - - await starredEntitiesApi.toggleStarred(mockEntityRef); - - expect(starredEntitiesApi.isStarred(mockEntityRef)).toBe(true); - }); - - it('should unstar starred entity', async () => { - const bucket = mockStorage.forBucket('starredEntities'); - await bucket.set('entityRefs', ['component:default/mock']); - - expect(starredEntitiesApi.isStarred(mockEntityRef)).toBe(true); - - await starredEntitiesApi.toggleStarred(mockEntityRef); - - expect(starredEntitiesApi.isStarred(mockEntityRef)).toBe(false); - }); - }); - - describe('starredEntities$', () => { - const handler = jest.fn(); - - beforeEach(async () => { - await new Promise(resolve => { - starredEntitiesApi.starredEntitie$().subscribe({ - next: (...args) => { - handler(...args); - - if (handler.mock.calls.length >= 2) { - resolve(); - } - }, - }); - - const bucket = mockStorage.forBucket('starredEntities'); - bucket.set('entityRefs', ['component:default/mock']).then(); + const api = new DefaultStarredEntitiesApi({ + storageApi: MockStorageApi.create(), }); + expect(performMigrationToTheNewBucket).toBeCalledTimes(1); + expect(api).toBeDefined(); + }); + }); + + it('should notify and toggle starred entities', async () => { + const entityRef = 'component:default/mock'; + + const storageApi = MockStorageApi.create(); + const storageBucket = storageApi.forBucket('starredEntities'); + const api = new DefaultStarredEntitiesApi({ storageApi }); + + const values = new Array>(); + api.starredEntitie$().subscribe({ + next: value => { + values.push(value); + }, }); - it('should receive updates', async () => { - expect(handler).toBeCalledTimes(2); - expect(handler).toBeCalledWith(new Set()); - expect(handler).toBeCalledWith(new Set(['component:default/mock'])); - }); + await expect(getStarred(api)).resolves.toEqual(new Set()); + + await api.toggleStarred(entityRef); + await expect(getStarred(api)).resolves.toEqual(new Set([entityRef])); + expect(storageBucket.snapshot('entityRefs')).toEqual( + expect.objectContaining({ presence: 'present', value: [entityRef] }), + ); + + await api.toggleStarred(entityRef); + await expect(getStarred(api)).resolves.toEqual(new Set()); + expect(storageBucket.snapshot('entityRefs')).toEqual( + expect.objectContaining({ presence: 'present', value: [] }), + ); + + expect(values).toEqual([new Set(), new Set([entityRef]), new Set()]); + }); + + it('should read starred entities from storage', async () => { + const entityRef = 'component:default/mock'; + + const storageApi = MockStorageApi.create(); + const storageBucket = storageApi.forBucket('starredEntities'); + storageBucket.set('entityRefs', [entityRef]); + const api = new DefaultStarredEntitiesApi({ storageApi }); + + await expect(getStarred(api)).resolves.toEqual(new Set([entityRef])); }); }); diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts b/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts index 46a8e63084..c411c5a81f 100644 --- a/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts +++ b/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts @@ -64,17 +64,13 @@ export class DefaultStarredEntitiesApi implements StarredEntitiesApi { return this.observable; } - isStarred(entityRef: string): boolean { - return this.starredEntities.has(entityRef); - } - private readonly subscribers = new Set< ZenObservable.SubscriptionObserver> >(); private readonly observable = new ObservableImpl>(subscriber => { // forward the the latest value - subscriber.next(this.starredEntities); + subscriber.next(new Set(this.starredEntities)); this.subscribers.add(subscriber); return () => { @@ -84,7 +80,7 @@ export class DefaultStarredEntitiesApi implements StarredEntitiesApi { private notifyChanges() { for (const subscription of this.subscribers) { - subscription.next(this.starredEntities); + subscription.next(new Set(this.starredEntities)); } } } From c077b432b8cf1f76ea95881b84df9d2949cba2be Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 18 Feb 2022 17:42:18 +0100 Subject: [PATCH 025/353] catalog-react: moved DefaultStarredEntitiesApi implementation to catalog plugin Signed-off-by: Patrik Oldsberg --- .changeset/silver-boxes-flash.md | 2 ++ .../src/components/catalog/EntityPage.test.tsx | 4 +++- .../DefaultApiExplorerPage.test.tsx | 6 ++++-- plugins/catalog-react/api-report.md | 10 ---------- .../src/apis/StarredEntitiesApi/index.ts | 1 - .../src/hooks/useEntityListProvider.test.tsx | 3 ++- .../src/hooks/useStarredEntities.test.tsx | 3 ++- plugins/catalog/api-report.md | 12 ++++++++++++ plugins/catalog/package.json | 4 +++- .../DefaultStarredEntitiesApi.test.ts | 0 .../DefaultStarredEntitiesApi.ts | 2 +- .../src/apis/StarredEntitiesApi/index.ts | 17 +++++++++++++++++ .../apis/StarredEntitiesApi/migration.test.ts | 0 .../src/apis/StarredEntitiesApi/migration.ts | 0 plugins/catalog/src/apis/index.ts | 17 +++++++++++++++++ .../CatalogPage/DefaultCatalogPage.test.tsx | 2 +- .../CatalogTable/CatalogTable.test.tsx | 2 +- .../EntityLayout/EntityLayout.test.tsx | 2 +- plugins/catalog/src/index.ts | 2 ++ plugins/catalog/src/plugin.ts | 2 +- .../StarredEntities/Content.test.tsx | 2 +- .../StarredEntities/StarredEntities.stories.tsx | 2 +- .../src/templates/DefaultTemplate.stories.tsx | 3 +-- plugins/scaffolder/dev/index.tsx | 6 ++++-- .../components/DefaultTechDocsHome.test.tsx | 2 +- 25 files changed, 77 insertions(+), 29 deletions(-) rename plugins/{catalog-react => catalog}/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts (100%) rename plugins/{catalog-react => catalog}/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts (97%) create mode 100644 plugins/catalog/src/apis/StarredEntitiesApi/index.ts rename plugins/{catalog-react => catalog}/src/apis/StarredEntitiesApi/migration.test.ts (100%) rename plugins/{catalog-react => catalog}/src/apis/StarredEntitiesApi/migration.ts (100%) create mode 100644 plugins/catalog/src/apis/index.ts diff --git a/.changeset/silver-boxes-flash.md b/.changeset/silver-boxes-flash.md index 4d3d70259a..3b860a06d6 100644 --- a/.changeset/silver-boxes-flash.md +++ b/.changeset/silver-boxes-flash.md @@ -2,6 +2,8 @@ '@backstage/plugin-catalog-react': patch --- +**BREAKING**: Moved **DefaultStarredEntitiesApi** to `@backstage/plugin-catalog`. If you were using this in tests, you can add `@backstage/plugin-catalog` your packages `devDependencies` instead. + Fixed a risky behavior where `DefaultStarredEntitiesApi` forwarded values to observers that were later mutated. Removed the `isStarred` method from `DefaultStarredEntitiesApi`, as it is not part of the `StarredEntitiesApi`. diff --git a/packages/app/src/components/catalog/EntityPage.test.tsx b/packages/app/src/components/catalog/EntityPage.test.tsx index b68b9e9895..4608dc7533 100644 --- a/packages/app/src/components/catalog/EntityPage.test.tsx +++ b/packages/app/src/components/catalog/EntityPage.test.tsx @@ -14,9 +14,11 @@ * limitations under the License. */ -import { EntityLayout } from '@backstage/plugin-catalog'; import { + EntityLayout, DefaultStarredEntitiesApi, +} from '@backstage/plugin-catalog'; +import { EntityProvider, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; diff --git a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx index 16b335b0a7..5924af3ef3 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx @@ -22,11 +22,13 @@ import { configApiRef, storageApiRef, } from '@backstage/core-plugin-api'; -import { CatalogTableRow } from '@backstage/plugin-catalog'; +import { + CatalogTableRow, + DefaultStarredEntitiesApi, +} from '@backstage/plugin-catalog'; import { CatalogApi, catalogApiRef, - DefaultStarredEntitiesApi, entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index add76a7c1c..b8f0fa5538 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -24,7 +24,6 @@ import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { StorageApi } from '@backstage/core-plugin-api'; import { StyleRules } from '@material-ui/core/styles/withStyles'; import { SystemEntity } from '@backstage/catalog-model'; import { TableColumn } from '@backstage/core-components'; @@ -138,15 +137,6 @@ export type DefaultEntityFilters = { text?: EntityTextFilter; }; -// @public -export class DefaultStarredEntitiesApi implements StarredEntitiesApi { - constructor(opts: { storageApi: StorageApi }); - // (undocumented) - starredEntitie$(): Observable>; - // (undocumented) - toggleStarred(entityRef: string): Promise; -} - // @public (undocumented) export type EntityFilter = { getCatalogFilters?: () => Record< diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/index.ts b/plugins/catalog-react/src/apis/StarredEntitiesApi/index.ts index e9f9c8923a..94a221b36f 100644 --- a/plugins/catalog-react/src/apis/StarredEntitiesApi/index.ts +++ b/plugins/catalog-react/src/apis/StarredEntitiesApi/index.ts @@ -14,6 +14,5 @@ * limitations under the License. */ -export { DefaultStarredEntitiesApi } from './DefaultStarredEntitiesApi'; export { starredEntitiesApiRef } from './StarredEntitiesApi'; export type { StarredEntitiesApi } from './StarredEntitiesApi'; diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index c8ca29b07f..5db9e2be9c 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -23,13 +23,14 @@ import { identityApiRef, storageApiRef, } from '@backstage/core-plugin-api'; +import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; import { MockStorageApi, TestApiProvider } from '@backstage/test-utils'; import { act, renderHook } from '@testing-library/react-hooks'; import qs from 'qs'; import React, { PropsWithChildren } from 'react'; import { MemoryRouter } from 'react-router'; import { catalogApiRef } from '../api'; -import { DefaultStarredEntitiesApi, starredEntitiesApiRef } from '../apis'; +import { starredEntitiesApiRef } from '../apis'; import { EntityKindPicker, UserListPicker } from '../components'; import { EntityKindFilter, EntityTypeFilter, UserListFilter } from '../filters'; import { UserListFilterKind } from '../types'; diff --git a/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx b/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx index aef876a1b2..949ac6515b 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx +++ b/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx @@ -16,10 +16,11 @@ import { Entity } from '@backstage/catalog-model'; import { StorageApi } from '@backstage/core-plugin-api'; +import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; import { MockStorageApi, TestApiProvider } from '@backstage/test-utils'; import { act, renderHook } from '@testing-library/react-hooks'; import React, { PropsWithChildren } from 'react'; -import { DefaultStarredEntitiesApi, starredEntitiesApiRef } from '../apis'; +import { starredEntitiesApiRef } from '../apis'; import { useStarredEntities } from './useStarredEntities'; describe('useStarredEntities', () => { diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index bd5edcbfa6..f314932b9a 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -13,10 +13,13 @@ import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { IndexableDocument } from '@backstage/search-common'; import { InfoCardVariants } from '@backstage/core-components'; +import { Observable } from '@backstage/types'; import { Overrides } from '@material-ui/core/styles/overrides'; import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; +import { StarredEntitiesApi } from '@backstage/plugin-catalog-react'; +import { StorageApi } from '@backstage/core-plugin-api'; import { StyleRules } from '@material-ui/core/styles/withStyles'; import { TableColumn } from '@backstage/core-components'; import { TableProps } from '@backstage/core-components'; @@ -170,6 +173,15 @@ export interface DefaultCatalogPageProps { initiallySelectedFilter?: UserListFilterKind; } +// @public +export class DefaultStarredEntitiesApi implements StarredEntitiesApi { + constructor(opts: { storageApi: StorageApi }); + // (undocumented) + starredEntitie$(): Observable>; + // (undocumented) + toggleStarred(entityRef: string): Promise; +} + // @public (undocumented) export interface DependencyOfComponentsCardProps { // (undocumented) diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index f7800af139..6179cf6c44 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -44,6 +44,7 @@ "@backstage/plugin-catalog-react": "^0.7.0", "@backstage/search-common": "^0.2.4", "@backstage/theme": "^0.2.15", + "@backstage/types": "^0.1.2", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -51,7 +52,8 @@ "lodash": "^4.17.21", "react-helmet": "6.1.0", "react-router": "6.0.0-beta.0", - "react-use": "^17.2.4" + "react-use": "^17.2.4", + "zen-observable": "^0.8.15" }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts b/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts similarity index 100% rename from plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts rename to plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts b/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts similarity index 97% rename from plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts rename to plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts index c411c5a81f..e9cf84f11b 100644 --- a/plugins/catalog-react/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts +++ b/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts @@ -15,10 +15,10 @@ */ import { StorageApi } from '@backstage/core-plugin-api'; +import { StarredEntitiesApi } from '@backstage/plugin-catalog-react'; import { Observable } from '@backstage/types'; import ObservableImpl from 'zen-observable'; import { performMigrationToTheNewBucket } from './migration'; -import { StarredEntitiesApi } from './StarredEntitiesApi'; /** * Default implementation of the StarredEntitiesApi that is backed by the StorageApi. diff --git a/plugins/catalog/src/apis/StarredEntitiesApi/index.ts b/plugins/catalog/src/apis/StarredEntitiesApi/index.ts new file mode 100644 index 0000000000..42dc977fb6 --- /dev/null +++ b/plugins/catalog/src/apis/StarredEntitiesApi/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { DefaultStarredEntitiesApi } from './DefaultStarredEntitiesApi'; diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/migration.test.ts b/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts similarity index 100% rename from plugins/catalog-react/src/apis/StarredEntitiesApi/migration.test.ts rename to plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/migration.ts b/plugins/catalog/src/apis/StarredEntitiesApi/migration.ts similarity index 100% rename from plugins/catalog-react/src/apis/StarredEntitiesApi/migration.ts rename to plugins/catalog/src/apis/StarredEntitiesApi/migration.ts diff --git a/plugins/catalog/src/apis/index.ts b/plugins/catalog/src/apis/index.ts new file mode 100644 index 0000000000..5c7e980890 --- /dev/null +++ b/plugins/catalog/src/apis/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 './StarredEntitiesApi'; diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx index dcf819f976..e36c1e9338 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx @@ -29,7 +29,6 @@ import { } from '@backstage/core-plugin-api'; import { catalogApiRef, - DefaultStarredEntitiesApi, entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; @@ -43,6 +42,7 @@ import { import DashboardIcon from '@material-ui/icons/Dashboard'; import { fireEvent, screen } from '@testing-library/react'; import React from 'react'; +import { DefaultStarredEntitiesApi } from '../../apis'; import { createComponentRouteRef } from '../../routes'; import { CatalogTableRow } from '../CatalogTable'; import { DefaultCatalogPage } from './DefaultCatalogPage'; diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index db9835e3a3..1d49638eed 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -22,7 +22,6 @@ import { import { ApiProvider } from '@backstage/core-app-api'; import { entityRouteRef, - DefaultStarredEntitiesApi, MockEntityListContextProvider, starredEntitiesApiRef, UserListFilter, @@ -34,6 +33,7 @@ import { } from '@backstage/test-utils'; import { act, fireEvent } from '@testing-library/react'; import * as React from 'react'; +import { DefaultStarredEntitiesApi } from '../../apis'; import { CatalogTable } from './CatalogTable'; const entities: Entity[] = [ diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx index 78dcf855e8..b29261c28b 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx @@ -21,7 +21,6 @@ import { AlertApi, alertApiRef } from '@backstage/core-plugin-api'; import { AsyncEntityProvider, catalogApiRef, - DefaultStarredEntitiesApi, EntityProvider, entityRouteRef, starredEntitiesApiRef, @@ -36,6 +35,7 @@ import { import { act, fireEvent } from '@testing-library/react'; import React from 'react'; import { Route, Routes } from 'react-router'; +import { DefaultStarredEntitiesApi } from '../../apis'; import { EntityLayout } from './EntityLayout'; const mockEntity = { diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index fef420d6d3..662a920f8b 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -20,6 +20,8 @@ * @packageDocumentation */ +export * from './apis'; + export * from './components/AboutCard'; export * from './components/CatalogKindHeader'; export * from './components/CatalogSearchResultListItem'; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index b4d1a61bc1..520656e921 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -19,7 +19,6 @@ import { Entity } from '@backstage/catalog-model'; import { catalogApiRef, catalogRouteRef, - DefaultStarredEntitiesApi, entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; @@ -33,6 +32,7 @@ import { fetchApiRef, storageApiRef, } from '@backstage/core-plugin-api'; +import { DefaultStarredEntitiesApi } from './apis'; import { AboutCardProps } from './components/AboutCard'; import { DefaultCatalogPageProps } from './components/CatalogPage'; import { DependencyOfComponentsCardProps } from './components/DependencyOfComponentsCard'; diff --git a/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx b/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx index ccc36f6964..ae4a84473f 100644 --- a/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx +++ b/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx @@ -21,8 +21,8 @@ import { import { starredEntitiesApiRef, entityRouteRef, - DefaultStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; +import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; import React from 'react'; import { Content } from './Content'; diff --git a/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx b/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx index 2e63762369..55486a5dcd 100644 --- a/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx +++ b/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx @@ -23,8 +23,8 @@ import { import { starredEntitiesApiRef, entityRouteRef, - DefaultStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; +import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; import { Grid } from '@material-ui/core'; import React, { ComponentType } from 'react'; diff --git a/plugins/home/src/templates/DefaultTemplate.stories.tsx b/plugins/home/src/templates/DefaultTemplate.stories.tsx index ba5e1f33fd..86d1b60022 100644 --- a/plugins/home/src/templates/DefaultTemplate.stories.tsx +++ b/plugins/home/src/templates/DefaultTemplate.stories.tsx @@ -26,8 +26,8 @@ import { Content, Page, InfoCard } from '@backstage/core-components'; import { starredEntitiesApiRef, entityRouteRef, - DefaultStarredEntitiesApi } from '@backstage/plugin-catalog-react'; +import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; import { HomePageSearchBar, SearchContextProvider, @@ -153,4 +153,3 @@ export const DefaultTemplate = () => { ); }; - diff --git a/plugins/scaffolder/dev/index.tsx b/plugins/scaffolder/dev/index.tsx index 78e6254e09..6ecc1a4564 100644 --- a/plugins/scaffolder/dev/index.tsx +++ b/plugins/scaffolder/dev/index.tsx @@ -20,7 +20,6 @@ import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { catalogApiRef, starredEntitiesApiRef, - DefaultStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; import React from 'react'; import { scaffolderApiRef, ScaffolderClient } from '../src'; @@ -30,7 +29,10 @@ import { fetchApiRef, storageApiRef, } from '@backstage/core-plugin-api'; -import { CatalogEntityPage } from '@backstage/plugin-catalog'; +import { + CatalogEntityPage, + DefaultStarredEntitiesApi, +} from '@backstage/plugin-catalog'; createDevApp() .addPage({ diff --git a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx index 8618c4432f..678c737547 100644 --- a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx +++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx @@ -20,10 +20,10 @@ import { configApiRef, storageApiRef, } from '@backstage/core-plugin-api'; +import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; import { CatalogApi, catalogApiRef, - DefaultStarredEntitiesApi, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; import { From 2c5d38e337548cf3078addfebcee20894bca0c68 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 28 Feb 2022 16:19:05 +0100 Subject: [PATCH 026/353] chore: don't export the components that back the field extensions Signed-off-by: blam --- plugins/scaffolder/api-report.md | 62 ++++++------------- .../fields/EntityNamePicker/index.ts | 1 - .../components/fields/EntityPicker/index.ts | 1 - .../fields/EntityTagsPicker/index.ts | 1 - .../fields/OwnedEntityPicker/index.ts | 1 - .../fields/OwnerPicker/OwnerPicker.tsx | 2 +- .../components/fields/OwnerPicker/index.ts | 1 - .../components/fields/RepoUrlPicker/index.ts | 1 - plugins/scaffolder/src/extensions/default.ts | 20 +++--- plugins/scaffolder/src/extensions/index.tsx | 6 +- plugins/scaffolder/src/plugin.ts | 20 +++--- 11 files changed, 40 insertions(+), 76 deletions(-) diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 82dfecfaa5..eed65578d3 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -28,6 +28,7 @@ import { ScmIntegrationRegistry } from '@backstage/integration'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +// Warning: (ae-forgotten-export) The symbol "FieldExtensionComponent" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "createScaffolderFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -36,7 +37,7 @@ export function createScaffolderFieldExtension< TInputProps = unknown, >( options: FieldExtensionOptions, -): Extension<() => null>; +): Extension>; // @public export type CustomFieldValidator = ( @@ -50,19 +51,16 @@ export type CustomFieldValidator = ( // Warning: (ae-missing-release-tag) "EntityNamePickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityNamePickerFieldExtension: () => null; - -// Warning: (ae-missing-release-tag) "EntityPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export const EntityPicker: ( - props: FieldExtensionComponentProps, -) => JSX.Element; +export const EntityNamePickerFieldExtension: FieldExtensionComponent< + FieldExtensionComponentProps +>; // Warning: (ae-missing-release-tag) "EntityPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityPickerFieldExtension: () => null; +export const EntityPickerFieldExtension: FieldExtensionComponent< + FieldExtensionComponentProps +>; // Warning: (ae-missing-release-tag) "EntityPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -76,15 +74,10 @@ export interface EntityPickerUiOptions { defaultKind?: string; } -// Warning: (ae-missing-release-tag) "EntityTagsPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export const EntityTagsPicker: ( - props: FieldExtensionComponentProps, -) => JSX.Element; - -// @public -export const EntityTagsPickerFieldExtension: () => null; +export const EntityTagsPickerFieldExtension: FieldExtensionComponent< + FieldExtensionComponentProps +>; // Warning: (ae-missing-release-tag) "EntityTagsPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -153,17 +146,12 @@ export type LogEvent = { taskId: string; }; -// Warning: (ae-missing-release-tag) "OwnedEntityPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export const OwnedEntityPicker: ( - props: FieldExtensionComponentProps, -) => JSX.Element; - // Warning: (ae-missing-release-tag) "OwnedEntityPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const OwnedEntityPickerFieldExtension: () => null; +export const OwnedEntityPickerFieldExtension: FieldExtensionComponent< + FieldExtensionComponentProps +>; // Warning: (ae-missing-release-tag) "OwnedEntityPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -175,17 +163,12 @@ export interface OwnedEntityPickerUiOptions { defaultKind?: string; } -// Warning: (ae-missing-release-tag) "OwnerPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export const OwnerPicker: ( - props: FieldExtensionComponentProps, -) => JSX.Element; - // Warning: (ae-missing-release-tag) "OwnerPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const OwnerPickerFieldExtension: () => null; +export const OwnerPickerFieldExtension: FieldExtensionComponent< + FieldExtensionComponentProps +>; // Warning: (ae-missing-release-tag) "OwnerPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -206,17 +189,12 @@ export const repoPickerValidation: ( }, ) => void; -// Warning: (ae-missing-release-tag) "RepoUrlPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export const RepoUrlPicker: ( - props: FieldExtensionComponentProps, -) => JSX.Element; - // Warning: (ae-missing-release-tag) "RepoUrlPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const RepoUrlPickerFieldExtension: () => null; +export const RepoUrlPickerFieldExtension: FieldExtensionComponent< + FieldExtensionComponentProps +>; // Warning: (ae-missing-release-tag) "RepoUrlPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts b/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts index 0ba88d1277..7076221480 100644 --- a/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts @@ -13,5 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { EntityNamePicker } from './EntityNamePicker'; export { entityNamePickerValidation } from './validation'; diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/index.ts b/plugins/scaffolder/src/components/fields/EntityPicker/index.ts index eb7511bbef..891b5bef16 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityPicker/index.ts @@ -13,5 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { EntityPicker } from './EntityPicker'; export type { EntityPickerUiOptions } from './EntityPicker'; diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts b/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts index 3ba36a2409..9ff6e553a6 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts @@ -13,5 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { EntityTagsPicker } from './EntityTagsPicker'; export type { EntityTagsPickerUiOptions } from './EntityTagsPicker'; diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts index 125f6fdef0..2988ba8cdc 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts @@ -13,5 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { OwnedEntityPicker } from './OwnedEntityPicker'; export type { OwnedEntityPickerUiOptions } from './OwnedEntityPicker'; diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx index 9012ed149e..1824275380 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { EntityPicker } from '../EntityPicker'; +import { EntityPicker } from '../EntityPicker/EntityPicker'; import { FieldExtensionComponentProps } from '../../../extensions'; export interface OwnerPickerUiOptions { diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts index b8cb97eb97..aa26024b5b 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts @@ -13,5 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { OwnerPicker } from './OwnerPicker'; export type { OwnerPickerUiOptions } from './OwnerPicker'; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts index 2fdbf0aac9..c5f596a786 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts @@ -13,6 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { RepoUrlPicker } from './RepoUrlPicker'; export type { RepoUrlPickerUiOptions } from './RepoUrlPicker'; export { repoPickerValidation } from './validation'; diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index 10c0746e41..b3bb37065e 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -13,19 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityPicker } from '../components/fields/EntityPicker'; -import { - EntityNamePicker, - entityNamePickerValidation, -} from '../components/fields/EntityNamePicker'; -import { EntityTagsPicker } from '../components/fields/EntityTagsPicker'; -import { OwnerPicker } from '../components/fields/OwnerPicker'; -import { - repoPickerValidation, - RepoUrlPicker, -} from '../components/fields/RepoUrlPicker'; +import { EntityPicker } from '../components/fields/EntityPicker/EntityPicker'; +import { EntityNamePicker } from '../components/fields/EntityNamePicker/EntityNamePicker'; +import { entityNamePickerValidation } from '../components/fields/EntityNamePicker/validation'; +import { EntityTagsPicker } from '../components/fields/EntityTagsPicker/EntityTagsPicker'; +import { OwnerPicker } from '../components/fields/OwnerPicker/OwnerPicker'; +import { RepoUrlPicker } from '../components/fields/RepoUrlPicker/RepoUrlPicker'; +import { repoPickerValidation } from '../components/fields/RepoUrlPicker/validation'; import { FieldExtensionOptions } from './types'; -import { OwnedEntityPicker } from '../components/fields/OwnedEntityPicker'; +import { OwnedEntityPicker } from '../components/fields/OwnedEntityPicker/OwnedEntityPicker'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS: FieldExtensionOptions[] = [ diff --git a/plugins/scaffolder/src/extensions/index.tsx b/plugins/scaffolder/src/extensions/index.tsx index 53db4cd927..72df8d4df3 100644 --- a/plugins/scaffolder/src/extensions/index.tsx +++ b/plugins/scaffolder/src/extensions/index.tsx @@ -25,14 +25,14 @@ import { Extension, attachComponentData } from '@backstage/core-plugin-api'; export const FIELD_EXTENSION_WRAPPER_KEY = 'scaffolder.extensions.wrapper.v1'; export const FIELD_EXTENSION_KEY = 'scaffolder.extensions.field.v1'; +export type FieldExtensionComponent<_TInputProps> = () => null; + export function createScaffolderFieldExtension< TReturnValue = unknown, TInputProps = unknown, >( options: FieldExtensionOptions, - // TODO: need know how to embed these types nicely so the api report looks nice. - // then we can remove the export of the components -): Extension<() => null> { +): Extension> { return { expose() { const FieldExtensionDataHolder: any = () => null; diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index ccf8d2ee34..0d44a40c80 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -16,16 +16,12 @@ import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { scaffolderApiRef, ScaffolderClient } from './api'; -import { EntityPicker } from './components/fields/EntityPicker'; -import { - entityNamePickerValidation, - EntityNamePicker, -} from './components/fields/EntityNamePicker'; -import { OwnerPicker } from './components/fields/OwnerPicker'; -import { - repoPickerValidation, - RepoUrlPicker, -} from './components/fields/RepoUrlPicker'; +import { EntityPicker } from './components/fields/EntityPicker/EntityPicker'; +import { entityNamePickerValidation } from './components/fields/EntityNamePicker'; +import { EntityNamePicker } from './components/fields/EntityNamePicker/EntityNamePicker'; +import { OwnerPicker } from './components/fields/OwnerPicker/OwnerPicker'; +import { repoPickerValidation } from './components/fields/RepoUrlPicker'; +import { RepoUrlPicker } from './components/fields/RepoUrlPicker/RepoUrlPicker'; import { createScaffolderFieldExtension } from './extensions'; import { registerComponentRouteRef, rootRouteRef } from './routes'; import { @@ -35,8 +31,8 @@ import { discoveryApiRef, fetchApiRef, } from '@backstage/core-plugin-api'; -import { OwnedEntityPicker } from './components/fields/OwnedEntityPicker'; -import { EntityTagsPicker } from './components/fields/EntityTagsPicker'; +import { OwnedEntityPicker } from './components/fields/OwnedEntityPicker/OwnedEntityPicker'; +import { EntityTagsPicker } from './components/fields/EntityTagsPicker/EntityTagsPicker'; export const scaffolderPlugin = createPlugin({ id: 'scaffolder', From 86da51cec553f9bf86761d6240d1aef0f0c666d7 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 28 Feb 2022 16:21:42 +0100 Subject: [PATCH 027/353] ochore: added changeset Signed-off-by: blam --- .changeset/swift-roses-hug.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/swift-roses-hug.md diff --git a/.changeset/swift-roses-hug.md b/.changeset/swift-roses-hug.md new file mode 100644 index 0000000000..47a84e3247 --- /dev/null +++ b/.changeset/swift-roses-hug.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +**BREAKING**: Removing the exports of the raw components that back the `CustomFieldExtensions`. From 303063a655cc5dad3b59c45aa221bffecc41a83c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 28 Feb 2022 14:29:23 +0100 Subject: [PATCH 028/353] catalog-react: added MockStarredEntitiesApi + usage Signed-off-by: Patrik Oldsberg --- .changeset/silver-boxes-flash.md | 2 +- .../components/catalog/EntityPage.test.tsx | 14 +---- plugins/catalog-react/api-report.md | 8 +++ .../MockStarredEntitiesApi.test.ts | 61 +++++++++++++++++++ .../MockStarredEntitiesApi.ts | 54 ++++++++++++++++ .../src/apis/StarredEntitiesApi/index.ts | 1 + .../src/hooks/useEntityListProvider.test.tsx | 10 +-- .../src/hooks/useStarredEntities.test.tsx | 32 +++++----- .../CatalogPage/DefaultCatalogPage.test.tsx | 7 +-- .../CatalogTable/CatalogTable.test.tsx | 10 +-- .../EntityLayout/EntityLayout.test.tsx | 8 +-- .../StarredEntities/Content.test.tsx | 29 ++------- .../StarredEntities.stories.tsx | 11 +--- .../src/templates/DefaultTemplate.stories.tsx | 6 +- plugins/scaffolder/dev/index.tsx | 16 ++--- .../components/DefaultTechDocsHome.test.tsx | 4 +- 16 files changed, 168 insertions(+), 105 deletions(-) create mode 100644 plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.test.ts create mode 100644 plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.ts diff --git a/.changeset/silver-boxes-flash.md b/.changeset/silver-boxes-flash.md index 3b860a06d6..9f30c1a34b 100644 --- a/.changeset/silver-boxes-flash.md +++ b/.changeset/silver-boxes-flash.md @@ -2,7 +2,7 @@ '@backstage/plugin-catalog-react': patch --- -**BREAKING**: Moved **DefaultStarredEntitiesApi** to `@backstage/plugin-catalog`. If you were using this in tests, you can add `@backstage/plugin-catalog` your packages `devDependencies` instead. +**BREAKING**: Moved **DefaultStarredEntitiesApi** to `@backstage/plugin-catalog`. If you were using this in tests, you can use the new `MockStarredEntitiesApi` from `@backstage/plugin-catalog-react` instead. Fixed a risky behavior where `DefaultStarredEntitiesApi` forwarded values to observers that were later mutated. diff --git a/packages/app/src/components/catalog/EntityPage.test.tsx b/packages/app/src/components/catalog/EntityPage.test.tsx index 4608dc7533..68ce5fb497 100644 --- a/packages/app/src/components/catalog/EntityPage.test.tsx +++ b/packages/app/src/components/catalog/EntityPage.test.tsx @@ -14,19 +14,16 @@ * limitations under the License. */ -import { - EntityLayout, - DefaultStarredEntitiesApi, -} from '@backstage/plugin-catalog'; +import { EntityLayout } from '@backstage/plugin-catalog'; import { EntityProvider, starredEntitiesApiRef, + MockStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; import { githubActionsApiRef } from '@backstage/plugin-github-actions'; import { permissionApiRef } from '@backstage/plugin-permission-react'; import { MockPermissionApi, - MockStorageApi, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -61,12 +58,7 @@ describe('EntityPage Test', () => { diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index b8f0fa5538..3e1136313f 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -476,6 +476,14 @@ export const MockEntityListContextProvider: ({ value?: Partial> | undefined; }>) => JSX.Element; +// @public +export class MockStarredEntitiesApi implements StarredEntitiesApi { + // (undocumented) + starredEntitie$(): Observable>; + // (undocumented) + toggleStarred(entityRef: string): Promise; +} + // @public @deprecated (undocumented) export function reduceCatalogFilters( filters: EntityFilter[], diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.test.ts b/plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.test.ts new file mode 100644 index 0000000000..aadecefe98 --- /dev/null +++ b/plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { MockStarredEntitiesApi } from './MockStarredEntitiesApi'; + +describe('MockStarredEntitiesApi', () => { + it('should toggle starred entities', async () => { + const api = new MockStarredEntitiesApi(); + + const updates1 = new Array>(); + const sub1 = api + .starredEntitie$() + .subscribe(entities => updates1.push(entities)); + + api.toggleStarred('k:ns/e1'); + api.toggleStarred('k:ns/e2'); + + await Promise.resolve(); + expect(updates1).toEqual([ + new Set(), + new Set(['k:ns/e1']), + new Set(['k:ns/e1', 'k:ns/e2']), + ]); + + const updates2 = new Array>(); + const sub2 = api + .starredEntitie$() + .subscribe(entities => updates2.push(entities)); + + api.toggleStarred('k:ns/e2'); + sub1.unsubscribe(); + api.toggleStarred('k:ns/e2'); + + await Promise.resolve(); + expect(updates1).toEqual([ + new Set(), + new Set(['k:ns/e1']), + new Set(['k:ns/e1', 'k:ns/e2']), + new Set(['k:ns/e1']), + ]); + expect(updates2).toEqual([ + new Set(['k:ns/e1', 'k:ns/e2']), + new Set(['k:ns/e1']), + new Set(['k:ns/e1', 'k:ns/e2']), + ]); + sub2.unsubscribe(); + }); +}); diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.ts b/plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.ts new file mode 100644 index 0000000000..9467153dd9 --- /dev/null +++ b/plugins/catalog-react/src/apis/StarredEntitiesApi/MockStarredEntitiesApi.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { Observable } from '@backstage/types'; +import ObservableImpl from 'zen-observable'; +import { StarredEntitiesApi } from './StarredEntitiesApi'; + +/** + * An in-memory mock implementation of the StarredEntitiesApi. + * + * @public + */ +export class MockStarredEntitiesApi implements StarredEntitiesApi { + private readonly starredEntities = new Set(); + private readonly subscribers = new Set< + ZenObservable.SubscriptionObserver> + >(); + + private readonly observable = new ObservableImpl>(subscriber => { + subscriber.next(new Set(this.starredEntities)); + + this.subscribers.add(subscriber); + return () => { + this.subscribers.delete(subscriber); + }; + }); + + async toggleStarred(entityRef: string): Promise { + if (!this.starredEntities.delete(entityRef)) { + this.starredEntities.add(entityRef); + } + + for (const subscription of this.subscribers) { + subscription.next(new Set(this.starredEntities)); + } + } + + starredEntitie$(): Observable> { + return this.observable; + } +} diff --git a/plugins/catalog-react/src/apis/StarredEntitiesApi/index.ts b/plugins/catalog-react/src/apis/StarredEntitiesApi/index.ts index 94a221b36f..ba44c38e1d 100644 --- a/plugins/catalog-react/src/apis/StarredEntitiesApi/index.ts +++ b/plugins/catalog-react/src/apis/StarredEntitiesApi/index.ts @@ -16,3 +16,4 @@ export { starredEntitiesApiRef } from './StarredEntitiesApi'; export type { StarredEntitiesApi } from './StarredEntitiesApi'; +export { MockStarredEntitiesApi } from './MockStarredEntitiesApi'; diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index 5db9e2be9c..27fde81cc7 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -23,14 +23,13 @@ import { identityApiRef, storageApiRef, } from '@backstage/core-plugin-api'; -import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; import { MockStorageApi, TestApiProvider } from '@backstage/test-utils'; import { act, renderHook } from '@testing-library/react-hooks'; import qs from 'qs'; import React, { PropsWithChildren } from 'react'; import { MemoryRouter } from 'react-router'; import { catalogApiRef } from '../api'; -import { starredEntitiesApiRef } from '../apis'; +import { starredEntitiesApiRef, MockStarredEntitiesApi } from '../apis'; import { EntityKindPicker, UserListPicker } from '../components'; import { EntityKindFilter, EntityTypeFilter, UserListFilter } from '../filters'; import { UserListFilterKind } from '../types'; @@ -96,12 +95,7 @@ const wrapper = ({ [catalogApiRef, mockCatalogApi], [identityApiRef, mockIdentityApi], [storageApiRef, MockStorageApi.create()], - [ - starredEntitiesApiRef, - new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), - }), - ], + [starredEntitiesApiRef, new MockStarredEntitiesApi()], ]} > diff --git a/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx b/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx index 949ac6515b..16fa19a1cd 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx +++ b/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx @@ -15,16 +15,18 @@ */ import { Entity } from '@backstage/catalog-model'; -import { StorageApi } from '@backstage/core-plugin-api'; -import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; -import { MockStorageApi, TestApiProvider } from '@backstage/test-utils'; +import { TestApiProvider } from '@backstage/test-utils'; import { act, renderHook } from '@testing-library/react-hooks'; import React, { PropsWithChildren } from 'react'; -import { starredEntitiesApiRef } from '../apis'; +import { + starredEntitiesApiRef, + StarredEntitiesApi, + MockStarredEntitiesApi, +} from '../apis'; import { useStarredEntities } from './useStarredEntities'; describe('useStarredEntities', () => { - let mockStorage: StorageApi; + let mockApi: StarredEntitiesApi; let wrapper: React.ComponentType; const mockEntity: Entity = { @@ -45,22 +47,15 @@ describe('useStarredEntities', () => { }; beforeEach(() => { - mockStorage = MockStorageApi.create(); + mockApi = new MockStarredEntitiesApi(); wrapper = ({ children }: PropsWithChildren<{}>) => ( - + {children} ); }); - it('should return an empty set for when there is no items in storage', async () => { + it('should return an empty set', async () => { const { result, waitForNextUpdate } = renderHook( () => useStarredEntities(), { wrapper }, @@ -71,10 +66,11 @@ describe('useStarredEntities', () => { expect(result.current.starredEntities.size).toBe(0); }); - it('should return a set with the current items when there are items in storage', async () => { + it('should return a set with the current items', async () => { const expectedIds = ['i', 'am', 'some', 'test', 'ids']; - const store = mockStorage?.forBucket('starredEntities'); - await store?.set('entityRefs', expectedIds); + for (const id of expectedIds) { + mockApi.toggleStarred(id); + } const { result, waitForNextUpdate } = renderHook( () => useStarredEntities(), diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx index e36c1e9338..cf2eedb14c 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx @@ -31,6 +31,7 @@ import { catalogApiRef, entityRouteRef, starredEntitiesApiRef, + MockStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; import { mockBreakpoint, @@ -42,7 +43,6 @@ import { import DashboardIcon from '@material-ui/icons/Dashboard'; import { fireEvent, screen } from '@testing-library/react'; import React from 'react'; -import { DefaultStarredEntitiesApi } from '../../apis'; import { createComponentRouteRef } from '../../routes'; import { CatalogTableRow } from '../CatalogTable'; import { DefaultCatalogPage } from './DefaultCatalogPage'; @@ -141,10 +141,7 @@ describe('DefaultCatalogPage', () => { [catalogApiRef, catalogApi], [identityApiRef, identityApi], [storageApiRef, storageApi], - [ - starredEntitiesApiRef, - new DefaultStarredEntitiesApi({ storageApi }), - ], + [starredEntitiesApiRef, new MockStarredEntitiesApi()], ]} > {children} diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index 1d49638eed..cc05bbe2ae 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -25,15 +25,11 @@ import { MockEntityListContextProvider, starredEntitiesApiRef, UserListFilter, + MockStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; -import { - MockStorageApi, - renderInTestApp, - TestApiRegistry, -} from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { act, fireEvent } from '@testing-library/react'; import * as React from 'react'; -import { DefaultStarredEntitiesApi } from '../../apis'; import { CatalogTable } from './CatalogTable'; const entities: Entity[] = [ @@ -57,7 +53,7 @@ const entities: Entity[] = [ describe('CatalogTable component', () => { const mockApis = TestApiRegistry.from([ starredEntitiesApiRef, - new DefaultStarredEntitiesApi({ storageApi: MockStorageApi.create() }), + new MockStarredEntitiesApi(), ]); beforeEach(() => { diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx index b29261c28b..c1deaf4b74 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx @@ -24,18 +24,17 @@ import { EntityProvider, entityRouteRef, starredEntitiesApiRef, + MockStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; import { permissionApiRef } from '@backstage/plugin-permission-react'; import { MockPermissionApi, - MockStorageApi, renderInTestApp, TestApiRegistry, } from '@backstage/test-utils'; import { act, fireEvent } from '@testing-library/react'; import React from 'react'; import { Route, Routes } from 'react-router'; -import { DefaultStarredEntitiesApi } from '../../apis'; import { EntityLayout } from './EntityLayout'; const mockEntity = { @@ -48,10 +47,7 @@ const mockEntity = { const mockApis = TestApiRegistry.from( [catalogApiRef, {} as CatalogApi], [alertApiRef, {} as AlertApi], - [ - starredEntitiesApiRef, - new DefaultStarredEntitiesApi({ storageApi: MockStorageApi.create() }), - ], + [starredEntitiesApiRef, new MockStarredEntitiesApi()], [permissionApiRef, new MockPermissionApi()], ); diff --git a/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx b/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx index ae4a84473f..9087ed3605 100644 --- a/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx +++ b/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx @@ -13,40 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - renderInTestApp, - TestApiProvider, - MockStorageApi, -} from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { starredEntitiesApiRef, + MockStarredEntitiesApi, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; import React from 'react'; import { Content } from './Content'; describe('StarredEntitiesContent', () => { it('should render list of tools', async () => { - const mockStorageApi = MockStorageApi.create(); - await mockStorageApi - .forBucket('starredEntities') - .set('entityRefs', [ - 'component:default/mock-starred-entity', - 'component:default/mock-starred-entity-2', - ]); + const mockedApi = new MockStarredEntitiesApi(); + mockedApi.toggleStarred('component:default/mock-starred-entity'); + mockedApi.toggleStarred('component:default/mock-starred-entity-2'); const { getByText } = await renderInTestApp( - + , { diff --git a/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx b/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx index 55486a5dcd..315c0d0d16 100644 --- a/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx +++ b/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx @@ -22,9 +22,9 @@ import { } from '@backstage/test-utils'; import { starredEntitiesApiRef, + MockStarredEntitiesApi, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; import { Grid } from '@material-ui/core'; import React, { ComponentType } from 'react'; @@ -44,14 +44,7 @@ export default { (Story: ComponentType<{}>) => wrapInTestApp( , diff --git a/plugins/home/src/templates/DefaultTemplate.stories.tsx b/plugins/home/src/templates/DefaultTemplate.stories.tsx index 86d1b60022..5dc4ff61a0 100644 --- a/plugins/home/src/templates/DefaultTemplate.stories.tsx +++ b/plugins/home/src/templates/DefaultTemplate.stories.tsx @@ -25,9 +25,9 @@ import { wrapInTestApp, TestApiProvider, MockStorageApi} from '@backstage/test-u import { Content, Page, InfoCard } from '@backstage/core-components'; import { starredEntitiesApiRef, + MockStarredEntitiesApi, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; import { HomePageSearchBar, SearchContextProvider, @@ -57,9 +57,7 @@ export default { apis={[ [ starredEntitiesApiRef, - new DefaultStarredEntitiesApi({ - storageApi: mockStorageApi, - }), + new MockStarredEntitiesApi(), ], [searchApiRef, { query: () => Promise.resolve({ results: [] }) }], ]} diff --git a/plugins/scaffolder/dev/index.tsx b/plugins/scaffolder/dev/index.tsx index 6ecc1a4564..03632f434d 100644 --- a/plugins/scaffolder/dev/index.tsx +++ b/plugins/scaffolder/dev/index.tsx @@ -20,19 +20,13 @@ import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { catalogApiRef, starredEntitiesApiRef, + MockStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; import React from 'react'; import { scaffolderApiRef, ScaffolderClient } from '../src'; import { ScaffolderPage } from '../src/plugin'; -import { - discoveryApiRef, - fetchApiRef, - storageApiRef, -} from '@backstage/core-plugin-api'; -import { - CatalogEntityPage, - DefaultStarredEntitiesApi, -} from '@backstage/plugin-catalog'; +import { discoveryApiRef, fetchApiRef } from '@backstage/core-plugin-api'; +import { CatalogEntityPage } from '@backstage/plugin-catalog'; createDevApp() .addPage({ @@ -46,8 +40,8 @@ createDevApp() }) .registerApi({ api: starredEntitiesApiRef, - deps: { storageApi: storageApiRef }, - factory: ({ storageApi }) => new DefaultStarredEntitiesApi({ storageApi }), + deps: {}, + factory: () => new MockStarredEntitiesApi(), }) .registerApi({ api: scaffolderApiRef, diff --git a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx index 678c737547..33f5e0a08a 100644 --- a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx +++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx @@ -20,11 +20,11 @@ import { configApiRef, storageApiRef, } from '@backstage/core-plugin-api'; -import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; import { CatalogApi, catalogApiRef, starredEntitiesApiRef, + MockStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; import { MockStorageApi, @@ -73,7 +73,7 @@ describe('TechDocs Home', () => { [catalogApiRef, mockCatalogApi], [configApiRef, configApi], [storageApiRef, storageApi], - [starredEntitiesApiRef, new DefaultStarredEntitiesApi({ storageApi })], + [starredEntitiesApiRef, new MockStarredEntitiesApi()], ); it('should render a TechDocs home page', async () => { From 580210f5a9b4f5db58a4b2280f8be388ae778656 Mon Sep 17 00:00:00 2001 From: Andy Tan Date: Mon, 28 Feb 2022 09:21:40 -0800 Subject: [PATCH 029/353] update changeset to patch Signed-off-by: Andy Tan --- .changeset/mean-panthers-wonder.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/mean-panthers-wonder.md b/.changeset/mean-panthers-wonder.md index f055c4a064..1659c855f8 100644 --- a/.changeset/mean-panthers-wonder.md +++ b/.changeset/mean-panthers-wonder.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-scaffolder': minor +'@backstage/plugin-scaffolder': patch --- Render markdown for description in software templates From 0c9cf2822d77e27e3e2967b3e5a71d93b5bee3dd Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Mon, 28 Feb 2022 18:31:30 +0000 Subject: [PATCH 030/353] catalog-backend: mark permission-related exports as alpha Marks all strictly permission-related exports in catalog-backend as alpha. There are permission-related properties on other exports, so this doesn't entirely protect us from future breaking changes, but it will certainly reduce the number. Signed-off-by: Mike Lewis --- .changeset/chilled-dolls-agree.md | 10 ++++++++++ plugins/catalog-backend/api-report.md | 8 ++++---- .../src/permissions/conditionExports.ts | 6 ++++-- plugins/catalog-backend/src/permissions/rules/index.ts | 3 ++- plugins/catalog-backend/src/permissions/rules/util.ts | 2 +- 5 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 .changeset/chilled-dolls-agree.md diff --git a/.changeset/chilled-dolls-agree.md b/.changeset/chilled-dolls-agree.md new file mode 100644 index 0000000000..0468bdfee0 --- /dev/null +++ b/.changeset/chilled-dolls-agree.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Mark permission-related exports as alpha. This means that the exports below should now be imported from `@backstage/plugin-catalog-backend/alpha` instead of `@backstage/plugin-catalog-backend`. + +- `catalogConditions` +- `createCatalogPolicyDecision` +- `permissionRules` +- `createCatalogPermissionRule` diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 9b4db418b3..5ae8a76d19 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -213,7 +213,7 @@ export class CatalogBuilder { setRefreshIntervalSeconds(seconds: number): CatalogBuilder; } -// @public +// @alpha export const catalogConditions: Conditions<{ hasAnnotation: PermissionRule< Entity, @@ -394,12 +394,12 @@ export class CodeOwnersProcessor implements CatalogProcessor { preProcessEntity(entity: Entity, location: LocationSpec): Promise; } -// @public +// @alpha export const createCatalogPermissionRule: ( rule: PermissionRule, ) => PermissionRule; -// @public +// @alpha export const createCatalogPolicyDecision: ( conditions: PermissionCriteria>, ) => ConditionalPolicyDecision; @@ -896,7 +896,7 @@ export function parseEntityYaml( location: LocationSpec, ): Iterable; -// @public +// @alpha export const permissionRules: { hasAnnotation: PermissionRule< Entity, diff --git a/plugins/catalog-backend/src/permissions/conditionExports.ts b/plugins/catalog-backend/src/permissions/conditionExports.ts index 28e74c53c2..2846c8c3ec 100644 --- a/plugins/catalog-backend/src/permissions/conditionExports.ts +++ b/plugins/catalog-backend/src/permissions/conditionExports.ts @@ -27,7 +27,8 @@ const conditionExports = createConditionExports({ /** * These conditions are used when creating conditional decisions that are returned * by authorization policies. - * @public + * + * @alpha */ export const catalogConditions = conditionExports.conditions; @@ -50,7 +51,8 @@ export const catalogConditions = conditionExports.conditions; * } * } * ``` - * @public + * + * @alpha */ export const createCatalogPolicyDecision = conditionExports.createPolicyDecision; diff --git a/plugins/catalog-backend/src/permissions/rules/index.ts b/plugins/catalog-backend/src/permissions/rules/index.ts index 4eec796c74..f9d192781e 100644 --- a/plugins/catalog-backend/src/permissions/rules/index.ts +++ b/plugins/catalog-backend/src/permissions/rules/index.ts @@ -24,7 +24,8 @@ import { hasSpec } from './hasSpec'; /** * These permission rules can be used to conditionally filter catalog entities * or describe a user's access to the entities. - * @public + * + * @alpha */ export const permissionRules = { hasAnnotation, diff --git a/plugins/catalog-backend/src/permissions/rules/util.ts b/plugins/catalog-backend/src/permissions/rules/util.ts index 7ef0ca3546..a1316c7fb2 100644 --- a/plugins/catalog-backend/src/permissions/rules/util.ts +++ b/plugins/catalog-backend/src/permissions/rules/util.ts @@ -23,7 +23,7 @@ import { EntitiesSearchFilter } from '../../catalog/types'; * {@link @backstage/plugin-permission-node#PermissionRule}s for the * catalog-backend. * - * @public + * @alpha */ export const createCatalogPermissionRule = makeCreatePermissionRule< Entity, From abc91a6843ea0f4807644694625efbc75269d2f5 Mon Sep 17 00:00:00 2001 From: Marcus Crane Date: Tue, 1 Mar 2022 09:13:12 +1300 Subject: [PATCH 031/353] Update Postgres tutorial SSL block to match Postgres app config template Signed-off-by: Marcus Crane --- docs/tutorials/switching-sqlite-postgres.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/switching-sqlite-postgres.md b/docs/tutorials/switching-sqlite-postgres.md index fafd2ad586..8a7f90b21f 100644 --- a/docs/tutorials/switching-sqlite-postgres.md +++ b/docs/tutorials/switching-sqlite-postgres.md @@ -43,9 +43,11 @@ backend: + user: ${POSTGRES_USER} + password: ${POSTGRES_PASSWORD} + # https://node-postgres.com/features/ssl -+ #ssl: require # see https://www.postgresql.org/docs/current/libpq-ssl.html Table 33.1. SSL Mode Descriptions (e.g. require) -+ #ca: # if you have a CA file and want to verify it you can uncomment this section -+ #$file: /ca/server.crt ++ # you can set the sslmode configuration option via the `PGSSLMODE` environment variable ++ # see https://www.postgresql.org/docs/current/libpq-ssl.html Table 33.1. SSL Mode Descriptions (e.g. require) ++ # ssl: ++ # ca: # if you have a CA file and want to verify it you can uncomment this section ++ # $file: /ca/server.crt ``` If you have an `app-config.local.yaml` for local development, a similar update From 55031596f23c5ef65462594f4f23fc4755924c57 Mon Sep 17 00:00:00 2001 From: Nikolas Skoufis Date: Tue, 1 Mar 2022 09:05:59 +1100 Subject: [PATCH 032/353] Prettify how to Signed-off-by: Nikolas Skoufis --- docs/features/techdocs/how-to-guides.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index 5ec8aac307..5a5fe36526 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -566,6 +566,7 @@ To achieve this hybrid build model: a given entity. For example, to only build docs when an entity has the `company.com/techdocs-builder` annotation set to `'local'`: + ```typescript export class AnnotationBasedBuildStrategy { private readonly config: Config; @@ -575,10 +576,14 @@ To achieve this hybrid build model: } async shouldBuild(_: Entity): Promise { - return this.entity.metadata?.annotations?.["company.com/techdocs-builder"] === 'local' + return ( + this.entity.metadata?.annotations?.['company.com/techdocs-builder'] === + 'local' + ); } } ``` + 4. Pass an instance of this Build Strategy as the `docsBuildStrategy` parameter of the TechDocs backend `createRouter` method. From 588445b175d70a40810fbaa5724226254d833bf7 Mon Sep 17 00:00:00 2001 From: Nikolas Skoufis Date: Tue, 1 Mar 2022 09:18:44 +1100 Subject: [PATCH 033/353] Exporting interface and adding docs This makes the api reporter happy, with good reason Signed-off-by: Nikolas Skoufis --- plugins/techdocs-backend/api-report.md | 10 ++++++---- plugins/techdocs-backend/src/index.ts | 1 + .../techdocs-backend/src/service/DocsBuildStrategy.ts | 5 +++++ plugins/techdocs-backend/src/service/index.ts | 1 + 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/plugins/techdocs-backend/api-report.md b/plugins/techdocs-backend/api-report.md index 28700b245b..3ce3a2504b 100644 --- a/plugins/techdocs-backend/api-report.md +++ b/plugins/techdocs-backend/api-report.md @@ -42,6 +42,12 @@ export class DefaultTechDocsCollator implements DocumentCollator { readonly visibilityPermission: Permission; } +// @public +export interface DocsBuildStrategy { + // (undocumented) + shouldBuild(entity: Entity): Promise; +} + // @public export type OutOfTheBoxDeploymentOptions = { preparers: PreparerBuilder; @@ -84,8 +90,4 @@ export type TechDocsCollatorOptions = { export { TechDocsDocument }; export * from '@backstage/techdocs-common'; - -// Warnings were encountered during analysis: -// -// src/service/router.d.ts:24:5 - (ae-forgotten-export) The symbol "DocsBuildStrategy" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/techdocs-backend/src/index.ts b/plugins/techdocs-backend/src/index.ts index 12e7df1e9e..2a17bf2736 100644 --- a/plugins/techdocs-backend/src/index.ts +++ b/plugins/techdocs-backend/src/index.ts @@ -25,6 +25,7 @@ export type { RouterOptions, RecommendedDeploymentOptions, OutOfTheBoxDeploymentOptions, + DocsBuildStrategy, } from './service'; export { DefaultTechDocsCollator } from './search'; diff --git a/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts b/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts index 69b0474a85..2e8cc52d3e 100644 --- a/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts +++ b/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts @@ -16,6 +16,11 @@ import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; +/** + * A strategy for when to build TechDocs locally, and when to skip building TechDocs (allowing for an external build) + * + * @public + */ export interface DocsBuildStrategy { shouldBuild(entity: Entity): Promise; } diff --git a/plugins/techdocs-backend/src/service/index.ts b/plugins/techdocs-backend/src/service/index.ts index 0065e33a2a..29db2d2038 100644 --- a/plugins/techdocs-backend/src/service/index.ts +++ b/plugins/techdocs-backend/src/service/index.ts @@ -20,3 +20,4 @@ export type { RecommendedDeploymentOptions, OutOfTheBoxDeploymentOptions, } from './router'; +export type { DocsBuildStrategy } from './DocsBuildStrategy'; From 7a22a78180fffc4a1c82f27367344ffc10c6c800 Mon Sep 17 00:00:00 2001 From: Nik Skoufis Date: Tue, 1 Mar 2022 09:05:09 +1100 Subject: [PATCH 034/353] Apply suggestions from code review Co-authored-by: Emma Indal Signed-off-by: Nikolas Skoufis --- plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts b/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts index 71e5377a03..029d8d223f 100644 --- a/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts +++ b/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * Copyright 2022 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 5adcf26b0513d6958d736ab4117c810415093772 Mon Sep 17 00:00:00 2001 From: Nikolas Skoufis Date: Tue, 1 Mar 2022 09:23:47 +1100 Subject: [PATCH 035/353] Refactor to a private constructor and static fromConfig Signed-off-by: Nikolas Skoufis --- .../techdocs-backend/src/service/DocsBuildStrategy.test.ts | 6 ++++-- plugins/techdocs-backend/src/service/DocsBuildStrategy.ts | 6 +++++- plugins/techdocs-backend/src/service/router.ts | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts b/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts index 029d8d223f..76b70e67e9 100644 --- a/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts +++ b/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts @@ -41,7 +41,8 @@ describe('DefaultDocsBuildStrategy', () => { describe('shouldBuild', () => { it('should return true when techdocs.build is set to local', async () => { - const defaultDocsBuildStrategy = new DefaultDocsBuildStrategy(config); + const defaultDocsBuildStrategy = + DefaultDocsBuildStrategy.fromConfig(config); MockedConfigReader.prototype.getString.mockReturnValue('local'); @@ -51,7 +52,8 @@ describe('DefaultDocsBuildStrategy', () => { }); it('should return false when techdocs.build is set to external', async () => { - const defaultDocsBuildStrategy = new DefaultDocsBuildStrategy(config); + const defaultDocsBuildStrategy = + DefaultDocsBuildStrategy.fromConfig(config); MockedConfigReader.prototype.getString.mockReturnValue('external'); diff --git a/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts b/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts index 2e8cc52d3e..e0220d7a32 100644 --- a/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts +++ b/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts @@ -28,10 +28,14 @@ export interface DocsBuildStrategy { export class DefaultDocsBuildStrategy { private readonly config: Config; - constructor(config: Config) { + private constructor(config: Config) { this.config = config; } + static fromConfig(config: Config): DefaultDocsBuildStrategy { + return new DefaultDocsBuildStrategy(config); + } + async shouldBuild(_: Entity): Promise { return this.config.getString('techdocs.builder') === 'local'; } diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index d620020245..4d51ebd485 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -106,7 +106,7 @@ export async function createRouter( const { publisher, config, logger, discovery } = options; const catalogClient = new CatalogClient({ discoveryApi: discovery }); const docsBuildStrategy = - options.docsBuildStrategy ?? new DefaultDocsBuildStrategy(config); + options.docsBuildStrategy ?? DefaultDocsBuildStrategy.fromConfig(config); // Entities are cached to optimize the /static/docs request path, which can be called many times // when loading a single techdocs page. From 1543dcf31a51f206b51567ca7e2f3a902e2bfa58 Mon Sep 17 00:00:00 2001 From: Hasan Oezdemir <21654050+nodify-at@users.noreply.github.com> Date: Tue, 1 Mar 2022 00:08:35 +0100 Subject: [PATCH 036/353] (feature): remove integration router and use catalog resource type since we use the same resources required by the catalog entity Signed-off-by: Hasan Oezdemir <21654050+nodify-at@users.noreply.github.com> --- plugins/jenkins-backend/api-report.md | 3 ++ plugins/jenkins-backend/package.json | 4 ++ .../src/permissions/conditionExports.ts | 4 +- .../permissions/permission-router-factory.ts | 40 ------------------- .../src/service/jenkinsApi.test.ts | 40 ++++++++++++++++++- .../jenkins-backend/src/service/jenkinsApi.ts | 26 +++++++++++- plugins/jenkins-backend/src/service/router.ts | 24 ++++------- plugins/jenkins-common/api-report.md | 3 -- plugins/jenkins-common/package.json | 1 + plugins/jenkins-common/src/permissions.ts | 8 +--- yarn.lock | 1 - 11 files changed, 84 insertions(+), 70 deletions(-) delete mode 100644 plugins/jenkins-backend/src/permissions/permission-router-factory.ts diff --git a/plugins/jenkins-backend/api-report.md b/plugins/jenkins-backend/api-report.md index 6f9ec10cf4..41bebb1061 100644 --- a/plugins/jenkins-backend/api-report.md +++ b/plugins/jenkins-backend/api-report.md @@ -12,6 +12,7 @@ import { Entity } from '@backstage/catalog-model'; import { EntityName } from '@backstage/catalog-model'; import express from 'express'; import { Logger as Logger_2 } from 'winston'; +import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; import { PermissionCondition } from '@backstage/plugin-permission-common'; import { PermissionCriteria } from '@backstage/plugin-permission-common'; import { PermissionRule } from '@backstage/plugin-permission-node'; @@ -140,5 +141,7 @@ export interface RouterOptions { jenkinsInfoProvider: JenkinsInfoProvider; // (undocumented) logger: Logger_2; + // (undocumented) + permissions?: PermissionAuthorizer; } ``` diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index 72ebc1ee2b..f0659f42d8 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -29,8 +29,12 @@ "@backstage/catalog-client": "^0.7.2", "@backstage/catalog-model": "^0.11.0", "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/plugin-auth-node": "^0.1.3", "@backstage/plugin-catalog-backend": "^0.22.0", + "@backstage/plugin-catalog-common": "^0.1.4", "@backstage/plugin-jenkins-common": "^0.1.0", + "@backstage/plugin-permission-common": "^0.5.1", "@backstage/plugin-permission-node": "^0.5.2", "@types/express": "^4.17.6", "express": "^4.17.1", diff --git a/plugins/jenkins-backend/src/permissions/conditionExports.ts b/plugins/jenkins-backend/src/permissions/conditionExports.ts index 5266b1b091..197f2cb0ec 100644 --- a/plugins/jenkins-backend/src/permissions/conditionExports.ts +++ b/plugins/jenkins-backend/src/permissions/conditionExports.ts @@ -15,11 +15,11 @@ */ import { jenkinsPermissionRules } from './rules'; import { createConditionExports } from '@backstage/plugin-permission-node'; -import { RESOURCE_TYPE_JENKINS } from '@backstage/plugin-jenkins-common'; +import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common'; const { conditions, createPolicyDecision } = createConditionExports({ pluginId: 'jenkins', - resourceType: RESOURCE_TYPE_JENKINS, + resourceType: RESOURCE_TYPE_CATALOG_ENTITY, rules: jenkinsPermissionRules, }); diff --git a/plugins/jenkins-backend/src/permissions/permission-router-factory.ts b/plugins/jenkins-backend/src/permissions/permission-router-factory.ts deleted file mode 100644 index 1959eb24a7..0000000000 --- a/plugins/jenkins-backend/src/permissions/permission-router-factory.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * 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 { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; -import { jenkinsPermissionRules } from './rules'; -import { parseEntityRef } from '@backstage/catalog-model'; -import type { PluginEndpointDiscovery } from '@backstage/backend-common'; -import { CatalogClient } from '@backstage/catalog-client'; -import { RESOURCE_TYPE_JENKINS } from '@backstage/plugin-jenkins-common'; - -export const jenkinsPermissionIntegrationRouterFactory = ( - discoveryApi: PluginEndpointDiscovery, - fetchApi?: { fetch: typeof fetch }, -) => { - const catalogApi = new CatalogClient({ discoveryApi, fetchApi: fetchApi }); - return createPermissionIntegrationRouter({ - resourceType: RESOURCE_TYPE_JENKINS, - rules: Object.values(jenkinsPermissionRules), - getResources: resourceRefs => { - const entities = resourceRefs.map(async resourceRef => { - const { kind, namespace, name } = parseEntityRef(resourceRef); - return catalogApi.getEntityByName({ kind, name, namespace }); - }); - // combine the promises to return entities as any array, getResources expects a single promise with all entities - return Promise.all(entities); - }, - }); -}; diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts index 683b9334bc..350ec996ca 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts @@ -18,6 +18,8 @@ import { JenkinsApiImpl } from './jenkinsApi'; import jenkins from 'jenkins'; import { JenkinsInfo } from './jenkinsInfoProvider'; import { JenkinsBuild, JenkinsProject } from '../types'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; +import { NotAllowedError } from '@backstage/errors'; jest.mock('jenkins'); const mockedJenkinsClient = { @@ -40,8 +42,16 @@ const jenkinsInfo: JenkinsInfo = { jobFullName: 'example-jobName', }; +const fakePermissionApi = { + authorize: jest.fn().mockResolvedValue([ + { + result: AuthorizeResult.ALLOW, + }, + ]), +}; + describe('JenkinsApi', () => { - const jenkinsApi = new JenkinsApiImpl(); + const jenkinsApi = new JenkinsApiImpl(fakePermissionApi); describe('getProjects', () => { const project: JenkinsProject = { @@ -413,6 +423,34 @@ describe('JenkinsApi', () => { expect(mockedJenkinsClient.job.build).toBeCalledWith(jobFullName); }); + it('buildProject should fail if it does not have required permissions', async () => { + fakePermissionApi.authorize.mockResolvedValueOnce([ + { + result: AuthorizeResult.DENY, + }, + ]); + + await expect(() => + jenkinsApi.buildProject(jenkinsInfo, jobFullName), + ).rejects.toThrow(NotAllowedError); + }); + + it('buildProject should success if it have required permissions', async () => { + fakePermissionApi.authorize.mockResolvedValueOnce([ + { + result: AuthorizeResult.ALLOW, + }, + ]); + + await jenkinsApi.buildProject(jenkinsInfo, jobFullName); + expect(mockedJenkins).toHaveBeenCalledWith({ + baseUrl: jenkinsInfo.baseUrl, + headers: jenkinsInfo.headers, + promisify: true, + }); + expect(mockedJenkinsClient.job.build).toBeCalledWith(jobFullName); + }); + it('buildProject with crumbIssuer option', async () => { const info: JenkinsInfo = { ...jenkinsInfo, crumbIssuer: true }; await jenkinsApi.buildProject(info, jobFullName); diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.ts b/plugins/jenkins-backend/src/service/jenkinsApi.ts index 3dbba13db6..c0b1c63112 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.ts @@ -23,6 +23,12 @@ import { JenkinsProject, ScmDetails, } from '../types'; +import { + AuthorizeResult, + PermissionAuthorizer, +} from '@backstage/plugin-permission-common'; +import { jenkinsExecutePermission } from '@backstage/plugin-jenkins-common'; +import { NotAllowedError } from '@backstage/errors'; export class JenkinsApiImpl { private static readonly lastBuildTreeSpec = `lastBuild[ @@ -58,6 +64,8 @@ export class JenkinsApiImpl { ${JenkinsApiImpl.jobTreeSpec} ]{0,50}`; + constructor(private readonly permissionApi?: PermissionAuthorizer) {} + /** * Get a list of projects for the given JenkinsInfo. * @see ../../../jenkins/src/api/JenkinsApi.ts#getProjects @@ -128,9 +136,25 @@ export class JenkinsApiImpl { * Trigger a build of a project * @see ../../../jenkins/src/api/JenkinsApi.ts#retry */ - async buildProject(jenkinsInfo: JenkinsInfo, jobFullName: string) { + async buildProject( + jenkinsInfo: JenkinsInfo, + jobFullName: string, + options?: { token?: string }, + ) { const client = await JenkinsApiImpl.getClient(jenkinsInfo); + if (this.permissionApi) { + const response = await this.permissionApi.authorize( + [{ permission: jenkinsExecutePermission }], + { token: options?.token }, + ); + // permission api returns always at least one item, we need to check only one result since we do not expect any additional results + const { result } = response[0]; + if (result === AuthorizeResult.DENY) { + throw new NotAllowedError(); + } + } + // looks like the current SDK only supports triggering a new build // can't see any support for replay (re-running the specific build with the same SCM info) diff --git a/plugins/jenkins-backend/src/service/router.ts b/plugins/jenkins-backend/src/service/router.ts index 85320bb520..89d755efed 100644 --- a/plugins/jenkins-backend/src/service/router.ts +++ b/plugins/jenkins-backend/src/service/router.ts @@ -24,7 +24,8 @@ import { Logger } from 'winston'; import { JenkinsInfoProvider } from './jenkinsInfoProvider'; import { JenkinsApiImpl } from './jenkinsApi'; import { Config } from '@backstage/config'; -import { jenkinsPermissionIntegrationRouterFactory } from '../permissions/permission-router-factory'; +import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; +import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; export interface RouterOptions { logger: Logger; @@ -32,6 +33,7 @@ export interface RouterOptions { config?: Config; discovery?: PluginEndpointDiscovery; fetchApi?: { fetch: typeof fetch }; + permissions?: PermissionAuthorizer; } export async function createRouter( @@ -39,7 +41,7 @@ export async function createRouter( ): Promise { const { jenkinsInfoProvider } = options; - const jenkinsApi = new JenkinsApiImpl(); + const jenkinsApi = new JenkinsApiImpl(options.permissions); const router = Router(); router.use(express.json()); @@ -111,7 +113,6 @@ export async function createRouter( '/v1/entity/:namespace/:kind/:name/job/:jobFullName/:buildNumber::rebuild', async (request, response) => { const { namespace, kind, name, jobFullName } = request.params; - const jenkinsInfo = await jenkinsInfoProvider.getInstance({ entityRef: { kind, @@ -120,23 +121,14 @@ export async function createRouter( }, jobFullName, }); + const token = getBearerTokenFromAuthorizationHeader( + request.header('authorization'), + ); - await jenkinsApi.buildProject(jenkinsInfo, jobFullName); + await jenkinsApi.buildProject(jenkinsInfo, jobFullName, { token }); response.json({}); }, ); router.use(errorHandler()); - - if (options.config?.getOptionalBoolean('permission.enabled')) { - if (!options.discovery) { - throw new Error('Discovery API is required if permissions are enabled.'); - } - router.use( - jenkinsPermissionIntegrationRouterFactory( - options.discovery, - options.fetchApi, - ), - ); - } return router; } diff --git a/plugins/jenkins-common/api-report.md b/plugins/jenkins-common/api-report.md index b63cc94374..7b0d3759ba 100644 --- a/plugins/jenkins-common/api-report.md +++ b/plugins/jenkins-common/api-report.md @@ -8,8 +8,5 @@ import { Permission } from '@backstage/plugin-permission-common'; // @public export const jenkinsExecutePermission: Permission; -// @public (undocumented) -export const RESOURCE_TYPE_JENKINS = 'jenkins'; - // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/jenkins-common/package.json b/plugins/jenkins-common/package.json index 5739794197..08f12960bc 100644 --- a/plugins/jenkins-common/package.json +++ b/plugins/jenkins-common/package.json @@ -22,6 +22,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { + "@backstage/plugin-catalog-common": "^0.1.4", "@backstage/plugin-permission-common": "^0.5.1" }, "devDependencies": { diff --git a/plugins/jenkins-common/src/permissions.ts b/plugins/jenkins-common/src/permissions.ts index f521e0454b..2fa8c14232 100644 --- a/plugins/jenkins-common/src/permissions.ts +++ b/plugins/jenkins-common/src/permissions.ts @@ -13,13 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common'; import { Permission } from '@backstage/plugin-permission-common'; -/** - * @public - */ -export const RESOURCE_TYPE_JENKINS = 'jenkins'; - /** * This permission is used to determine if a user is allowed to execute an action in jenkins plugin * @@ -30,5 +26,5 @@ export const jenkinsExecutePermission: Permission = { attributes: { action: 'update', }, - resourceType: RESOURCE_TYPE_JENKINS, + resourceType: RESOURCE_TYPE_CATALOG_ENTITY, }; diff --git a/yarn.lock b/yarn.lock index 17489d9bb1..997e55c9e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11929,7 +11929,6 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: "@backstage/plugin-graphiql" "^0.2.32" "@backstage/plugin-home" "^0.4.16" "@backstage/plugin-jenkins" "^0.6.0" - "@backstage/plugin-jenkins-common" "^0.1.0" "@backstage/plugin-kafka" "^0.3.0" "@backstage/plugin-kubernetes" "^0.6.0" "@backstage/plugin-lighthouse" "^0.3.0" From 40ef5d33d3cf9d05e7bb95d4aa750dedb07721ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 04:10:40 +0000 Subject: [PATCH 037/353] chore(deps): bump @types/react-sparklines from 1.7.0 to 1.7.2 Bumps [@types/react-sparklines](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-sparklines) from 1.7.0 to 1.7.2. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-sparklines) --- updated-dependencies: - dependency-name: "@types/react-sparklines" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index a6b27ffa82..ab169bb33a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6236,9 +6236,9 @@ redux "^4.0.0" "@types/react-sparklines@^1.7.0": - version "1.7.0" - resolved "https://registry.npmjs.org/@types/react-sparklines/-/react-sparklines-1.7.0.tgz#f956d0f7b0e746ad445ce1cd250fe81f8a384684" - integrity sha512-Vd+cME7+Yy3kFNhnid9EBIKiyCQ/at8nqDczIs0UYfIB8AtaRJPqekigv02biOsIbQCvxyvIAIjiTKOC+hHNbA== + version "1.7.2" + resolved "https://registry.npmjs.org/@types/react-sparklines/-/react-sparklines-1.7.2.tgz#c14e80623abd3669a10f18d13f6fb9fbdc322f70" + integrity sha512-N1GwO7Ri5C5fE8+CxhiDntuSw1qYdGytBuedKrCxWpaojXm4WnfygbdBdc5sXGX7feMxDXBy9MNhxoUTwrMl4A== dependencies: "@types/react" "*" From 9b4b73719e09d22121e8ec5f1d923cc05684afc1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 04:11:05 +0000 Subject: [PATCH 038/353] chore(deps): bump postcss from 8.4.6 to 8.4.7 Bumps [postcss](https://github.com/postcss/postcss) from 8.4.6 to 8.4.7. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.6...8.4.7) --- updated-dependencies: - dependency-name: postcss dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/yarn.lock b/yarn.lock index a6b27ffa82..2e31950dd5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18139,15 +18139,10 @@ nanoclone@^0.2.1: resolved "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== -nanoid@^3.1.23: - version "3.2.0" - resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" - integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== - -nanoid@^3.2.0: - version "3.3.0" - resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.0.tgz#5906f776fd886c66c24f3653e0c46fcb1d4ad6b0" - integrity sha512-JzxqqT5u/x+/KOFSd7JP15DOo9nOoHpx6DYatqIHUW2+flybkm+mdcraotSQR5WcnZr+qhGVh8Ted0KdfSMxlg== +nanoid@^3.1.23, nanoid@^3.3.1: + version "3.3.1" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" + integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== nanomatch@^1.2.9: version "1.2.13" @@ -20142,11 +20137,11 @@ postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^ integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== postcss@^8.1.0, postcss@^8.4.5: - version "8.4.6" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.6.tgz#c5ff3c3c457a23864f32cb45ac9b741498a09ae1" - integrity sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA== + version "8.4.7" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.7.tgz#f99862069ec4541de386bf57f5660a6c7a0875a8" + integrity sha512-L9Ye3r6hkkCeOETQX6iOaWZgjp3LL6Lpqm6EtgbKrgqGGteRMNb9vzBfRL96YOSu8o7x3MfIH9Mo5cPJFGrW6A== dependencies: - nanoid "^3.2.0" + nanoid "^3.3.1" picocolors "^1.0.0" source-map-js "^1.0.2" From ea29f9279892bc54c044ea065085b544c0d1c921 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 04:11:36 +0000 Subject: [PATCH 039/353] chore(deps): bump apollo-server from 3.6.1 to 3.6.3 Bumps [apollo-server](https://github.com/apollographql/apollo-server/tree/HEAD/packages/apollo-server) from 3.6.1 to 3.6.3. - [Release notes](https://github.com/apollographql/apollo-server/releases) - [Changelog](https://github.com/apollographql/apollo-server/blob/main/CHANGELOG.md) - [Commits](https://github.com/apollographql/apollo-server/commits/apollo-server@3.6.3/packages/apollo-server) --- updated-dependencies: - dependency-name: apollo-server dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/yarn.lock b/yarn.lock index a6b27ffa82..df8c58bcab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7284,7 +7284,7 @@ apollo-server-caching@^3.3.0: dependencies: lru-cache "^6.0.0" -apollo-server-core@^3.6.1, apollo-server-core@^3.6.3: +apollo-server-core@^3.6.3: version "3.6.3" resolved "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-3.6.3.tgz#6b12ffa1af8bc8799930f72360090834915033d1" integrity sha512-TFJmAlI6vPp1MHOSXqYkE6leAyMekWv/D/3ma11uETkcd3EPjERGmxtTXPJElMVEkOK9BEElYKthCrH7bjYLuw== @@ -7322,7 +7322,7 @@ apollo-server-errors@^3.3.1: resolved "https://registry.npmjs.org/apollo-server-errors/-/apollo-server-errors-3.3.1.tgz#ba5c00cdaa33d4cbd09779f8cb6f47475d1cd655" integrity sha512-xnZJ5QWs6FixHICXHxUfm+ZWqqxrNuPlQ+kj5m6RtEgIpekOPssH/SD9gf2B4HuWV0QozorrygwZnux8POvyPA== -apollo-server-express@^3.0.0, apollo-server-express@^3.6.1: +apollo-server-express@^3.0.0, apollo-server-express@^3.6.3: version "3.6.3" resolved "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-3.6.3.tgz#5daf58bf0bdf0107ded7cd52c7e6ce6cd32c8b44" integrity sha512-3CjahZ+n+1T7pHH1qW1B6Ns0BzwOMeupAp2u0+M8ruOmE/e7VKn0OSOQQckZ8Z2AcWxWeno9K89fIv3PoSYgYA== @@ -7356,12 +7356,12 @@ apollo-server-types@^3.5.1: apollo-server-env "^4.2.1" apollo-server@^3.0.0: - version "3.6.1" - resolved "https://registry.npmjs.org/apollo-server/-/apollo-server-3.6.1.tgz#29420b1c0cddbf2e18147a3ca7299485f17137a2" - integrity sha512-Y2MY2/WvaTiofVoIR5ZIYt6c6wX8klZRaXI9x+7JBiFV9HMcOuLLpU3+P4r2EVXuN1LLe82m1PgiAYr+a1OmQg== + version "3.6.3" + resolved "https://registry.npmjs.org/apollo-server/-/apollo-server-3.6.3.tgz#0ba0ddb2835ccf27056d20b6f5b83b0ce9545a79" + integrity sha512-kNvOiDNkIaO+MsfR9v40Vz4ArlDdc9VwVKGJy5dniLW9AoDa/tSF99m8ItfGoMypqlRPMgrNGxkMuToBnvYXNQ== dependencies: - apollo-server-core "^3.6.1" - apollo-server-express "^3.6.1" + apollo-server-core "^3.6.3" + apollo-server-express "^3.6.3" express "^4.17.1" aproba@^1.0.3: From ff0a16fb1ad5bb072c96e4e83f1e2859d611faad Mon Sep 17 00:00:00 2001 From: Jussi Hallila Date: Tue, 15 Feb 2022 17:01:46 +0100 Subject: [PATCH 040/353] Add techdocs-core plugin to techdocs builder automatically Currently users need to modify their mkdocs files to container techdocs-core plugin manually when they want to expose techdocs in Backstage. To have a standardized set of extensions without the need to modify existing files (and possibly pollute alternative/additional mkdocs pipelines) this should be added automatically. This PR adds a helper function to modify the mkdocs file to contain this plugin if it doesn't exist in the config file. Co-authored-by: @emmaindal Signed-off-by: Jussi Hallila --- .changeset/tiny-jobs-hunt.md | 5 + docs/features/techdocs/cli.md | 2 + docs/features/techdocs/configuration.md | 5 + .../techdocs/creating-and-publishing.md | 3 + .../src/commands/generate/generate.ts | 4 + packages/techdocs-cli/src/commands/index.ts | 5 + .../mkdocs_with_additional_plugins.yml | 6 + .../mkdocs_with_techdocs_plugin.yml | 5 + .../__fixtures__/mkdocs_without_plugins.yml | 3 + .../src/stages/generate/helpers.test.ts | 69 +++++++- .../src/stages/generate/helpers.ts | 97 +--------- .../src/stages/generate/mkDocsPatchers.ts | 166 ++++++++++++++++++ .../src/stages/generate/techdocs.ts | 13 +- .../src/stages/generate/types.ts | 1 + 14 files changed, 286 insertions(+), 98 deletions(-) create mode 100644 .changeset/tiny-jobs-hunt.md create mode 100644 packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_additional_plugins.yml create mode 100644 packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_techdocs_plugin.yml create mode 100644 packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_without_plugins.yml create mode 100644 packages/techdocs-common/src/stages/generate/mkDocsPatchers.ts diff --git a/.changeset/tiny-jobs-hunt.md b/.changeset/tiny-jobs-hunt.md new file mode 100644 index 0000000000..8cf9db20b8 --- /dev/null +++ b/.changeset/tiny-jobs-hunt.md @@ -0,0 +1,5 @@ +--- +'@backstage/techdocs-common': patch +--- + +Modify techdocs builder to automatically append techdocs-core plugin to mkdocs.yaml file if it is missing. Adds an optional configuration item if this plugin needs to be omitted. diff --git a/docs/features/techdocs/cli.md b/docs/features/techdocs/cli.md index a5dc8c3c54..2a11e1181a 100644 --- a/docs/features/techdocs/cli.md +++ b/docs/features/techdocs/cli.md @@ -130,6 +130,8 @@ Options: if not found. --etag A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json. + --omitTechdocsCoreMkdocsPlugin An option to disable automatic addition of techdocs-core plugin to the mkdocs.yaml files. + Defaults to false, which means that the techdocs-core plugin is always added to the mkdocs file. -v --verbose Enable verbose output. (default: false) -h, --help display help for command ``` diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index 6317ae365b..129aa8957e 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -37,6 +37,11 @@ techdocs: pullImage: true + mkdocs: + # (Optional) techdocs.generator.omitTechdocsCoreMkdocsPlugin can be used to disable automatic addition of techdocs-core plugin to the mkdocs.yaml files. + # Defaults to false, which means that the techdocs-core plugin is always added to the mkdocs file. + omitTechdocsCorePlugin: false + # techdocs.builder can be either 'local' or 'external. # If builder is set to 'local' and you open a TechDocs page, techdocs-backend will try to generate the docs, publish to storage # and show the generated docs afterwords. This is the "Basic" setup of the TechDocs Architecture. diff --git a/docs/features/techdocs/creating-and-publishing.md b/docs/features/techdocs/creating-and-publishing.md index 383c3ae6d2..f63ff43b7f 100644 --- a/docs/features/techdocs/creating-and-publishing.md +++ b/docs/features/techdocs/creating-and-publishing.md @@ -79,6 +79,9 @@ plugins: - techdocs-core ``` +> Note - The plugins section above is optional. Backstage automatically adds the `techdocs-core` plugin to the +> mkdocs file if it is missing. This functionality can be turned off with a [configuration option](./configuration.md) in Backstage. + Update your component's entity description by adding the following lines to its `catalog-info.yaml` in the root of its repository: diff --git a/packages/techdocs-cli/src/commands/generate/generate.ts b/packages/techdocs-cli/src/commands/generate/generate.ts index a255805139..0f5213df34 100644 --- a/packages/techdocs-cli/src/commands/generate/generate.ts +++ b/packages/techdocs-cli/src/commands/generate/generate.ts @@ -39,6 +39,7 @@ export default async function generate(cmd: Command) { const sourceDir = resolve(cmd.sourceDir); const outputDir = resolve(cmd.outputDir); + const omitTechdocsCorePlugin = cmd.omitTechdocsCoreMkdocsPlugin; const dockerImage = cmd.dockerImage; const pullImage = cmd.pull; @@ -55,6 +56,9 @@ export default async function generate(cmd: Command) { runIn: cmd.docker ? 'docker' : 'local', dockerImage, pullImage, + mkdocs: { + omitTechdocsCorePlugin, + }, }, }, }); diff --git a/packages/techdocs-cli/src/commands/index.ts b/packages/techdocs-cli/src/commands/index.ts index 115fe8e364..6f1d9847e1 100644 --- a/packages/techdocs-cli/src/commands/index.ts +++ b/packages/techdocs-cli/src/commands/index.ts @@ -54,6 +54,11 @@ export function registerCommands(program: CommanderStatic) { 'A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json.', ) .option('-v --verbose', 'Enable verbose output.', false) + .option( + '--omitTechdocsCoreMkdocsPlugin', + "Don't patch MkDocs file automatically with techdocs-core plugin.", + false, + ) .alias('build') .action(lazy(() => import('./generate/generate').then(m => m.default))); diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_additional_plugins.yml b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_additional_plugins.yml new file mode 100644 index 0000000000..09e8fd7ac7 --- /dev/null +++ b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_additional_plugins.yml @@ -0,0 +1,6 @@ +site_name: Test site name +site_description: Test site description +docs_dir: docs/ +plugins: + - not-techdocs-core + - also-not-techdocs-core diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_techdocs_plugin.yml b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_techdocs_plugin.yml new file mode 100644 index 0000000000..eea9a8a3d9 --- /dev/null +++ b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_techdocs_plugin.yml @@ -0,0 +1,5 @@ +site_name: Test site name +site_description: Test site description +# This is a comment that is removed after editing +plugins: + - techdocs-core diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_without_plugins.yml b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_without_plugins.yml new file mode 100644 index 0000000000..e75b06ada7 --- /dev/null +++ b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_without_plugins.yml @@ -0,0 +1,3 @@ +site_name: Test site name +site_description: Test site description +docs_dir: docs/ diff --git a/packages/techdocs-common/src/stages/generate/helpers.test.ts b/packages/techdocs-common/src/stages/generate/helpers.test.ts index 9323ab4f18..7590df94fb 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.test.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.test.ts @@ -28,10 +28,14 @@ import { getMkdocsYml, getRepoUrlFromLocationAnnotation, patchIndexPreBuild, - patchMkdocsYmlPreBuild, storeEtagMetadata, validateMkdocsYaml, } from './helpers'; +import { + patchMkdocsYmlPreBuild, + pathMkdocsYmlWithTechdocsPlugin, +} from './mkDocsPatchers'; +import yaml from 'js-yaml'; const mockEntity = { apiVersion: 'version', @@ -65,6 +69,15 @@ const mkdocsYmlWithInvalidDocDir2 = fs.readFileSync( const mkdocsYmlWithComments = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_with_comments.yml'), ); +const mkdocsYmlWithTechdocsPlugins = fs.readFileSync( + resolvePath(__filename, '../__fixtures__/mkdocs_with_techdocs_plugin.yml'), +); +const mkdocsYmlWithoutPlugins = fs.readFileSync( + resolvePath(__filename, '../__fixtures__/mkdocs_without_plugins.yml'), +); +const mkdocsYmlWithAdditionalPlugins = fs.readFileSync( + resolvePath(__filename, '../__fixtures__/mkdocs_with_additional_plugins.yml'), +); const mockLogger = getVoidLogger(); const warn = jest.spyOn(mockLogger, 'warn'); @@ -289,6 +302,60 @@ describe('helpers', () => { }); }); + describe('pathMkdocsYmlWithTechdocsPlugin', () => { + beforeEach(() => { + mockFs({ + '/mkdocs_with_techdocs_plugin.yml': mkdocsYmlWithTechdocsPlugins, + '/mkdocs_without_plugins.yml': mkdocsYmlWithoutPlugins, + '/mkdocs_with_additional_plugins.yml': mkdocsYmlWithAdditionalPlugins, + }); + }); + it('should not add additional plugins if techdocs exists already in mkdocs file', async () => { + await pathMkdocsYmlWithTechdocsPlugin( + '/mkdocs_with_techdocs_plugin.yml', + mockLogger, + ); + + const updatedMkdocsYml = await fs.readFile( + '/mkdocs_with_techdocs_plugin.yml', + ); + const parsedYml = yaml.load(updatedMkdocsYml.toString()) as { + plugins: string[]; + }; + expect(parsedYml.plugins).toHaveLength(1); + expect(parsedYml.plugins).toContain('techdocs-core'); + }); + it("should add the needed plugin if it doesn't exist in mkdocs file", async () => { + await pathMkdocsYmlWithTechdocsPlugin( + '/mkdocs_without_plugins.yml', + mockLogger, + ); + + const updatedMkdocsYml = await fs.readFile('/mkdocs_without_plugins.yml'); + const parsedYml = yaml.load(updatedMkdocsYml.toString()) as { + plugins: string[]; + }; + expect(parsedYml.plugins).toHaveLength(1); + expect(parsedYml.plugins).toContain('techdocs-core'); + }); + it('should not override existing plugins', async () => { + await pathMkdocsYmlWithTechdocsPlugin( + '/mkdocs_with_additional_plugins.yml', + mockLogger, + ); + const updatedMkdocsYml = await fs.readFile( + '/mkdocs_with_additional_plugins.yml', + ); + const parsedYml = yaml.load(updatedMkdocsYml.toString()) as { + plugins: string[]; + }; + expect(parsedYml.plugins).toHaveLength(3); + expect(parsedYml.plugins).toContain('techdocs-core'); + expect(parsedYml.plugins).toContain('not-techdocs-core'); + expect(parsedYml.plugins).toContain('also-not-techdocs-core'); + }); + }); + describe('patchIndexPreBuild', () => { afterEach(() => { warn.mockClear(); diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/packages/techdocs-common/src/stages/generate/helpers.ts index 2debd5822c..349783d2bd 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.ts @@ -125,7 +125,7 @@ class UnknownTag { constructor(public readonly data: any, public readonly type?: string) {} } -const MKDOCS_SCHEMA = DEFAULT_SCHEMA.extend([ +export const MKDOCS_SCHEMA = DEFAULT_SCHEMA.extend([ new Type('', { kind: 'scalar', multi: true, @@ -203,101 +203,6 @@ export const validateMkdocsYaml = async ( return parsedMkdocsYml.docs_dir; }; -/** - * Update the mkdocs.yml file before TechDocs generator uses it to generate docs site. - * - * List of tasks: - * - Add repo_url or edit_uri if it does not exists - * If mkdocs.yml has a repo_url, the generated docs site gets an Edit button on the pages by default. - * If repo_url is missing in mkdocs.yml, we will use techdocs annotation of the entity to possibly get - * the repository URL. - * - * This function will not throw an error since this is not critical to the whole TechDocs pipeline. - * Instead it will log warnings if there are any errors in reading, parsing or writing YAML. - * - * @param mkdocsYmlPath - Absolute path to mkdocs.yml or equivalent of a docs site - * @param logger - A logger instance - * @param parsedLocationAnnotation - Object with location url and type - * @param scmIntegrations - the scmIntegration to do url transformations - */ -export const patchMkdocsYmlPreBuild = async ( - mkdocsYmlPath: string, - logger: Logger, - parsedLocationAnnotation: ParsedLocationAnnotation, - scmIntegrations: ScmIntegrationRegistry, -) => { - // We only want to override the mkdocs.yml if it has actually changed. This is relevant if - // used with a 'dir' location on the file system as this would permanently update the file. - let didEdit = false; - - let mkdocsYmlFileString; - try { - mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); - } catch (error) { - assertError(error); - logger.warn( - `Could not read MkDocs YAML config file ${mkdocsYmlPath} before running the generator: ${error.message}`, - ); - return; - } - - let mkdocsYml: any; - try { - mkdocsYml = yaml.load(mkdocsYmlFileString, { schema: MKDOCS_SCHEMA }); - - // mkdocsYml should be an object type after successful parsing. - // But based on its type definition, it can also be a string or undefined, which we don't want. - if (typeof mkdocsYml === 'string' || typeof mkdocsYml === 'undefined') { - throw new Error('Bad YAML format.'); - } - } catch (error) { - assertError(error); - logger.warn( - `Error in parsing YAML at ${mkdocsYmlPath} before running the generator. ${error.message}`, - ); - return; - } - - // Add edit_uri and/or repo_url to mkdocs.yml if it is missing. - // This will enable the Page edit button generated by MkDocs. - // If the either has been set, keep the original value - if (!('repo_url' in mkdocsYml) && !('edit_uri' in mkdocsYml)) { - const result = getRepoUrlFromLocationAnnotation( - parsedLocationAnnotation, - scmIntegrations, - mkdocsYml.docs_dir, - ); - - if (result.repo_url || result.edit_uri) { - mkdocsYml.repo_url = result.repo_url; - mkdocsYml.edit_uri = result.edit_uri; - didEdit = true; - - logger.info( - `Set ${JSON.stringify( - result, - )}. You can disable this feature by manually setting 'repo_url' or 'edit_uri' according to the MkDocs documentation at https://www.mkdocs.org/user-guide/configuration/#repo_url`, - ); - } - } - - try { - if (didEdit) { - await fs.writeFile( - mkdocsYmlPath, - yaml.dump(mkdocsYml, { schema: MKDOCS_SCHEMA }), - 'utf8', - ); - } - } catch (error) { - assertError(error); - logger.warn( - `Could not write to ${mkdocsYmlPath} after updating it before running the generator. ${error.message}`, - ); - return; - } -}; - /** * Update docs/index.md file before TechDocs generator uses it to generate docs site, * falling back to docs/README.md or README.md in case a default docs/index.md diff --git a/packages/techdocs-common/src/stages/generate/mkDocsPatchers.ts b/packages/techdocs-common/src/stages/generate/mkDocsPatchers.ts new file mode 100644 index 0000000000..d03b5d83c2 --- /dev/null +++ b/packages/techdocs-common/src/stages/generate/mkDocsPatchers.ts @@ -0,0 +1,166 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Logger } from 'winston'; +import fs from 'fs-extra'; +import yaml from 'js-yaml'; +import { ParsedLocationAnnotation } from '../../helpers'; +import { getRepoUrlFromLocationAnnotation, MKDOCS_SCHEMA } from './helpers'; +import { assertError } from '@backstage/errors'; +import { ScmIntegrationRegistry } from '@backstage/integration'; + +type MkDocsObject = { + plugins?: string[]; + docs_dir: string; + repo_url?: string; + edit_uri?: string; +}; + +const patchMkdocsFile = async ( + mkdocsYmlPath: string, + logger: Logger, + updateAction: (mkdocsYml: MkDocsObject) => boolean, +) => { + // We only want to override the mkdocs.yml if it has actually changed. This is relevant if + // used with a 'dir' location on the file system as this would permanently update the file. + let didEdit = false; + + let mkdocsYmlFileString; + try { + mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); + } catch (error) { + assertError(error); + logger.warn( + `Could not read MkDocs YAML config file ${mkdocsYmlPath} before running the generator: ${error.message}`, + ); + return; + } + + let mkdocsYml: any; + try { + mkdocsYml = yaml.load(mkdocsYmlFileString, { schema: MKDOCS_SCHEMA }); + + // mkdocsYml should be an object type after successful parsing. + // But based on its type definition, it can also be a string or undefined, which we don't want. + if (typeof mkdocsYml === 'string' || typeof mkdocsYml === 'undefined') { + throw new Error('Bad YAML format.'); + } + } catch (error) { + assertError(error); + logger.warn( + `Error in parsing YAML at ${mkdocsYmlPath} before running the generator. ${error.message}`, + ); + return; + } + + didEdit = updateAction(mkdocsYml); + + try { + if (didEdit) { + await fs.writeFile( + mkdocsYmlPath, + yaml.dump(mkdocsYml, { schema: MKDOCS_SCHEMA }), + 'utf8', + ); + } + } catch (error) { + assertError(error); + logger.warn( + `Could not write to ${mkdocsYmlPath} after updating it before running the generator. ${error.message}`, + ); + return; + } +}; + +/** + * Update the mkdocs.yml file before TechDocs generator uses it to generate docs site. + * + * List of tasks: + * - Add repo_url or edit_uri if it does not exists + * If mkdocs.yml has a repo_url, the generated docs site gets an Edit button on the pages by default. + * If repo_url is missing in mkdocs.yml, we will use techdocs annotation of the entity to possibly get + * the repository URL. + * + * This function will not throw an error since this is not critical to the whole TechDocs pipeline. + * Instead it will log warnings if there are any errors in reading, parsing or writing YAML. + * + * @param mkdocsYmlPath - Absolute path to mkdocs.yml or equivalent of a docs site + * @param logger - A logger instance + * @param parsedLocationAnnotation - Object with location url and type + * @param scmIntegrations - the scmIntegration to do url transformations + */ +export const patchMkdocsYmlPreBuild = async ( + mkdocsYmlPath: string, + logger: Logger, + parsedLocationAnnotation: ParsedLocationAnnotation, + scmIntegrations: ScmIntegrationRegistry, +) => { + await patchMkdocsFile(mkdocsYmlPath, logger, mkdocsYml => { + if (!('repo_url' in mkdocsYml) && !('edit_uri' in mkdocsYml)) { + // Add edit_uri and/or repo_url to mkdocs.yml if it is missing. + // This will enable the Page edit button generated by MkDocs. + // If the either has been set, keep the original value + const result = getRepoUrlFromLocationAnnotation( + parsedLocationAnnotation, + scmIntegrations, + mkdocsYml.docs_dir, + ); + + if (result.repo_url || result.edit_uri) { + mkdocsYml.repo_url = result.repo_url; + mkdocsYml.edit_uri = result.edit_uri; + + logger.info( + `Set ${JSON.stringify( + result, + )}. You can disable this feature by manually setting 'repo_url' or 'edit_uri' according to the MkDocs documentation at https://www.mkdocs.org/user-guide/configuration/#repo_url`, + ); + return true; + } + } + return false; + }); +}; + +/** + * Update the mkdocs.yml file before TechDocs generator uses it to generate docs site. + * + * List of tasks: + * - Add techdocs-core plugin to mkdocs file if it doesn't exist + * + * This function will not throw an error since this is not critical to the whole TechDocs pipeline. + * Instead it will log warnings if there are any errors in reading, parsing or writing YAML. + * + * @param mkdocsYmlPath - Absolute path to mkdocs.yml or equivalent of a docs site + * @param logger - A logger instance + */ +export const pathMkdocsYmlWithTechdocsPlugin = async ( + mkdocsYmlPath: string, + logger: Logger, +) => { + await patchMkdocsFile(mkdocsYmlPath, logger, mkdocsYml => { + // Modify mkdocs.yaml to contain the needed techdocs-core plugin if it is not there + if (!('plugins' in mkdocsYml)) { + mkdocsYml.plugins = ['techdocs-core']; + return true; + } + + if (mkdocsYml.plugins && !mkdocsYml.plugins.includes('techdocs-core')) { + mkdocsYml.plugins.push('techdocs-core'); + return true; + } + return false; + }); +}; diff --git a/packages/techdocs-common/src/stages/generate/techdocs.ts b/packages/techdocs-common/src/stages/generate/techdocs.ts index baa2e008a1..b46f52f99a 100644 --- a/packages/techdocs-common/src/stages/generate/techdocs.ts +++ b/packages/techdocs-common/src/stages/generate/techdocs.ts @@ -26,11 +26,15 @@ import { createOrUpdateMetadata, getMkdocsYml, patchIndexPreBuild, - patchMkdocsYmlPreBuild, runCommand, storeEtagMetadata, validateMkdocsYaml, } from './helpers'; + +import { + patchMkdocsYmlPreBuild, + pathMkdocsYmlWithTechdocsPlugin, +} from './mkDocsPatchers'; import { GeneratorBase, GeneratorConfig, @@ -110,6 +114,10 @@ export class TechdocsGenerator implements GeneratorBase { await patchIndexPreBuild({ inputDir, logger: childLogger, docsDir }); } + if (!this.options.omitTechdocsCoreMkdocsPlugin) { + await pathMkdocsYmlWithTechdocsPlugin(mkdocsYmlPath, childLogger); + } + // Directories to bind on container const mountDirs = { [inputDir]: '/input', @@ -207,5 +215,8 @@ export function readGeneratorConfig( 'docker', dockerImage: config.getOptionalString('techdocs.generator.dockerImage'), pullImage: config.getOptionalBoolean('techdocs.generator.pullImage'), + omitTechdocsCoreMkdocsPlugin: config.getOptionalBoolean( + 'techdocs.generator.mkdocs.omitTechdocsCorePlugin', + ), }; } diff --git a/packages/techdocs-common/src/stages/generate/types.ts b/packages/techdocs-common/src/stages/generate/types.ts index 2ff3064991..f46dbdc56a 100644 --- a/packages/techdocs-common/src/stages/generate/types.ts +++ b/packages/techdocs-common/src/stages/generate/types.ts @@ -39,6 +39,7 @@ export type GeneratorConfig = { runIn: GeneratorRunInType; dockerImage?: string; pullImage?: boolean; + omitTechdocsCoreMkdocsPlugin?: boolean; }; /** From 44a1a447cc37100cd6e175308e8b77b92f8f7a71 Mon Sep 17 00:00:00 2001 From: Nikolas Skoufis Date: Tue, 1 Mar 2022 19:28:53 +1100 Subject: [PATCH 041/353] Fix up links to other pages Signed-off-by: Nikolas Skoufis --- docs/features/techdocs/concepts.md | 2 +- docs/features/techdocs/how-to-guides.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/features/techdocs/concepts.md b/docs/features/techdocs/concepts.md index f8e813d89e..e34f58bc70 100644 --- a/docs/features/techdocs/concepts.md +++ b/docs/features/techdocs/concepts.md @@ -62,7 +62,7 @@ However any logic that satisfies the Build Strategy interface can be implemented config as well as the entity being processed to make a decision. For an example of how the Build Strategy can be used to implement a 'hybrid' build model, refer to -the [How to implement a hybrid build strategy](./how-to-guides#how-to-implement-a-hybrid-build-strategy) guide. +the [How to implement a hybrid build strategy](./how-to-guides.md#how-to-implement-a-hybrid-build-strategy) guide. ## TechDocs Container diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index 5a5fe36526..56ca2252d0 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -541,7 +541,7 @@ Done! Now you have a support of the following diagrams along with mermaid: ## How to implement a hybrid build strategy -One limitation of the [Recommended deployment](./architecture#recommended-deployment) is that +One limitation of the [Recommended deployment](./architecture.md#recommended-deployment) is that the experience for users requires modifying their CI/CD process to publish their TechDocs. For some users, this may be unnecessary, and provides a barrier to entry for onboarding users to Backstage. However, a purely local TechDocs @@ -549,7 +549,7 @@ build restricts TechDocs creators to using the tooling provided in Backstage, as well as the plugins and features provided in the Backstage-included `mkdocs` installation. -To accommodate both of these use-cases, users can implement a custom [Build Strategy](./concepts#techdocs-build-strategy) +To accommodate both of these use-cases, users can implement a custom [Build Strategy](./concepts.md#techdocs-build-strategy) with logic to encode which TechDocs should be built locally, and which will be built externally. From 2d5783aa5b24ce465df0e58ccb70af6e33a41d87 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 1 Mar 2022 09:44:44 +0100 Subject: [PATCH 042/353] chore: sort out the exporting Signed-off-by: blam --- plugins/scaffolder/src/extensions/index.tsx | 4 ++-- plugins/scaffolder/src/index.ts | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/plugins/scaffolder/src/extensions/index.tsx b/plugins/scaffolder/src/extensions/index.tsx index 72df8d4df3..75a166beff 100644 --- a/plugins/scaffolder/src/extensions/index.tsx +++ b/plugins/scaffolder/src/extensions/index.tsx @@ -25,14 +25,14 @@ import { Extension, attachComponentData } from '@backstage/core-plugin-api'; export const FIELD_EXTENSION_WRAPPER_KEY = 'scaffolder.extensions.wrapper.v1'; export const FIELD_EXTENSION_KEY = 'scaffolder.extensions.field.v1'; -export type FieldExtensionComponent<_TInputProps> = () => null; +export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null; export function createScaffolderFieldExtension< TReturnValue = unknown, TInputProps = unknown, >( options: FieldExtensionOptions, -): Extension> { +): Extension> { return { expose() { const FieldExtensionDataHolder: any = () => null; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 71cd1d8f84..9776549091 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -40,11 +40,7 @@ export { createScaffolderFieldExtension, ScaffolderFieldExtensions, } from './extensions'; -export type { - CustomFieldValidator, - FieldExtensionOptions, - FieldExtensionComponentProps, -} from './extensions'; + export { EntityPickerFieldExtension, EntityNamePickerFieldExtension, @@ -56,4 +52,5 @@ export { scaffolderPlugin, } from './plugin'; export * from './components'; +export * from './extensions'; export type { TaskPageProps } from './components/TaskPage'; From 5f489049693d6b0f5525b7cb8cd1460b8438e160 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 1 Mar 2022 09:44:46 +0100 Subject: [PATCH 043/353] scripts: Update list-deprecations script to use new release tags Signed-off-by: Johan Haals --- scripts/list-deprecations.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/list-deprecations.js b/scripts/list-deprecations.js index 9a159927fc..8047158138 100755 --- a/scripts/list-deprecations.js +++ b/scripts/list-deprecations.js @@ -57,9 +57,7 @@ class ReleaseProvider { ); // Filter out just the releases - const releases = tagOutput - .split('\n') - .filter(l => l.startsWith('release-')); + const releases = tagOutput.split('\n').filter(l => l.startsWith('v')); // Then find the earliest release that affected our package for (const release of releases) { From bb2bb3665190bb19f8a5da2d01c00c43c1a29da8 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 1 Mar 2022 09:28:50 +0100 Subject: [PATCH 044/353] core-plugin-api: Remove deprecated get method from StorageApi Signed-off-by: Johan Haals --- .changeset/new-foxes-matter.md | 7 ++++ .changeset/two-lobsters-hammer.md | 6 ++++ .../StorageApi/WebStorage.test.ts | 28 ++++++--------- .../implementations/StorageApi/WebStorage.ts | 2 +- .../DismissableBanner.test.tsx | 2 +- .../DismissableBanner/DismissableBanner.tsx | 8 ++--- packages/core-plugin-api/api-report.md | 8 ----- .../src/apis/definitions/StorageApi.ts | 19 ---------- packages/test-utils/api-report.md | 2 -- .../apis/StorageApi/MockStorageApi.test.ts | 35 ++++++++----------- .../apis/StorageApi/MockStorageApi.ts | 8 ----- .../DefaultStarredEntitiesApi.ts | 4 +-- .../apis/StarredEntitiesApi/migration.test.ts | 20 ++++++----- .../src/apis/StarredEntitiesApi/migration.ts | 6 ++-- 14 files changed, 60 insertions(+), 95 deletions(-) create mode 100644 .changeset/new-foxes-matter.md create mode 100644 .changeset/two-lobsters-hammer.md diff --git a/.changeset/new-foxes-matter.md b/.changeset/new-foxes-matter.md new file mode 100644 index 0000000000..234dd78451 --- /dev/null +++ b/.changeset/new-foxes-matter.md @@ -0,0 +1,7 @@ +--- +'@backstage/core-app-api': minor +'@backstage/core-plugin-api': minor +'@backstage/test-utils': minor +--- + +**BREAKING**: Removed the deprecated `get` method from `StorageAPI` and its implementations, this method has been replaced by the `snapshot` method. The return value from snapshot no longer includes `newValue` which has been replaced by `value`. For getting notified when a value changes, use `observe$`. diff --git a/.changeset/two-lobsters-hammer.md b/.changeset/two-lobsters-hammer.md new file mode 100644 index 0000000000..412901fb41 --- /dev/null +++ b/.changeset/two-lobsters-hammer.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-catalog-react': patch +--- + +Updated usage of `StorageApi` to use `snapshot` method instead of `get` diff --git a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts index d81cba48d2..2f82ed5e88 100644 --- a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts +++ b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts @@ -33,12 +33,11 @@ describe('WebStorage Storage API', () => { it('should return undefined for values which are unset', async () => { const storage = createWebStorage(); - expect(storage.get('myfakekey')).toBeUndefined(); + expect(storage.snapshot('myfakekey').value).toBeUndefined(); expect(storage.snapshot('myfakekey')).toEqual({ key: 'myfakekey', presence: 'absent', value: undefined, - newValue: undefined, }); }); @@ -48,26 +47,23 @@ describe('WebStorage Storage API', () => { await storage.set('myfakekey', 'helloimastring'); await storage.set('mysecondfakekey', 1234); await storage.set('mythirdfakekey', true); - expect(storage.get('myfakekey')).toBe('helloimastring'); - expect(storage.get('mysecondfakekey')).toBe(1234); - expect(storage.get('mythirdfakekey')).toBe(true); + expect(storage.snapshot('myfakekey').value).toBe('helloimastring'); + expect(storage.snapshot('mysecondfakekey').value).toBe(1234); + expect(storage.snapshot('mythirdfakekey').value).toBe(true); expect(storage.snapshot('myfakekey')).toEqual({ key: 'myfakekey', presence: 'present', value: 'helloimastring', - newValue: 'helloimastring', }); expect(storage.snapshot('mysecondfakekey')).toEqual({ key: 'mysecondfakekey', presence: 'present', value: 1234, - newValue: 1234, }); expect(storage.snapshot('mythirdfakekey')).toEqual({ key: 'mythirdfakekey', presence: 'present', value: true, - newValue: true, }); }); @@ -81,12 +77,11 @@ describe('WebStorage Storage API', () => { await storage.set('myfakekey', mockData); - expect(storage.get('myfakekey')).toEqual(mockData); + expect(storage.snapshot('myfakekey').value).toEqual(mockData); expect(storage.snapshot('myfakekey')).toEqual({ key: 'myfakekey', presence: 'present', value: mockData, - newValue: mockData, }); }); @@ -118,7 +113,6 @@ describe('WebStorage Storage API', () => { key: 'correctKey', presence: 'present', value: mockData, - newValue: mockData, }); }); @@ -152,7 +146,6 @@ describe('WebStorage Storage API', () => { key: 'correctKey', presence: 'absent', value: undefined, - newValue: undefined, }); }); @@ -166,9 +159,11 @@ describe('WebStorage Storage API', () => { await firstStorage.set(keyName, 'boop'); await secondStorage.set(keyName, 'deerp'); - expect(firstStorage.get(keyName)).not.toBe(secondStorage.get(keyName)); - expect(firstStorage.get(keyName)).toBe('boop'); - expect(secondStorage.get(keyName)).toBe('deerp'); + expect(firstStorage.snapshot(keyName)).not.toBe( + secondStorage.snapshot(keyName), + ); + expect(firstStorage.snapshot(keyName).value).toBe('boop'); + expect(secondStorage.snapshot(keyName).value).toBe('deerp'); expect(firstStorage.snapshot(keyName)).not.toEqual( secondStorage.snapshot(keyName), ); @@ -176,13 +171,11 @@ describe('WebStorage Storage API', () => { key: keyName, presence: 'present', value: 'boop', - newValue: 'boop', }); expect(secondStorage.snapshot(keyName)).toEqual({ key: keyName, presence: 'present', value: 'deerp', - newValue: 'deerp', }); }); @@ -217,7 +210,6 @@ describe('WebStorage Storage API', () => { key: 'key', presence: 'absent', value: undefined, - newValue: undefined, }); expect(mockErrorApi.post).toHaveBeenCalledWith(expect.any(Error)); expect(mockErrorApi.post).toHaveBeenCalledWith( diff --git a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts index c4cffaf184..ac3d3f20f2 100644 --- a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts +++ b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.ts @@ -65,7 +65,7 @@ export class WebStorage implements StorageApi { new Error(`Error when parsing JSON config from storage for: ${key}`), ); } - return { key, value, newValue: value, presence }; + return { key, value, presence }; } forBucket(name: string): WebStorage { diff --git a/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx b/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx index e62b3fa82d..3348c278ee 100644 --- a/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx +++ b/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx @@ -75,7 +75,7 @@ describe('', () => { ); fireEvent.click(button); const dismissedBanners = - notifications?.get('dismissedBanners') ?? []; + notifications?.snapshot('dismissedBanners').value ?? []; expect( dismissedBanners.includes('catalog_page_welcome_banner'), ).toBeTruthy(); diff --git a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx index 566b326cb5..9ff7616c45 100644 --- a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx +++ b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx @@ -101,7 +101,7 @@ export const DismissableBanner = (props: Props) => { const storageApi = useApi(storageApiRef); const notificationsStore = storageApi.forBucket('notifications'); const rawDismissedBanners = - notificationsStore.get('dismissedBanners') ?? []; + notificationsStore.snapshot('dismissedBanners').value ?? []; const [dismissedBanners, setDismissedBanners] = useState( new Set(rawDismissedBanners), @@ -112,11 +112,11 @@ export const DismissableBanner = (props: Props) => { ); useEffect(() => { - if (observedItems?.newValue) { - const currentValue = observedItems?.newValue ?? []; + if (observedItems?.value) { + const currentValue = observedItems?.value ?? []; setDismissedBanners(new Set(currentValue)); } - }, [observedItems?.newValue]); + }, [observedItems?.value]); const handleClick = () => { notificationsStore.set('dismissedBanners', [...dismissedBanners, id]); diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 83cca4477b..89361419ba 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -678,8 +678,6 @@ export type SignInPageProps = { // @public export interface StorageApi { forBucket(name: string): StorageApi; - // @deprecated - get(key: string): T | undefined; observe$( key: string, ): Observable>; @@ -691,23 +689,17 @@ export interface StorageApi { // @public export const storageApiRef: ApiRef; -// @public @deprecated (undocumented) -export type StorageValueChange = - StorageValueSnapshot; - // @public export type StorageValueSnapshot = | { key: string; presence: 'unknown' | 'absent'; value?: undefined; - newValue?: undefined; } | { key: string; presence: 'present'; value: TValue; - newValue?: TValue; }; // @public diff --git a/packages/core-plugin-api/src/apis/definitions/StorageApi.ts b/packages/core-plugin-api/src/apis/definitions/StorageApi.ts index 4bd49f9b83..1506e5f143 100644 --- a/packages/core-plugin-api/src/apis/definitions/StorageApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/StorageApi.ts @@ -27,24 +27,13 @@ export type StorageValueSnapshot = key: string; presence: 'unknown' | 'absent'; value?: undefined; - /** @deprecated Use `value` instead */ - newValue?: undefined; } | { key: string; presence: 'present'; value: TValue; - /** @deprecated Use `value` instead */ - newValue?: TValue; }; -/** - * @public - * @deprecated Use StorageValueSnapshot instead - */ -export type StorageValueChange = - StorageValueSnapshot; - /** * Provides a key-value persistence API. * @@ -59,14 +48,6 @@ export interface StorageApi { */ forBucket(name: string): StorageApi; - /** - * Get the current value for persistent data, use observe$ to be notified of updates. - * - * @deprecated Use `snapshot` instead. - * @param key - Unique key associated with the data. - */ - get(key: string): T | undefined; - /** * Remove persistent data. * diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index 4e5361ff80..2f2671885e 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -164,8 +164,6 @@ export class MockStorageApi implements StorageApi { // (undocumented) forBucket(name: string): StorageApi; // (undocumented) - get(key: string): T | undefined; - // (undocumented) observe$(key: string): Observable>; // (undocumented) remove(key: string): Promise; diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts index c974198be1..63aa4235e1 100644 --- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts @@ -24,7 +24,7 @@ describe('WebStorage Storage API', () => { it('should return undefined for values which are unset', async () => { const storage = createMockStorage(); - expect(storage.get('myfakekey')).toBeUndefined(); + expect(storage.snapshot('myfakekey').value).toBeUndefined(); expect(storage.snapshot('myfakekey')).toEqual({ key: 'myfakekey', presence: 'absent', @@ -33,32 +33,29 @@ describe('WebStorage Storage API', () => { }); }); - it('should allow the setting and getting of the simple data structures', async () => { + it('should allow the setting and snapshotting of the simple data structures', async () => { const storage = createMockStorage(); await storage.set('myfakekey', 'helloimastring'); await storage.set('mysecondfakekey', 1234); await storage.set('mythirdfakekey', true); - expect(storage.get('myfakekey')).toBe('helloimastring'); - expect(storage.get('mysecondfakekey')).toBe(1234); - expect(storage.get('mythirdfakekey')).toBe(true); + expect(storage.snapshot('myfakekey').value).toBe('helloimastring'); + expect(storage.snapshot('mysecondfakekey').value).toBe(1234); + expect(storage.snapshot('mythirdfakekey').value).toBe(true); expect(storage.snapshot('myfakekey')).toEqual({ key: 'myfakekey', presence: 'present', value: 'helloimastring', - newValue: 'helloimastring', }); expect(storage.snapshot('mysecondfakekey')).toEqual({ key: 'mysecondfakekey', presence: 'present', value: 1234, - newValue: 1234, }); expect(storage.snapshot('mythirdfakekey')).toEqual({ key: 'mythirdfakekey', presence: 'present', value: true, - newValue: true, }); }); @@ -72,12 +69,11 @@ describe('WebStorage Storage API', () => { await storage.set('myfakekey', mockData); - expect(storage.get('myfakekey')).toEqual(mockData); + expect(storage.snapshot('myfakekey').value).toEqual(mockData); expect(storage.snapshot('myfakekey')).toEqual({ key: 'myfakekey', presence: 'present', value: mockData, - newValue: mockData, }); }); @@ -107,7 +103,6 @@ describe('WebStorage Storage API', () => { key: 'correctKey', presence: 'present', value: mockData, - newValue: mockData, }); }); @@ -153,9 +148,11 @@ describe('WebStorage Storage API', () => { await firstStorage.set(keyName, 'boop'); await secondStorage.set(keyName, 'deerp'); - expect(firstStorage.get(keyName)).not.toBe(secondStorage.get(keyName)); - expect(firstStorage.get(keyName)).toBe('boop'); - expect(secondStorage.get(keyName)).toBe('deerp'); + expect(firstStorage.snapshot(keyName)).not.toBe( + secondStorage.snapshot(keyName), + ); + expect(firstStorage.snapshot(keyName).value).toBe('boop'); + expect(secondStorage.snapshot(keyName).value).toBe('deerp'); expect(firstStorage.snapshot(keyName)).not.toEqual( secondStorage.snapshot(keyName), ); @@ -163,13 +160,11 @@ describe('WebStorage Storage API', () => { key: keyName, presence: 'present', value: 'boop', - newValue: 'boop', }); expect(secondStorage.snapshot(keyName)).toEqual({ key: keyName, presence: 'present', value: 'deerp', - newValue: 'deerp', }); }); @@ -186,7 +181,7 @@ describe('WebStorage Storage API', () => { await firstStorage.set('test2', { error: true }); - expect(secondStorage.get('deep/test2')).toBe(undefined); + expect(secondStorage.snapshot('deep/test2').value).toBe(undefined); expect(secondStorage.snapshot('deep/test2')).toMatchObject({ presence: 'absent', }); @@ -201,19 +196,17 @@ describe('WebStorage Storage API', () => { await firstStorage.set('test2', true); - expect(firstStorage.get('test2')).toBe(true); - expect(secondStorage.get('test2')).toBe(undefined); + expect(firstStorage.snapshot('test2').value).toBe(true); + expect(secondStorage.snapshot('test2').value).toBe(undefined); expect(firstStorage.snapshot('test2')).toEqual({ key: 'test2', presence: 'present', value: true, - newValue: true, }); expect(secondStorage.snapshot('test2')).toEqual({ key: 'test2', presence: 'absent', value: undefined, - newValue: undefined, }); }); diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts index 25024f2211..6ce91a4a31 100644 --- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts @@ -61,10 +61,6 @@ export class MockStorageApi implements StorageApi { return this.bucketStorageApis.get(name)!; } - get(key: string): T | undefined { - return this.snapshot(key).value as T | undefined; - } - snapshot(key: string): StorageValueSnapshot { if (this.data.hasOwnProperty(this.getKeyName(key))) { const data = this.data[this.getKeyName(key)]; @@ -72,14 +68,12 @@ export class MockStorageApi implements StorageApi { key, presence: 'present', value: data, - newValue: data, }; } return { key, presence: 'absent', value: undefined, - newValue: undefined, }; } @@ -95,7 +89,6 @@ export class MockStorageApi implements StorageApi { key, presence: 'present', value: serialized, - newValue: serialized, }); } @@ -105,7 +98,6 @@ export class MockStorageApi implements StorageApi { key, presence: 'absent', value: undefined, - newValue: undefined, }); } diff --git a/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts b/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts index e9cf84f11b..f4b7771d04 100644 --- a/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts +++ b/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.ts @@ -36,12 +36,12 @@ export class DefaultStarredEntitiesApi implements StarredEntitiesApi { this.settingsStore = opts.storageApi.forBucket('starredEntities'); this.starredEntities = new Set( - this.settingsStore.get('entityRefs') ?? [], + this.settingsStore.snapshot('entityRefs').value ?? [], ); this.settingsStore.observe$('entityRefs').subscribe({ next: next => { - this.starredEntities = new Set(next.newValue ?? []); + this.starredEntities = new Set(next.value ?? []); this.notifyChanges(); }, }); diff --git a/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts b/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts index 7eb5648d13..b3bc5400d5 100644 --- a/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts +++ b/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts @@ -41,19 +41,19 @@ describe('performMigrationToTheNewBucket', () => { 'entity:Component:default:a', 'entity:template:custom:b', ]); - expect(oldBucket.get('starredEntities')).not.toBeUndefined(); + expect(oldBucket.snapshot('starredEntities').value).not.toBeUndefined(); await performMigrationToTheNewBucket({ storageApi: mockStorage }); // read NEW bucket - expect(await newBucket.get('entityRefs')).toEqual([ + expect(await newBucket.snapshot('entityRefs').value).toEqual([ 'component:default/c', 'component:default/a', 'template:custom/b', ]); // OLD bucket should be removed - expect(oldBucket.get('starredEntities')).toBeUndefined(); + expect(oldBucket.snapshot('starredEntities').value).toBeUndefined(); }); it('should ignore invalid entries', async () => { @@ -67,15 +67,17 @@ describe('performMigrationToTheNewBucket', () => { 'entity:Component:a', 'invalid', ]); - expect(oldBucket.get('starredEntities')).not.toBeUndefined(); + expect(oldBucket.snapshot('starredEntities')).not.toBeUndefined(); await performMigrationToTheNewBucket({ storageApi: mockStorage }); // read NEW bucket - expect(await newBucket.get('entityRefs')).toEqual(['component:default/a']); + expect(await newBucket.snapshot('entityRefs').value).toEqual([ + 'component:default/a', + ]); // OLD bucket should be removed - expect(oldBucket.get('starredEntities')).toBeUndefined(); + expect(oldBucket.snapshot('starredEntities').value).toBeUndefined(); }); it('should skip migration without old starred entities', async () => { @@ -88,7 +90,7 @@ describe('performMigrationToTheNewBucket', () => { await performMigrationToTheNewBucket({ storageApi: mockStorage }); // read NEW bucket - expect(newBucket.get('entityRefs')).toEqual(expectedEntries); + expect(newBucket.snapshot('entityRefs').value).toEqual(expectedEntries); }); it('should skip migration with non-array old starred entities', async () => { @@ -105,9 +107,9 @@ describe('performMigrationToTheNewBucket', () => { await performMigrationToTheNewBucket({ storageApi: mockStorage }); // read NEW bucket - expect(newBucket.get('entityRefs')).toEqual(expectedEntries); + expect(newBucket.snapshot('entityRefs').value).toEqual(expectedEntries); // OLD bucket should be unchanged - expect(oldBucket.get('starredEntities')).toBe('invalid'); + expect(oldBucket.snapshot('starredEntities').value).toBe('invalid'); }); }); diff --git a/plugins/catalog/src/apis/StarredEntitiesApi/migration.ts b/plugins/catalog/src/apis/StarredEntitiesApi/migration.ts index 8405d9e08d..51dcd6e7ce 100644 --- a/plugins/catalog/src/apis/StarredEntitiesApi/migration.ts +++ b/plugins/catalog/src/apis/StarredEntitiesApi/migration.ts @@ -35,13 +35,15 @@ export async function performMigrationToTheNewBucket({ const source = storageApi.forBucket('settings'); const target = storageApi.forBucket('starredEntities'); - const oldStarredEntities = source.get('starredEntities'); + const oldStarredEntities = source.snapshot('starredEntities').value; if (!isArray(oldStarredEntities)) { // nothing to do return; } - const targetEntities = new Set(target.get('entityRefs') ?? []); + const targetEntities = new Set( + target.snapshot('entityRefs').value ?? [], + ); oldStarredEntities .filter(isString) From f7b3c2f1b0264f9ffa95fcdce2f89223374d1680 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Tue, 1 Mar 2022 09:59:12 +0000 Subject: [PATCH 045/353] Initial commit Signed-off-by: Karan Shah --- .../EntityAirbrakeWidget.tsx | 109 +++++++++++------- 1 file changed, 65 insertions(+), 44 deletions(-) diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx index b11116aeef..93bf490fa8 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import { Entity } from '@backstage/catalog-model'; -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { Grid, Typography } from '@material-ui/core'; import { EmptyState, @@ -36,59 +36,80 @@ const useStyles = makeStyles(() => ({ }, })); +enum ComponentState { + Loading, + NoProjectId, + Error, + Loaded, +} + export const EntityAirbrakeWidget = ({ entity }: { entity: Entity }) => { const classes = useStyles(); const projectId = useProjectId(entity); const errorApi = useApi(errorApiRef); const airbrakeApi = useApi(airbrakeApiRef); - - const { loading, value, error } = useAsync( - () => airbrakeApi.fetchGroups(projectId), - [airbrakeApi, projectId], + const [componentState, setComponentState] = useState( + ComponentState.Loading, ); - useEffect(() => { - if (error) { - errorApi.post(error); - } - }, [error, errorApi]); - - if (loading || !projectId || error) { - return ( - - {loading && } - - {!loading && !projectId && ( - - )} - - {!loading && error && ( - - )} - - ); + if (!projectId) { + setComponentState(ComponentState.NoProjectId); } - return ( - - {value?.groups?.map(group => ( - - {group.errors?.map(groupError => ( - - - {groupError.message} - - + const { loading, value, error } = useAsync(async () => { + const result = await airbrakeApi.fetchGroups(projectId); + setComponentState(ComponentState.Loaded); + return result; + }, [airbrakeApi, projectId]); + + if (loading) { + setComponentState(ComponentState.Loading); + } + + if (componentState !== ComponentState.NoProjectId && error) { + setComponentState(ComponentState.Error); + } + + useEffect(() => { + if (componentState === ComponentState.Error && error) { + errorApi.post(error); + } + }, [componentState, error, errorApi]); + + switch (componentState) { + case ComponentState.Loaded: + return ( + + {value?.groups?.map(group => ( + + {group.errors?.map(groupError => ( + + + {groupError.message} + + + ))} + ))} - ))} - - ); + ); + case ComponentState.NoProjectId: + return ( + + ); + case ComponentState.Loading: + return ; + case ComponentState.Error: + default: + return ( + + ); + } }; From 393c97da851f0028abeb9aa8d34c4d2628a410db Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Tue, 1 Mar 2022 10:08:57 +0000 Subject: [PATCH 046/353] Wrap state changes with useEffect to stop infinite re-rendering Signed-off-by: Karan Shah --- .../EntityAirbrakeWidget.tsx | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx index 93bf490fa8..f85ebbf8a9 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx @@ -53,9 +53,11 @@ export const EntityAirbrakeWidget = ({ entity }: { entity: Entity }) => { ComponentState.Loading, ); - if (!projectId) { - setComponentState(ComponentState.NoProjectId); - } + useEffect(() => { + if (!projectId) { + setComponentState(ComponentState.NoProjectId); + } + }, [projectId]); const { loading, value, error } = useAsync(async () => { const result = await airbrakeApi.fetchGroups(projectId); @@ -63,16 +65,15 @@ export const EntityAirbrakeWidget = ({ entity }: { entity: Entity }) => { return result; }, [airbrakeApi, projectId]); - if (loading) { - setComponentState(ComponentState.Loading); - } - - if (componentState !== ComponentState.NoProjectId && error) { - setComponentState(ComponentState.Error); - } + useEffect(() => { + if (loading) { + setComponentState(ComponentState.Loading); + } + }, [loading]); useEffect(() => { - if (componentState === ComponentState.Error && error) { + if (componentState !== ComponentState.NoProjectId && error) { + setComponentState(ComponentState.Error); errorApi.post(error); } }, [componentState, error, errorApi]); From be68702834758f8dec018d413d9ff774026250a4 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 1 Mar 2022 11:13:28 +0100 Subject: [PATCH 047/353] catalog-backend: inline and deprecate BitbucketRepositoryParser type Co-authored-by: Patrik Oldsberg Signed-off-by: Johan Haals --- plugins/catalog-backend/api-report.md | 14 +++++++-- .../BitbucketDiscoveryProcessor.test.ts | 21 +++++++------ .../processors/BitbucketDiscoveryProcessor.ts | 29 ++++++++++++++---- .../BitbucketRepositoryParser.test.ts | 4 --- .../bitbucket/BitbucketRepositoryParser.ts | 30 +++++++++++-------- 5 files changed, 63 insertions(+), 35 deletions(-) diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 9b4db418b3..b69e1d5841 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -137,14 +137,22 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { export class BitbucketDiscoveryProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrationRegistry; - parser?: BitbucketRepositoryParser; + parser?: (options: { + integration: BitbucketIntegration; + target: string; + logger: Logger_2; + }) => AsyncIterable; logger: Logger_2; }); // (undocumented) static fromConfig( config: Config, options: { - parser?: BitbucketRepositoryParser; + parser?: (options: { + integration: BitbucketIntegration; + target: string; + logger: Logger_2; + }) => AsyncIterable; logger: Logger_2; }, ): BitbucketDiscoveryProcessor; @@ -158,7 +166,7 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor { ): Promise; } -// @public (undocumented) +// @public @deprecated (undocumented) export type BitbucketRepositoryParser = (options: { integration: BitbucketIntegration; target: string; diff --git a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts index 9ee8bf1b57..4a261386c0 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts @@ -18,7 +18,6 @@ import { BitbucketDiscoveryProcessor } from './BitbucketDiscoveryProcessor'; import { ConfigReader } from '@backstage/config'; import { BitbucketRepository20, - BitbucketRepositoryParser, PagedResponse, PagedResponse20, } from './bitbucket'; @@ -742,15 +741,6 @@ describe('BitbucketDiscoveryProcessor', () => { }); describe('Custom repository parser', () => { - const customRepositoryParser: BitbucketRepositoryParser = - async function* customRepositoryParser({}) { - yield results.location({ - type: 'custom-location-type', - target: 'custom-target', - presence: 'optional', - }); - }; - const processor = BitbucketDiscoveryProcessor.fromConfig( new ConfigReader({ integrations: { @@ -763,7 +753,16 @@ describe('BitbucketDiscoveryProcessor', () => { ], }, }), - { parser: customRepositoryParser, logger: getVoidLogger() }, + { + parser: async function* customRepositoryParser({}) { + yield results.location({ + type: 'custom-location-type', + target: 'custom-target', + presence: 'optional', + }); + }, + logger: getVoidLogger(), + }, ); it('use custom repository parser', async () => { diff --git a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts index 8fcf64dc55..25f52b7dd7 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts @@ -22,7 +22,6 @@ import { ScmIntegrations, } from '@backstage/integration'; import { - BitbucketRepositoryParser, BitbucketClient, defaultRepositoryParser, paginated, @@ -30,7 +29,12 @@ import { BitbucketRepository, BitbucketRepository20, } from './bitbucket'; -import { CatalogProcessor, CatalogProcessorEmit, LocationSpec } from './types'; +import { + CatalogProcessor, + CatalogProcessorEmit, + CatalogProcessorResult, + LocationSpec, +} from './types'; const DEFAULT_BRANCH = 'master'; const DEFAULT_CATALOG_LOCATION = '/catalog-info.yaml'; @@ -39,12 +43,23 @@ const EMPTY_CATALOG_LOCATION = '/'; /** @public */ export class BitbucketDiscoveryProcessor implements CatalogProcessor { private readonly integrations: ScmIntegrationRegistry; - private readonly parser: BitbucketRepositoryParser; + private readonly parser: (options: { + integration: BitbucketIntegration; + target: string; + logger: Logger; + }) => AsyncIterable; private readonly logger: Logger; static fromConfig( config: Config, - options: { parser?: BitbucketRepositoryParser; logger: Logger }, + options: { + parser?: (options: { + integration: BitbucketIntegration; + target: string; + logger: Logger; + }) => AsyncIterable; + logger: Logger; + }, ) { const integrations = ScmIntegrations.fromConfig(config); @@ -56,7 +71,11 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrationRegistry; - parser?: BitbucketRepositoryParser; + parser?: (options: { + integration: BitbucketIntegration; + target: string; + logger: Logger; + }) => AsyncIterable; logger: Logger; }) { this.integrations = options.integrations; diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts b/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts index 35e24ed4cb..e010865041 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts @@ -15,8 +15,6 @@ */ import { defaultRepositoryParser } from './BitbucketRepositoryParser'; import { results } from '../index'; -import { getVoidLogger } from '@backstage/backend-common'; -import { BitbucketIntegration } from '@backstage/integration'; describe('BitbucketRepositoryParser', () => { describe('defaultRepositoryParser', () => { @@ -32,9 +30,7 @@ describe('BitbucketRepositoryParser', () => { }), ]; const actual = await defaultRepositoryParser({ - integration: {} as BitbucketIntegration, target: `${browseUrl}${path}`, - logger: getVoidLogger(), }); let i = 0; diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts b/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts index 9eaec221b5..6671735f57 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts +++ b/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts @@ -18,21 +18,27 @@ import { results } from '../index'; import { Logger } from 'winston'; import { BitbucketIntegration } from '@backstage/integration'; -/** @public */ +/** + * @public + * @deprecated type inlined. + */ export type BitbucketRepositoryParser = (options: { integration: BitbucketIntegration; target: string; logger: Logger; }) => AsyncIterable; -export const defaultRepositoryParser: BitbucketRepositoryParser = - async function* defaultRepositoryParser({ target }) { - yield results.location({ - type: 'url', - target: target, - // Not all locations may actually exist, since the user defined them as a wildcard pattern. - // Thus, we emit them as optional and let the downstream processor find them while not outputting - // an error if it couldn't. - presence: 'optional', - }); - }; +export const defaultRepositoryParser = async function* defaultRepositoryParser({ + target, +}: { + target: string; +}) { + yield results.location({ + type: 'url', + target: target, + // Not all locations may actually exist, since the user defined them as a wildcard pattern. + // Thus, we emit them as optional and let the downstream processor find them while not outputting + // an error if it couldn't. + presence: 'optional', + }); +}; From fc6d31b5c3826531840c87bb35a85835385d1dc7 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 1 Mar 2022 11:15:06 +0100 Subject: [PATCH 048/353] add changeset Signed-off-by: Johan Haals --- .changeset/plenty-tables-mix.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/plenty-tables-mix.md diff --git a/.changeset/plenty-tables-mix.md b/.changeset/plenty-tables-mix.md new file mode 100644 index 0000000000..5dffdf6946 --- /dev/null +++ b/.changeset/plenty-tables-mix.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Deprecated the `BitbucketRepositoryParser` type. From babb280c77ec445fd7376484d69faabb8debbd83 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 1 Mar 2022 11:27:37 +0100 Subject: [PATCH 049/353] chore: deprecate some of the loading things from useEntity and force entity to be present Signed-off-by: blam --- plugins/catalog-react/src/hooks/index.ts | 3 + plugins/catalog-react/src/hooks/useEntity.tsx | 58 +++++++++++++++---- .../components/EntitySwitch/EntitySwitch.tsx | 7 ++- .../src/components/EntitySwitch/conditions.ts | 6 +- 4 files changed, 58 insertions(+), 16 deletions(-) diff --git a/plugins/catalog-react/src/hooks/index.ts b/plugins/catalog-react/src/hooks/index.ts index 3e6ef7c945..8e0fc48e72 100644 --- a/plugins/catalog-react/src/hooks/index.ts +++ b/plugins/catalog-react/src/hooks/index.ts @@ -18,11 +18,14 @@ export { useEntityFromUrl, EntityProvider, AsyncEntityProvider, + useAsyncEntity, } from './useEntity'; export type { EntityLoadingStatus, EntityProviderProps, AsyncEntityProviderProps, + UseEntityResponse, + UseAsyncEntityResponse, } from './useEntity'; export { useEntityCompoundName } from './useEntityCompoundName'; export { diff --git a/plugins/catalog-react/src/hooks/useEntity.tsx b/plugins/catalog-react/src/hooks/useEntity.tsx index 5cb67cb79a..1db14adc22 100644 --- a/plugins/catalog-react/src/hooks/useEntity.tsx +++ b/plugins/catalog-react/src/hooks/useEntity.tsx @@ -128,26 +128,35 @@ export const useEntityFromUrl = (): EntityLoadingStatus => { return { entity, loading, error, refresh }; }; +export interface UseEntityResponse { + entity: T; + /** @deprecated use useAsyncEntity instead */ + loading: boolean; + /** @deprecated use useAsyncEntity instead */ + error?: Error; + /** @deprecated use useAsyncEntity instead */ + refresh?: VoidFunction; +} + +export interface UseAsyncEntityResponse { + entity?: T; + loading: boolean; + error?: Error; + refresh?: VoidFunction; +} /** - * Grab the current entity from the context and its current loading state. + * Grab the current entity from the context, throws if the entity has not yet been loaded + * or is not available. * * @public */ -export function useEntity() { +export function useEntity(): UseEntityResponse { const versionedHolder = useVersionedContext<{ 1: EntityLoadingStatus }>('entity-context'); if (!versionedHolder) { - // TODO(Rugvip): Throw this once we fully migrate to the new context - // throw new Error('Entity context is not available'); - - return { - entity: undefined as unknown as T, - loading: true, - error: undefined, - refresh: () => {}, - }; + throw new Error('Entity context is not available'); } const value = versionedHolder.atVersion(1); @@ -155,6 +164,33 @@ export function useEntity() { throw new Error('EntityContext v1 not available'); } + if (!value.entity) { + throw new Error('Entity has not been loaded yet'); + } + + const { entity, loading, error, refresh } = value; + return { entity: entity as T, loading, error, refresh }; +} + +/** + * Grab the current entity from the context, provides loading state and errors, and the ability to refresh. + * + * @public + */ +export function useAsyncEntity< + T extends Entity = Entity, +>(): UseAsyncEntityResponse { + const versionedHolder = + useVersionedContext<{ 1: EntityLoadingStatus }>('entity-context'); + + if (!versionedHolder) { + throw new Error('Entity context is not available'); + } + const value = versionedHolder.atVersion(1); + if (!value) { + throw new Error('EntityContext v1 not available'); + } + const { entity, loading, error, refresh } = value; return { entity: entity as T, loading, error, refresh }; } diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx index 4433517247..1b8667ce2a 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -import { useEntity } from '@backstage/plugin-catalog-react'; +import { useAsyncEntity } from '@backstage/plugin-catalog-react'; import React, { ReactNode, ReactElement } from 'react'; import { attachComponentData, @@ -63,7 +63,7 @@ export interface EntitySwitchProps { /** @public */ export const EntitySwitch = (props: EntitySwitchProps) => { - const { entity } = useEntity(); + const { entity } = useAsyncEntity(); const apis = useApiHolder(); const results = useElementFilter( props.children, @@ -75,6 +75,9 @@ export const EntitySwitch = (props: EntitySwitchProps) => { }) .getElements() .flatMap((element: ReactElement) => { + if (!entity) { + return []; + } const { if: condition, children: elementsChildren } = element.props as EntitySwitchCase; return [ diff --git a/plugins/catalog/src/components/EntitySwitch/conditions.ts b/plugins/catalog/src/components/EntitySwitch/conditions.ts index a02170cc6f..907aa45afe 100644 --- a/plugins/catalog/src/components/EntitySwitch/conditions.ts +++ b/plugins/catalog/src/components/EntitySwitch/conditions.ts @@ -27,7 +27,7 @@ function strCmp(a: string | undefined, b: string | undefined): boolean { * @public */ export function isKind(kind: string) { - return (entity: Entity) => strCmp(entity?.kind, kind); + return (entity: Entity) => strCmp(entity.kind, kind); } /** @@ -36,7 +36,7 @@ export function isKind(kind: string) { */ export function isComponentType(type: string) { return (entity: Entity) => { - if (!strCmp(entity?.kind, 'component')) { + if (!strCmp(entity.kind, 'component')) { return false; } const componentEntity = entity as ComponentEntity; @@ -49,5 +49,5 @@ export function isComponentType(type: string) { * @public */ export function isNamespace(namespace: string) { - return (entity: Entity) => strCmp(entity?.metadata?.namespace, namespace); + return (entity: Entity) => strCmp(entity.metadata?.namespace, namespace); } From c85292b768c07e24554e3033a41f41f33fe20c63 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 1 Mar 2022 11:28:59 +0100 Subject: [PATCH 050/353] catalog-backend: Remove catalogProcessor.handleError Signed-off-by: Johan Haals --- .changeset/serious-mayflies-thank.md | 5 +++++ plugins/catalog-backend/api-report.md | 5 ----- .../src/ingestion/processors/types.ts | 14 -------------- 3 files changed, 5 insertions(+), 19 deletions(-) create mode 100644 .changeset/serious-mayflies-thank.md diff --git a/.changeset/serious-mayflies-thank.md b/.changeset/serious-mayflies-thank.md new file mode 100644 index 0000000000..b3c5e31527 --- /dev/null +++ b/.changeset/serious-mayflies-thank.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +**Breaking**: Removed optional `handleError()` from `CatalogProcessor`. This optional method is never called by the catalog processing engine and can therefore be removed. diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 9b4db418b3..23aae02610 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -300,11 +300,6 @@ export type CatalogProcessor = { emit: CatalogProcessorEmit, cache: CatalogProcessorCache, ): Promise; - handleError?( - error: Error, - location: LocationSpec, - emit: CatalogProcessorEmit, - ): Promise; }; // @public diff --git a/plugins/catalog-backend/src/ingestion/processors/types.ts b/plugins/catalog-backend/src/ingestion/processors/types.ts index 879b4d32a8..80beda4213 100644 --- a/plugins/catalog-backend/src/ingestion/processors/types.ts +++ b/plugins/catalog-backend/src/ingestion/processors/types.ts @@ -116,20 +116,6 @@ export type CatalogProcessor = { emit: CatalogProcessorEmit, cache: CatalogProcessorCache, ): Promise; - - /** - * Handles an emitted error. - * - * @param error - The error - * @param location - The location where the error occurred - * @param emit - A sink for items resulting from this handling - * @returns Nothing - */ - handleError?( - error: Error, - location: LocationSpec, - emit: CatalogProcessorEmit, - ): Promise; }; /** From 44403296e7c9fff1709548f3bdcff7bd8eceb854 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 1 Mar 2022 11:39:49 +0100 Subject: [PATCH 051/353] chore: added changeset Signed-off-by: blam --- .changeset/sour-eggs-kick.md | 9 +++++ plugins/catalog-react/api-report.md | 34 ++++++++++++++++--- plugins/catalog-react/src/hooks/useEntity.tsx | 26 +++++++++++--- 3 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 .changeset/sour-eggs-kick.md diff --git a/.changeset/sour-eggs-kick.md b/.changeset/sour-eggs-kick.md new file mode 100644 index 0000000000..6e814415ab --- /dev/null +++ b/.changeset/sour-eggs-kick.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-react': patch +--- + +Added the following deprecations to the `catalog-react` package: + +- **DEPRECATION**: `useEntity` will now warn if the entity has not yet been loaded. This hook is now designed only to be used inside of an `EntityPage` where the `entity` prop is guaranteed to be defined. If you would like to use it outside, please use `useAsyncEntity` instead. +- **DEPRECATION**: the `loading`, `error` and `refresh` properties that are returned from `useEntity` have been deprecated, and are available on `useAsyncEntity` instead. diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index e78ec75daa..0103ac2f0e 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -524,12 +524,24 @@ export type UnregisterEntityDialogProps = { }; // @public -export function useEntity(): { - entity: T; +export function useAsyncEntity< + T extends Entity = Entity, +>(): UseAsyncEntityResponse; + +// @public +export interface UseAsyncEntityResponse { + // (undocumented) + entity?: T; + // (undocumented) + error?: Error; + // (undocumented) loading: boolean; - error: Error | undefined; - refresh: VoidFunction | undefined; -}; + // (undocumented) + refresh?: VoidFunction; +} + +// @public +export function useEntity(): UseEntityResponse; // @public @deprecated export const useEntityCompoundName: () => { @@ -571,6 +583,18 @@ export function useEntityPermission(permission: Permission): { error?: Error; }; +// @public +export interface UseEntityResponse { + // (undocumented) + entity: T; + // @deprecated (undocumented) + error?: Error; + // @deprecated (undocumented) + loading: boolean; + // @deprecated (undocumented) + refresh?: VoidFunction; +} + // @public export function useEntityTypeFilter(): { loading: boolean; diff --git a/plugins/catalog-react/src/hooks/useEntity.tsx b/plugins/catalog-react/src/hooks/useEntity.tsx index 1db14adc22..9becedfee8 100644 --- a/plugins/catalog-react/src/hooks/useEntity.tsx +++ b/plugins/catalog-react/src/hooks/useEntity.tsx @@ -128,16 +128,27 @@ export const useEntityFromUrl = (): EntityLoadingStatus => { return { entity, loading, error, refresh }; }; + +/** + * @public + * + * The response shape for {@link useEntity} + */ export interface UseEntityResponse { entity: T; - /** @deprecated use useAsyncEntity instead */ + /** @deprecated use {@link useAsyncEntity} instead */ loading: boolean; - /** @deprecated use useAsyncEntity instead */ + /** @deprecated use {@link useAsyncEntity} instead */ error?: Error; - /** @deprecated use useAsyncEntity instead */ + /** @deprecated use {@link useAsyncEntity} instead */ refresh?: VoidFunction; } +/** + * @public + * + * The response shape for {@link useAsyncEntity} + */ export interface UseAsyncEntityResponse { entity?: T; loading: boolean; @@ -165,7 +176,14 @@ export function useEntity(): UseEntityResponse { } if (!value.entity) { - throw new Error('Entity has not been loaded yet'); + // Once we have removed the additional fields from being returned we can drop this deprecation + // and move to the error instead. + // throw new Error('useEntity hook is being called outside of an EntityPage where the entity has not been loaded. If this is intentional, please use useAsyncEntity instead.'); + + // eslint-disable-next-line no-console + console.warn( + 'DEPRECATION: useEntity hook is being called outside of an EntityPage where the entity has not been loaded. If this is intentional, please use useAsyncEntity instead. This warning will be replaced with an error in future releases.', + ); } const { entity, loading, error, refresh } = value; From 33ae7f244b7e50845ba772f5ae6d46bf5a205861 Mon Sep 17 00:00:00 2001 From: Luna Stadler Date: Tue, 1 Mar 2022 11:46:07 +0100 Subject: [PATCH 052/353] Fix header nesting in software-templates docs Most pages in the 'Software Templates' docs seemed to have one level too much nesting. This fixes that. Without the correct nesting, the table of contents on the right side was also missing and made reading the longer pages (e.g. writing-templates) more difficult to read. What's odd is that when rendering with `mkdocs` locally the nesting seems to be okay and the table of content renders. Pages affected live: - https://backstage.io/docs/features/software-templates/software-templates-index - https://backstage.io/docs/features/software-templates/configuration - https://backstage.io/docs/features/software-templates/writing-templates - https://backstage.io/docs/features/software-templates/builtin-actions - https://backstage.io/docs/features/software-templates/writing-custom-actions https://backstage.io/docs/features/software-templates/adding-templates is okay because it has no subheadings. And these two are okay because they have correct nesting starting at level two (`##`): - https://backstage.io/docs/features/software-templates/writing-custom-field-extensions - https://backstage.io/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3 Signed-off-by: Luna Stadler --- .../software-templates/builtin-actions.md | 6 +++--- .../features/software-templates/configuration.md | 6 +++--- docs/features/software-templates/index.md | 8 ++++---- .../software-templates/writing-custom-actions.md | 8 ++++---- .../software-templates/writing-templates.md | 16 ++++++++-------- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/features/software-templates/builtin-actions.md b/docs/features/software-templates/builtin-actions.md index af3ae97b21..c8f830f3ee 100644 --- a/docs/features/software-templates/builtin-actions.md +++ b/docs/features/software-templates/builtin-actions.md @@ -15,13 +15,13 @@ A list of all registered actions can be found under `/create/actions`. For local development you should be able to reach them at `http://localhost:3000/create/actions`. -### Migrating from `fetch:cookiecutter` to `fetch:template` +## Migrating from `fetch:cookiecutter` to `fetch:template` The `fetch:template` action is a new action with a similar API to `fetch:cookiecutter` but no dependency on `cookiecutter`. There are two options for migrating templates that use `fetch:cookiecutter` to use `fetch:template`: -#### Using `cookiecutterCompat` mode +### Using `cookiecutterCompat` mode The new `fetch:template` action has a `cookiecutterCompat` flag which should allow most templates built for `fetch:cookiecutter` to work without any changes. @@ -43,7 +43,7 @@ allow most templates built for `fetch:cookiecutter` to work without any changes. values: ``` -#### Manual migration +### Manual migration If you prefer, you can manually migrate your templates to avoid the need for enabling cookiecutter compatibility mode, which will result in slightly less diff --git a/docs/features/software-templates/configuration.md b/docs/features/software-templates/configuration.md index d22b82bc16..87c17ca1a3 100644 --- a/docs/features/software-templates/configuration.md +++ b/docs/features/software-templates/configuration.md @@ -18,7 +18,7 @@ The next step is to add [add templates](http://backstage.io/docs/features/software-templates/adding-templates) to your Backstage app. -### Publishing defaults +## Publishing defaults Software templates can define _publish_ actions, such as `publish:github`, to create new repositories or submit pull / merge requests to existing @@ -45,7 +45,7 @@ add the `repoVisibility` key within a software template: repoVisibility: public # or 'internal' or 'private' ``` -### Disabling Docker in Docker situation (Optional) +## Disabling Docker in Docker situation (Optional) Software templates use the `fetch:template` action by default, which requires no external dependencies and offers a @@ -68,7 +68,7 @@ RUN apt-get update && apt-get install -y python3 python3-pip RUN pip3 install cookiecutter ``` -### Customizing the ScaffolderPage with Grouping and Filtering +## Customizing the ScaffolderPage with Grouping and Filtering Once you have more than a few software templates you may want to customize your `ScaffolderPage` by grouping and surfacing certain templates together. You can diff --git a/docs/features/software-templates/index.md b/docs/features/software-templates/index.md index a854543acb..5911e192b1 100644 --- a/docs/features/software-templates/index.md +++ b/docs/features/software-templates/index.md @@ -15,7 +15,7 @@ locations like GitHub or GitLab. -### Getting Started +## Getting Started > Be sure to have covered > [Getting Started with Backstage](../../getting-started) before proceeding. @@ -27,7 +27,7 @@ Once there, you should see something that looks similar to this: ![Create Image](../../assets/software-templates/create.png) -### Choose a template +## Choose a template When you select a template that you want to create, you'll be taken to the next page which may or may not look different for each template. Each template can @@ -44,7 +44,7 @@ provider, for instance `https://github.com/backstage/my-new-repository`, or ![Enter Backstage vars](../../assets/software-templates/template-picked-2.png) -### Run! +## Run! Once you've entered values and confirmed, you'll then get a popup box with live progress of what is currently happening with the creation of your template. @@ -60,7 +60,7 @@ step that failed which can be helpful in debugging. ![Templating failed](../../assets/software-templates/failed.png) -### View Component in Catalog +## View Component in Catalog When it's been created, you'll see the `View in Catalog` button, which will take you to the registered component in the catalog: diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index 5afccc3450..8c57f202a7 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -12,7 +12,7 @@ by writing custom actions which can be used along side our > built-in actions too**. To ensure you can continue to include the builtin > actions, see below to include them during registration of your action. -### Writing your Custom Action +## Writing your Custom Action Your custom action can live where you choose, but simplest is to include it alongside your `backend` package in `packages/backend`. @@ -79,7 +79,7 @@ The `createTemplateAction` takes an object which specifies the following: function using `ctx.output` - `handler` - the actual code which is run part of the action, with a context -#### The context object +### The context object When the action `handler` is called, we provide you a `context` as the only argument. It looks like the following: @@ -98,7 +98,7 @@ argument. It looks like the following: - `ctx.metadata` - an object containing a `name` field, indicating the template name. More metadata fields may be added later. -### Registering Custom Actions +## Registering Custom Actions Once you have your Custom Action ready for usage with the scaffolder, you'll need to pass this into the `scaffolder-backend` `createRouter` function. You @@ -145,7 +145,7 @@ return await createRouter({ }); ``` -### List of custom action packages +## List of custom action packages Here is a list of Open Source custom actions that you can add to your Backstage scaffolder backend: diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 415f8cd85e..41061d45a3 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -99,7 +99,7 @@ spec: Let's dive in and pick apart what each of these sections do and what they are. -### `spec.parameters` - `FormStep | FormStep[]` +## `spec.parameters` - `FormStep | FormStep[]` These `parameters` are template variables which can be modified in the frontend as a sequence. It can either be one `Step` if you just want one big list of @@ -227,7 +227,7 @@ spec: inputType: tel ``` -#### Hide or mask sensitive data on Review step +### Hide or mask sensitive data on Review step Sometimes, specially in custom fields, you collect some data on Create form that must not be shown to the user on Review step. To hide or mask this data, you can @@ -254,7 +254,7 @@ use `ui:widget: password` or set some properties of `ui:backstage`: show: false # wont print any info about 'hidden' property on Review Step ``` -#### The Repository Picker +### The Repository Picker In order to make working with repository providers easier, we've built a custom picker that can be used by overriding the `ui:field` option in the `uiSchema` @@ -287,7 +287,7 @@ The `RepoUrlPicker` is a custom field that we provide part of the `plugin-scaffolder`. You can provide your own custom fields by [writing your own Custom Field Extensions](./writing-custom-field-extensions.md) -##### Using the Users `oauth` token +#### Using the Users `oauth` token There's a little bit of extra magic that you get out of the box when using the `RepoUrlPicker` as a field input. You can provide some additional options under @@ -360,7 +360,7 @@ There's also the ability to pass additional scopes when requesting the `oauth` token from the user, which you can do on a per-provider basis, in case your template can be published to multiple providers. -#### The Owner Picker +### The Owner Picker When the scaffolder needs to add new components to the catalog, it needs to have an owner for them. Ideally, users should be able to select an owner when they go @@ -380,7 +380,7 @@ owner: - Group ``` -### `spec.steps` - `Action[]` +## `spec.steps` - `Action[]` The `steps` is an array of the things that you want to happen part of this template. These follow the same standard format: @@ -400,7 +400,7 @@ By default we ship some [built in actions](./builtin-actions.md) that you can take a look at, or you can [create your own custom actions](./writing-custom-actions.md). -### Outputs +## Outputs Each individual step can output some variables that can be used in the scaffolder frontend for after the job is finished. This is useful for things @@ -415,7 +415,7 @@ output: entityRef: ${{ steps.register.output.entityRef }} # link to the entity that has been ingested to the catalog ``` -### The templating syntax +## The templating syntax You might have noticed variables wrapped in `${{ }}` in the examples. These are template strings for linking and gluing the different parts of the template From f2b4b747a6bd2311f0a51a327fdbdb7436733e68 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 1 Mar 2022 11:57:36 +0100 Subject: [PATCH 053/353] chore: removing deprecated routeRefs from catalog-react Signed-off-by: blam --- plugins/catalog-react/api-report.md | 13 ------------ plugins/catalog-react/src/index.ts | 8 +------ plugins/catalog-react/src/routes.ts | 21 ------------------- .../EntityOrphanWarning.test.tsx | 9 +++----- .../EntityOrphanWarning.tsx | 5 +++-- plugins/catalog/src/plugin.ts | 6 +++--- plugins/catalog/src/routes.ts | 9 +++++++- 7 files changed, 18 insertions(+), 53 deletions(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 3e1136313f..ffddfaedc6 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -95,9 +95,6 @@ export type CatalogReactUserListPickerClassKey = | 'menuItem' | 'groupWrapper'; -// @public @deprecated (undocumented) -export const catalogRouteRef: RouteRef; - // @public (undocumented) export const columnFactories: Readonly<{ createEntityRefColumn(options: { @@ -272,13 +269,6 @@ export type EntityRefLinksProps = { defaultKind?: string; } & Omit; -// @public @deprecated (undocumented) -export const entityRoute: RouteRef<{ - name: string; - kind: string; - namespace: string; -}>; - // @public export function entityRouteParams(entity: Entity): { readonly kind: string; @@ -494,9 +484,6 @@ export function reduceEntityFilters( filters: EntityFilter[], ): (entity: Entity) => boolean; -// @public @deprecated (undocumented) -export const rootRoute: RouteRef; - // @public export interface StarredEntitiesApi { starredEntitie$(): Observable>; diff --git a/plugins/catalog-react/src/index.ts b/plugins/catalog-react/src/index.ts index 7bf1bd1221..2279d49a2c 100644 --- a/plugins/catalog-react/src/index.ts +++ b/plugins/catalog-react/src/index.ts @@ -27,13 +27,7 @@ export * from './apis'; export * from './components'; export * from './hooks'; export * from './filters'; -export { - catalogRouteRef, - entityRoute, - entityRouteParams, - entityRouteRef, - rootRoute, -} from './routes'; +export { entityRouteParams, entityRouteRef } from './routes'; export * from './testUtils'; export * from './types'; export * from './utils'; diff --git a/plugins/catalog-react/src/routes.ts b/plugins/catalog-react/src/routes.ts index b9aa0de531..013bf7b5be 100644 --- a/plugins/catalog-react/src/routes.ts +++ b/plugins/catalog-react/src/routes.ts @@ -18,21 +18,6 @@ import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { createRouteRef } from '@backstage/core-plugin-api'; import { getOrCreateGlobalSingleton } from '@backstage/version-bridge'; -// TODO(Rugvip): Move these route refs back to the catalog plugin once we're all ported to using external routes -/** - * @deprecated Use an `ExternalRouteRef` instead, which can point to `catalogPlugin.routes.catalogIndex`. - * @public - */ -export const rootRoute = createRouteRef({ - id: 'catalog', -}); - -/** - * @deprecated Use an `ExternalRouteRef` instead, which can point to `catalogPlugin.routes.catalogIndex`. - * @public - */ -export const catalogRouteRef = rootRoute; - /** * A stable route ref that points to the catalog page for an individual entity. * @@ -52,12 +37,6 @@ export const entityRouteRef = getOrCreateGlobalSingleton( }), ); -/** - * @deprecated use `entityRouteRef` instead. - * @public - */ -export const entityRoute = entityRouteRef; - /** * Utility function to get suitable route params for entityRoute, given an * @public diff --git a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx index a8aac405fe..f00474868f 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx @@ -14,14 +14,11 @@ * limitations under the License. */ -import { - catalogApiRef, - catalogRouteRef, - EntityProvider, -} from '@backstage/plugin-catalog-react'; +import { catalogApiRef, EntityProvider } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; +import { rootRouteRef } from '../../routes'; import { EntityOrphanWarning } from './EntityOrphanWarning'; describe('', () => { @@ -59,7 +56,7 @@ describe('', () => { , { mountedRoutes: { - '/create': catalogRouteRef, + '/create': rootRouteRef, }, }, ); diff --git a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx index aba28c558f..26b0e5de45 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx @@ -15,12 +15,13 @@ */ import { Entity } from '@backstage/catalog-model'; -import { catalogRouteRef, useEntity } from '@backstage/plugin-catalog-react'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { Alert } from '@material-ui/lab'; import React, { useState } from 'react'; import { useNavigate } from 'react-router'; import { DeleteEntityDialog } from './DeleteEntityDialog'; import { useRouteRef } from '@backstage/core-plugin-api'; +import { rootRouteRef } from '../../routes'; /** * Returns true if the given entity has the orphan annotation given by the @@ -40,7 +41,7 @@ export function isOrphan(entity: Entity): boolean { */ export function EntityOrphanWarning() { const navigate = useNavigate(); - const catalogLink = useRouteRef(catalogRouteRef); + const catalogLink = useRouteRef(rootRouteRef); const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false); const { entity } = useEntity(); diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 520656e921..1b9083e7ca 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -18,7 +18,6 @@ import { CatalogClient } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { catalogApiRef, - catalogRouteRef, entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; @@ -43,6 +42,7 @@ import { HasResourcesCardProps } from './components/HasResourcesCard'; import { HasSubcomponentsCardProps } from './components/HasSubcomponentsCard'; import { HasSystemsCardProps } from './components/HasSystemsCard'; import { RelatedEntitiesCardProps } from './components/RelatedEntitiesCard'; +import { rootRouteRef } from './routes'; /** @public */ export const catalogPlugin = createPlugin({ @@ -65,7 +65,7 @@ export const catalogPlugin = createPlugin({ }), ], routes: { - catalogIndex: catalogRouteRef, + catalogIndex: rootRouteRef, catalogEntity: entityRouteRef, }, externalRoutes: { @@ -81,7 +81,7 @@ export const CatalogIndexPage: (props: DefaultCatalogPageProps) => JSX.Element = name: 'CatalogIndexPage', component: () => import('./components/CatalogPage').then(m => m.CatalogPage), - mountPoint: catalogRouteRef, + mountPoint: rootRouteRef, }), ); diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts index 63d9ccfb87..5c0d195d74 100644 --- a/plugins/catalog/src/routes.ts +++ b/plugins/catalog/src/routes.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { createExternalRouteRef } from '@backstage/core-plugin-api'; +import { + createExternalRouteRef, + createRouteRef, +} from '@backstage/core-plugin-api'; export const createComponentRouteRef = createExternalRouteRef({ id: 'create-component', @@ -26,3 +29,7 @@ export const viewTechDocRouteRef = createExternalRouteRef({ optional: true, params: ['namespace', 'kind', 'name'], }); + +export const rootRouteRef = createRouteRef({ + id: 'catalog', +}); From 7e0a0109bf84e9cee035a9f5bbc40a18273e323b Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 1 Mar 2022 12:44:06 +0100 Subject: [PATCH 054/353] Export permission criteria utilities Signed-off-by: Vincenzo Scamporlino --- plugins/permission-node/api-report.md | 18 +++++++ .../permission-node/src/integration/index.ts | 1 + .../permission-node/src/integration/util.ts | 48 ++++++++++++++----- 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 534ad45110..f6de7f4909 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -3,6 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AllOfCriteria } from '@backstage/plugin-permission-common'; +import { AnyOfCriteria } from '@backstage/plugin-permission-common'; import { AuthorizeDecision } from '@backstage/plugin-permission-common'; import { AuthorizeQuery } from '@backstage/plugin-permission-common'; import { AuthorizeRequestOptions } from '@backstage/plugin-permission-common'; @@ -11,6 +13,7 @@ import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { Config } from '@backstage/config'; import express from 'express'; import { Identified } from '@backstage/plugin-permission-common'; +import { NotCriteria } from '@backstage/plugin-permission-common'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; import { PermissionCondition } from '@backstage/plugin-permission-common'; import { PermissionCriteria } from '@backstage/plugin-permission-common'; @@ -118,6 +121,21 @@ export type DefinitivePolicyDecision = { result: AuthorizeResult.ALLOW | AuthorizeResult.DENY; }; +// @alpha +export const isAndCriteria: ( + criteria: PermissionCriteria, +) => criteria is AllOfCriteria; + +// @alpha +export const isNotCriteria: ( + criteria: PermissionCriteria, +) => criteria is NotCriteria; + +// @alpha +export const isOrCriteria: ( + criteria: PermissionCriteria, +) => criteria is AnyOfCriteria; + // @public export const makeCreatePermissionRule: () => < TParams extends unknown[], diff --git a/plugins/permission-node/src/integration/index.ts b/plugins/permission-node/src/integration/index.ts index 978342e4ed..7702fea95b 100644 --- a/plugins/permission-node/src/integration/index.ts +++ b/plugins/permission-node/src/integration/index.ts @@ -19,3 +19,4 @@ export * from './createConditionExports'; export * from './createConditionTransformer'; export * from './createPermissionIntegrationRouter'; export * from './createPermissionRule'; +export { isAndCriteria, isOrCriteria, isNotCriteria } from './util'; diff --git a/plugins/permission-node/src/integration/util.ts b/plugins/permission-node/src/integration/util.ts index 3878e18895..9102f092c4 100644 --- a/plugins/permission-node/src/integration/util.ts +++ b/plugins/permission-node/src/integration/util.ts @@ -22,20 +22,44 @@ import { } from '@backstage/plugin-permission-common'; import { PermissionRule } from '../types'; -export const isAndCriteria = ( - filter: PermissionCriteria, -): filter is AllOfCriteria => - Object.prototype.hasOwnProperty.call(filter, 'allOf'); +/** + * Utility function used to parse a PermissionCriteria + * @param criteria - a PermissionCriteria + * @alpha + * + * @returns `true` if the permission criteria is of type allOf, + * narrowing down `criteria` to the specific type. + */ +export const isAndCriteria = ( + criteria: PermissionCriteria, +): criteria is AllOfCriteria => + Object.prototype.hasOwnProperty.call(criteria, 'allOf'); -export const isOrCriteria = ( - filter: PermissionCriteria, -): filter is AnyOfCriteria => - Object.prototype.hasOwnProperty.call(filter, 'anyOf'); +/** + * Utility function used to parse a PermissionCriteria of type + * @param criteria - a PermissionCriteria + * @alpha + * + * @returns `true` if the permission criteria is of type anyOf, + * narrowing down `criteria` to the specific type. + */ +export const isOrCriteria = ( + criteria: PermissionCriteria, +): criteria is AnyOfCriteria => + Object.prototype.hasOwnProperty.call(criteria, 'anyOf'); -export const isNotCriteria = ( - filter: PermissionCriteria, -): filter is NotCriteria => - Object.prototype.hasOwnProperty.call(filter, 'not'); +/** + * Utility function used to parse a PermissionCriteria + * @param criteria - a PermissionCriteria + * @alpha + * + * @returns `true` if the permission criteria is of type not, + * narrowing down `criteria` to the specific type. + */ +export const isNotCriteria = ( + criteria: PermissionCriteria, +): criteria is NotCriteria => + Object.prototype.hasOwnProperty.call(criteria, 'not'); export const createGetRule = ( rules: PermissionRule[], From 580f4e1df8f6af6616b0de24f51bfdc9c2a46b03 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 1 Mar 2022 12:46:17 +0100 Subject: [PATCH 055/353] Add changeset Signed-off-by: Vincenzo Scamporlino --- .changeset/popular-items-tan.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/popular-items-tan.md diff --git a/.changeset/popular-items-tan.md b/.changeset/popular-items-tan.md new file mode 100644 index 0000000000..b91773a5d8 --- /dev/null +++ b/.changeset/popular-items-tan.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-permission-node': patch +--- + +Export some utility functions for parsing PermissionCriteria + +`isAndCriteria`, `isOrCriteria`, `isNotCriteria` are now exported. From 6fe5d70cace2d1123c8f3dff3d84fe8b4eb5189a Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 1 Mar 2022 12:49:04 +0100 Subject: [PATCH 056/353] chore: fix up the changeset Signed-off-by: blam --- .changeset/sour-eggs-kick.md | 3 ++- plugins/catalog-react/src/hooks/useEntity.tsx | 2 +- .../catalog/src/components/EntityLayout/EntityLayout.tsx | 8 +++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.changeset/sour-eggs-kick.md b/.changeset/sour-eggs-kick.md index 6e814415ab..f796b173fb 100644 --- a/.changeset/sour-eggs-kick.md +++ b/.changeset/sour-eggs-kick.md @@ -5,5 +5,6 @@ Added the following deprecations to the `catalog-react` package: -- **DEPRECATION**: `useEntity` will now warn if the entity has not yet been loaded. This hook is now designed only to be used inside of an `EntityPage` where the `entity` prop is guaranteed to be defined. If you would like to use it outside, please use `useAsyncEntity` instead. +- **DEPRECATION**: `useEntity` will now warn if the entity has not yet been loaded, and will soon throw errors instead. If you're using the default implementation of `EntityLayout` and `EntitySwitch` then these components will ensure that there is an entity loaded before rendering children. If you're implementing your own `EntityLayout` or `EntitySwitch` or something that operates outside or adjacent to them, then use `useAsyncEntity`. + - **DEPRECATION**: the `loading`, `error` and `refresh` properties that are returned from `useEntity` have been deprecated, and are available on `useAsyncEntity` instead. diff --git a/plugins/catalog-react/src/hooks/useEntity.tsx b/plugins/catalog-react/src/hooks/useEntity.tsx index 9becedfee8..e46ee680eb 100644 --- a/plugins/catalog-react/src/hooks/useEntity.tsx +++ b/plugins/catalog-react/src/hooks/useEntity.tsx @@ -182,7 +182,7 @@ export function useEntity(): UseEntityResponse { // eslint-disable-next-line no-console console.warn( - 'DEPRECATION: useEntity hook is being called outside of an EntityPage where the entity has not been loaded. If this is intentional, please use useAsyncEntity instead. This warning will be replaced with an error in future releases.', + 'DEPRECATION: useEntity hook is being called outside of an EntityLayout where the entity has not been loaded. If this is intentional, please use useAsyncEntity instead. This warning will be replaced with an error in future releases.', ); } diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index 634662f386..40c4f9e873 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -40,7 +40,7 @@ import { getEntityRelations, InspectEntityDialog, UnregisterEntityDialog, - useEntity, + useAsyncEntity, useEntityCompoundName, } from '@backstage/plugin-catalog-react'; import { Box, TabProps } from '@material-ui/core'; @@ -177,7 +177,7 @@ export const EntityLayout = (props: EntityLayoutProps) => { children, } = props; const { kind, namespace, name } = useEntityCompoundName(); - const { entity, loading, error } = useEntity(); + const { entity, loading, error } = useAsyncEntity(); const location = useLocation(); const routes = useElementFilter( children, @@ -190,7 +190,9 @@ export const EntityLayout = (props: EntityLayoutProps) => { }) .getElements() // all nodes, element data, maintain structure or not? .flatMap(({ props: elementProps }) => { - if (elementProps.if && entity && !elementProps.if(entity)) { + if (!entity) { + return []; + } else if (elementProps.if && !elementProps.if(entity)) { return []; } From 9caa47d98f273cd9fcaeaccfbc67d865a8b7904b Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Tue, 1 Mar 2022 11:50:28 +0000 Subject: [PATCH 057/353] Handle no project ID being present Signed-off-by: Karan Shah --- plugins/airbrake/src/api/ProductionApi.ts | 10 +++++ .../EntityAirbrakeWidget.tsx | 37 +++++++++++++------ 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/plugins/airbrake/src/api/ProductionApi.ts b/plugins/airbrake/src/api/ProductionApi.ts index 21ca19ee7d..abd8c8863f 100644 --- a/plugins/airbrake/src/api/ProductionApi.ts +++ b/plugins/airbrake/src/api/ProductionApi.ts @@ -18,10 +18,20 @@ import { Groups } from './airbrakeGroups'; import { AirbrakeApi } from './AirbrakeApi'; import { DiscoveryApi } from '@backstage/core-plugin-api'; +export class NoProjectIdError extends Error { + constructor() { + super('Project ID is not present'); + } +} + export class ProductionAirbrakeApi implements AirbrakeApi { constructor(private readonly discoveryApi: DiscoveryApi) {} async fetchGroups(projectId: string): Promise { + if (!projectId) { + throw new NoProjectIdError(); + } + const baseUrl = await this.discoveryApi.getBaseUrl('airbrake'); const apiUrl = `${baseUrl}/api/v4/projects/${projectId}/groups`; diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx index f85ebbf8a9..affb822f64 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx @@ -29,6 +29,7 @@ import { ErrorApi, errorApiRef, useApi } from '@backstage/core-plugin-api'; import { airbrakeApiRef } from '../../api'; import useAsync from 'react-use/lib/useAsync'; import { AIRBRAKE_PROJECT_ID_ANNOTATION, useProjectId } from '../useProjectId'; +import { NoProjectIdError } from '../../api/ProductionApi'; const useStyles = makeStyles(() => ({ multilineText: { @@ -56,14 +57,35 @@ export const EntityAirbrakeWidget = ({ entity }: { entity: Entity }) => { useEffect(() => { if (!projectId) { setComponentState(ComponentState.NoProjectId); + } else { + setComponentState(ComponentState.Loading); } }, [projectId]); const { loading, value, error } = useAsync(async () => { - const result = await airbrakeApi.fetchGroups(projectId); - setComponentState(ComponentState.Loaded); - return result; - }, [airbrakeApi, projectId]); + try { + const result = await airbrakeApi.fetchGroups(projectId); + setComponentState(ComponentState.Loaded); + return result; + } catch (e) { + if (e instanceof NoProjectIdError) { + setComponentState(ComponentState.NoProjectId); + } else { + setComponentState(ComponentState.Error); + } + throw e; + } + }, [componentState, airbrakeApi, projectId]); + + useEffect(() => { + if ( + componentState === ComponentState.Error && + error && + !(error instanceof NoProjectIdError) + ) { + errorApi.post(error); + } + }, [componentState, error, errorApi]); useEffect(() => { if (loading) { @@ -71,13 +93,6 @@ export const EntityAirbrakeWidget = ({ entity }: { entity: Entity }) => { } }, [loading]); - useEffect(() => { - if (componentState !== ComponentState.NoProjectId && error) { - setComponentState(ComponentState.Error); - errorApi.post(error); - } - }, [componentState, error, errorApi]); - switch (componentState) { case ComponentState.Loaded: return ( From 862e41623974165913fdd4244bd04fd7ed986c02 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 1 Mar 2022 13:34:38 +0100 Subject: [PATCH 058/353] catalog-backend: Remove entityRef from CatalogProcessorRelationResult Signed-off-by: Johan Haals --- .changeset/real-kids-hide.md | 5 +++++ plugins/catalog-backend/src/ingestion/processors/types.ts | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .changeset/real-kids-hide.md diff --git a/.changeset/real-kids-hide.md b/.changeset/real-kids-hide.md new file mode 100644 index 0000000000..188a4061c9 --- /dev/null +++ b/.changeset/real-kids-hide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +**Breaking**: Removed `entityRef` from `CatalogProcessorRelationResult`. The field is not used by the catalog and relation information is already available inside the `reation` property. diff --git a/plugins/catalog-backend/src/ingestion/processors/types.ts b/plugins/catalog-backend/src/ingestion/processors/types.ts index 879b4d32a8..8a66d3eeb1 100644 --- a/plugins/catalog-backend/src/ingestion/processors/types.ts +++ b/plugins/catalog-backend/src/ingestion/processors/types.ts @@ -193,7 +193,6 @@ export type CatalogProcessorEntityResult = { export type CatalogProcessorRelationResult = { type: 'relation'; relation: EntityRelationSpec; - entityRef?: string; }; /** @public */ From b19061a5471a3ff63574f934ab9021c6019a88ae Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 1 Mar 2022 13:47:25 +0100 Subject: [PATCH 059/353] update api report Signed-off-by: Johan Haals --- plugins/catalog-backend/api-report.md | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 9b4db418b3..604f92a4c3 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -347,7 +347,6 @@ export type CatalogProcessorParser = (options: { export type CatalogProcessorRelationResult = { type: 'relation'; relation: EntityRelationSpec; - entityRef?: string; }; // @public (undocumented) From e02960f30f31f78af1661e82bf812ca07726bdb7 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Tue, 1 Mar 2022 12:56:42 +0000 Subject: [PATCH 060/353] catalog-backend: add alpha setup to package.json Signed-off-by: Mike Lewis --- plugins/catalog-backend/package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 55c92c92be..be0772e199 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -9,7 +9,8 @@ "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "backstage": { "role": "backend-plugin" @@ -25,7 +26,7 @@ ], "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -87,6 +88,7 @@ }, "files": [ "dist", + "alpha", "migrations/**/*.{js,d.ts}", "config.d.ts" ], From aa69677928adc0467a44b8f47589fec00e898b11 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 1 Mar 2022 14:03:08 +0100 Subject: [PATCH 061/353] chore: fixing tests Signed-off-by: blam --- .../AllureReportComponent.test.tsx | 11 ++++++++++- plugins/todo/src/plugin.test.tsx | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx index 34d12aa032..e6355d861f 100644 --- a/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx +++ b/plugins/allure/src/components/AllureReportComponent/AllureReportComponent.test.tsx @@ -23,6 +23,7 @@ import { setupRequestMockHandlers, renderInTestApp, } from '@backstage/test-utils'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; describe('ExampleComponent', () => { const server = setupServer(); @@ -39,7 +40,15 @@ describe('ExampleComponent', () => { it('should render', async () => { const rendered = await renderInTestApp( - + + + , ); expect(rendered.getByText('Missing Annotation')).toBeInTheDocument(); diff --git a/plugins/todo/src/plugin.test.tsx b/plugins/todo/src/plugin.test.tsx index 57b9be291d..847ab7f65c 100644 --- a/plugins/todo/src/plugin.test.tsx +++ b/plugins/todo/src/plugin.test.tsx @@ -19,6 +19,7 @@ import { Route } from 'react-router'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { todoPlugin, EntityTodoContent } from './plugin'; import { todoApiRef } from './api'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; describe('todo', () => { it('should export plugin', () => { @@ -47,7 +48,15 @@ describe('todo', () => { ], ]} > - } /> + + } /> + , ); From dd7e34b4b8685389b31c5162dac3f38bc11c8ca8 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 1 Mar 2022 14:04:15 +0100 Subject: [PATCH 062/353] chore: added additional changeset Signed-off-by: blam --- .changeset/tall-pillows-smash.md | 6 ++++++ plugins/todo/src/plugin.test.tsx | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/tall-pillows-smash.md diff --git a/.changeset/tall-pillows-smash.md b/.changeset/tall-pillows-smash.md new file mode 100644 index 0000000000..3c1d18cbe3 --- /dev/null +++ b/.changeset/tall-pillows-smash.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-allure': patch +'@backstage/plugin-todo': patch +--- + +Fixing broken tests for the packages with the new `useEntity` change diff --git a/plugins/todo/src/plugin.test.tsx b/plugins/todo/src/plugin.test.tsx index 847ab7f65c..af4704c16f 100644 --- a/plugins/todo/src/plugin.test.tsx +++ b/plugins/todo/src/plugin.test.tsx @@ -50,7 +50,7 @@ describe('todo', () => { > Date: Tue, 1 Mar 2022 14:16:39 +0100 Subject: [PATCH 063/353] cli: install both v16 and v17 of @hot-loader/react-dom Signed-off-by: Patrik Oldsberg --- .changeset/poor-hounds-beam.md | 5 +++++ packages/cli/package.json | 3 ++- packages/cli/src/lib/bundler/config.ts | 7 +++++-- yarn.lock | 12 +++++++++++- 4 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 .changeset/poor-hounds-beam.md diff --git a/.changeset/poor-hounds-beam.md b/.changeset/poor-hounds-beam.md new file mode 100644 index 0000000000..925370c1b0 --- /dev/null +++ b/.changeset/poor-hounds-beam.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The CLI now bundles both version 16 and 17 of the patched `@hot-loader/react-dom` dependency, and selects the appropriate one based on what version of `react-dom` is installed within the app. diff --git a/packages/cli/package.json b/packages/cli/package.json index b17d517ba1..e5551ce29e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -37,7 +37,8 @@ "@backstage/errors": "^0.2.2", "@backstage/release-manifests": "^0.0.2", "@backstage/types": "^0.1.3", - "@hot-loader/react-dom": "^17.0.2", + "@hot-loader/react-dom-v16": "npm:@hot-loader/react-dom@^16.0.2", + "@hot-loader/react-dom-v17": "npm:@hot-loader/react-dom@^17.0.2", "@manypkg/get-packages": "^1.1.3", "@octokit/request": "^5.4.12", "@rollup/plugin-commonjs": "^21.0.1", diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 3cfcafc005..fe64f23209 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -167,13 +167,16 @@ export async function createConfig( }), ); + // Detect and use the appropriate react-dom hot-loader patch based on what + // version of React is used within the target repo. const resolveAliases: Record = {}; try { // eslint-disable-next-line import/no-extraneous-dependencies const { version: reactDomVersion } = require('react-dom/package.json'); - // Only apply the alias for hook support if we're running with React 16 if (reactDomVersion.startsWith('16.')) { - resolveAliases['react-dom'] = '@hot-loader/react-dom'; + resolveAliases['react-dom'] = '@hot-loader/react-dom-v16'; + } else { + resolveAliases['react-dom'] = '@hot-loader/react-dom-v17'; } } catch (error) { console.warn(`WARNING: Failed to read react-dom version, ${error}`); diff --git a/yarn.lock b/yarn.lock index a6b27ffa82..3b14b3dcfa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2557,7 +2557,17 @@ dependencies: "@hapi/hoek" "^9.0.0" -"@hot-loader/react-dom@^17.0.2": +"@hot-loader/react-dom-v16@npm:@hot-loader/react-dom@^16.0.2": + version "16.14.0" + resolved "https://registry.npmjs.org/@hot-loader/react-dom/-/react-dom-16.14.0.tgz#3cfc64e40bb78fa623e59b582b8f09dcdaad648a" + integrity sha512-EN9czvcLsMYmSDo5yRKZOAq3ZGRlDpad1gPtX0NdMMomJXcPE3yFSeFzE94X/NjOaiSVimB7LuqPYpkWVaIi4Q== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.19.1" + +"@hot-loader/react-dom-v17@npm:@hot-loader/react-dom@^17.0.2": version "17.0.2" resolved "https://registry.npmjs.org/@hot-loader/react-dom/-/react-dom-17.0.2.tgz#0b24e484093e8f97eb5c72bebdda44fc20bc8400" integrity sha512-G2RZrFhsQClS+bdDh/Ojpk3SgocLPUGnvnJDTQYnmKSSwXtU+Yh+8QMs+Ia3zaAvBiOSpIIDSUxuN69cvKqrWg== From da79aac2a68a349d893144f9dd2df3dc17da60c4 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 1 Mar 2022 14:17:38 +0100 Subject: [PATCH 064/353] chore: add some beautiful changesets Signed-off-by: blam --- .changeset/ninety-kids-drop.md | 8 ++++++++ .changeset/spicy-onions-kiss.md | 5 +++++ 2 files changed, 13 insertions(+) create mode 100644 .changeset/ninety-kids-drop.md create mode 100644 .changeset/spicy-onions-kiss.md diff --git a/.changeset/ninety-kids-drop.md b/.changeset/ninety-kids-drop.md new file mode 100644 index 0000000000..ed5cf6330b --- /dev/null +++ b/.changeset/ninety-kids-drop.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +Removed some previously deprecated `routeRefs` as follows: + +- **BREAKING**: Removed `entityRoute` in favor of `entityRouteRef`. +- **BREAKING**: Removed the previously deprecated `rootRoute` and `catalogRouteRef`. If you want to refer to the catalog index page from a public plugin you now need to use an `ExternalRouteRef` instead. For private plugins it is possible to take the shortcut of referring directly to `catalogPlugin.routes.indexPage` instead. diff --git a/.changeset/spicy-onions-kiss.md b/.changeset/spicy-onions-kiss.md new file mode 100644 index 0000000000..777e233b88 --- /dev/null +++ b/.changeset/spicy-onions-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +- Replaced usage of the deprecated and now removed `rootRoute` and `catalogRouteRef`s from the `catalog-react` package From e3c2bfef11b268981627d1ffebfcd513549ecac6 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Tue, 1 Mar 2022 13:35:41 +0000 Subject: [PATCH 065/353] catalog-common: remove resourceType from catalogEntityCreatePermission The resourceType on permissions refers to the resource whose ref is expected to be passed along with the permission during authorization. This allows the permission-backend to make the decision based on characteristics of the resource. Since the entity being created by definition doesn't yet exist, it's not correct for this permission to include a resourceType. Signed-off-by: Mike Lewis --- .changeset/giant-taxis-drop.md | 5 +++++ plugins/catalog-common/src/permissions.ts | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .changeset/giant-taxis-drop.md diff --git a/.changeset/giant-taxis-drop.md b/.changeset/giant-taxis-drop.md new file mode 100644 index 0000000000..41f8a91d40 --- /dev/null +++ b/.changeset/giant-taxis-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-common': minor +--- + +Remove resourceType property from catalogEntityCreatePermission. Resource type refers to the type of resources whose resourceRefs should be passed along with authorize requests, to allow conditional responses for that resource type. Since creation does not correspond to an entity (as the entity does not exist at the time of authorization), the resourceRef should not be included on the permission. diff --git a/plugins/catalog-common/src/permissions.ts b/plugins/catalog-common/src/permissions.ts index 52c225e441..51b09bab7e 100644 --- a/plugins/catalog-common/src/permissions.ts +++ b/plugins/catalog-common/src/permissions.ts @@ -49,7 +49,6 @@ export const catalogEntityCreatePermission: Permission = { attributes: { action: 'create', }, - resourceType: RESOURCE_TYPE_CATALOG_ENTITY, }; /** From 83a83381b09890e0c2f6f7a6a5b4edcda7ae2982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sun, 27 Feb 2022 19:16:01 +0100 Subject: [PATCH 066/353] rearrange MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/lemon-needles-applaud.md | 5 ++ .changeset/long-weeks-thank.md | 5 ++ .changeset/old-waves-wash.md | 8 +++ .../software-catalog/external-integrations.md | 4 +- .../AwsOrganizationCloudAccountProcessor.ts | 4 +- .../src/processors/LdapOrgReaderProcessor.ts | 6 +- .../MicrosoftGraphOrgReaderProcessor.ts | 6 +- plugins/catalog-backend/api-report.md | 37 ++++++++-- plugins/catalog-backend/src/api/common.ts | 56 +++++++++++++++ .../results.ts => api/deprecatedResult.ts} | 36 +++++++--- plugins/catalog-backend/src/api/index.ts | 38 ++++++++++ .../src/api/processingResult.ts | 71 +++++++++++++++++++ .../processors/types.ts => api/processor.ts} | 19 +---- .../{providers/types.ts => api/provider.ts} | 2 +- plugins/catalog-backend/src/database/types.ts | 3 +- plugins/catalog-backend/src/index.ts | 3 +- .../src/ingestion/CatalogRules.test.ts | 2 +- .../src/ingestion/CatalogRules.ts | 2 +- .../catalog-backend/src/ingestion/index.ts | 2 - .../catalog-backend/src/ingestion/types.ts | 2 +- .../aws}/AwsS3DiscoveryProcessor.test.ts | 13 ++-- .../aws}/AwsS3DiscoveryProcessor.ts | 8 +-- .../aws/__fixtures__}/awsS3-mock-object.txt | 0 .../providers => modules/aws}/index.ts | 4 +- .../AzureDevOpsDiscoveryProcessor.test.ts | 6 +- .../azure}/AzureDevOpsDiscoveryProcessor.ts | 12 ++-- .../src/{providers => modules/azure}/index.ts | 8 +-- .../azure => modules/azure/lib}/azure.test.ts | 0 .../azure => modules/azure/lib}/azure.ts | 0 .../azure => modules/azure/lib}/index.ts | 0 .../BitbucketDiscoveryProcessor.test.ts | 12 ++-- .../bitbucket}/BitbucketDiscoveryProcessor.ts | 5 +- .../src/modules/bitbucket/index.ts | 18 +++++ .../lib}/BitbucketRepositoryParser.test.ts | 5 +- .../lib}/BitbucketRepositoryParser.ts | 31 ++++---- .../bitbucket/lib}/client.ts | 2 +- .../bitbucket/lib}/index.ts | 1 + .../bitbucket/lib}/types.ts | 0 .../codeowners}/CodeOwnersProcessor.test.ts | 2 +- .../codeowners}/CodeOwnersProcessor.ts | 5 +- .../src/modules/codeowners/index.ts | 17 +++++ .../codeowners/lib}/index.ts | 0 .../codeowners/lib}/read.test.ts | 0 .../codeowners/lib}/read.ts | 0 .../codeowners/lib}/resolve.test.ts | 0 .../codeowners/lib}/resolve.ts | 0 .../codeowners/lib}/scm.ts | 0 .../AnnotateLocationEntityProcessor.test.ts | 2 +- .../core}/AnnotateLocationEntityProcessor.ts | 6 +- .../AnnotateScmSlugEntityProcessor.test.ts | 2 +- .../core}/AnnotateScmSlugEntityProcessor.ts | 2 +- .../core}/BuiltinKindsEntityProcessor.test.ts | 0 .../core}/BuiltinKindsEntityProcessor.ts | 12 ++-- .../ConfigLocationEntityProvider.test.ts | 2 +- .../core}/ConfigLocationEntityProvider.ts | 6 +- .../core}/DefaultLocationStore.test.ts | 2 +- .../core}/DefaultLocationStore.ts | 10 +-- .../core}/FileReaderProcessor.test.ts | 10 ++- .../core}/FileReaderProcessor.ts | 8 +-- .../core}/LocationEntityProcessor.test.ts | 2 +- .../core}/LocationEntityProcessor.ts | 12 ++-- .../core}/PlaceholderProcessor.test.ts | 0 .../core}/PlaceholderProcessor.ts | 2 +- .../core}/StaticLocationProcessor.ts | 9 ++- .../core}/UrlReaderProcessor.test.ts | 4 +- .../core}/UrlReaderProcessor.ts | 8 +-- .../awsS3/awsS3-mock-object.txt | 1 + .../fileReaderProcessor/component.yaml | 0 .../fileReaderProcessor/dir/api.yaml | 0 .../__fixtures__/fileReaderProcessor/test.txt | 0 .../processors => modules/core}/index.ts | 17 +---- .../github}/GitHubOrgEntityProvider.test.ts | 8 ++- .../github}/GitHubOrgEntityProvider.ts | 7 +- .../github}/GithubDiscoveryProcessor.test.ts | 10 +-- .../github}/GithubDiscoveryProcessor.ts | 12 ++-- .../github}/GithubMultiOrgReaderProcessor.ts | 16 +++-- .../github}/GithubOrgReaderProcessor.test.ts | 3 +- .../github}/GithubOrgReaderProcessor.ts | 16 +++-- .../src/modules/github/index.ts | 21 ++++++ .../github/lib}/config.test.ts | 0 .../github => modules/github/lib}/config.ts | 0 .../github/lib}/github.test.ts | 0 .../github => modules/github/lib}/github.ts | 0 .../github => modules/github/lib}/index.ts | 0 .../github/lib}/util.test.ts | 1 + .../github => modules/github/lib}/util.ts | 1 + .../gitlab}/GitLabDiscoveryProcessor.test.ts | 4 +- .../gitlab}/GitLabDiscoveryProcessor.ts | 22 +++--- .../src/modules/gitlab/index.ts | 17 +++++ .../gitlab/lib}/client.test.ts | 2 +- .../gitlab => modules/gitlab/lib}/client.ts | 0 .../gitlab => modules/gitlab/lib}/index.ts | 0 .../gitlab => modules/gitlab/lib}/types.ts | 0 plugins/catalog-backend/src/modules/index.ts | 23 ++++++ .../processors => modules}/util/org.test.ts | 0 .../processors => modules}/util/org.ts | 0 .../processors => modules}/util/parse.test.ts | 21 +++--- .../processors => modules}/util/parse.ts | 12 ++-- ...faultCatalogProcessingOrchestrator.test.ts | 12 ++-- .../DefaultCatalogProcessingOrchestrator.ts | 22 +++--- .../processing/ProcessorCacheManager.test.ts | 2 +- .../src/processing/ProcessorCacheManager.ts | 3 +- .../processing/ProcessorOutputCollector.ts | 4 +- .../src/processing/connectEntityProviders.ts | 2 +- .../catalog-backend/src/processing/index.ts | 1 - .../catalog-backend/src/processing/types.ts | 25 +------ .../catalog-backend/src/processing/util.ts | 2 +- .../src/service/AuthorizedRefreshService.ts | 1 + .../src/service/CatalogBuilder.ts | 18 ++--- .../src/service/DefaultLocationService.ts | 1 + .../catalog-backend/src/util/conversion.ts | 2 +- .../processor/ScaffolderEntitiesProcessor.ts | 6 +- 112 files changed, 594 insertions(+), 280 deletions(-) create mode 100644 .changeset/lemon-needles-applaud.md create mode 100644 .changeset/long-weeks-thank.md create mode 100644 .changeset/old-waves-wash.md create mode 100644 plugins/catalog-backend/src/api/common.ts rename plugins/catalog-backend/src/{ingestion/processors/results.ts => api/deprecatedResult.ts} (69%) create mode 100644 plugins/catalog-backend/src/api/index.ts create mode 100644 plugins/catalog-backend/src/api/processingResult.ts rename plugins/catalog-backend/src/{ingestion/processors/types.ts => api/processor.ts} (92%) rename plugins/catalog-backend/src/{providers/types.ts => api/provider.ts} (96%) rename plugins/catalog-backend/src/{ingestion/processors => modules/aws}/AwsS3DiscoveryProcessor.test.ts (89%) rename plugins/catalog-backend/src/{ingestion/processors => modules/aws}/AwsS3DiscoveryProcessor.ts (92%) rename plugins/catalog-backend/src/{ingestion/processors/__fixtures__/fileReaderProcessor/awsS3 => modules/aws/__fixtures__}/awsS3-mock-object.txt (100%) rename plugins/catalog-backend/src/{ingestion/providers => modules/aws}/index.ts (83%) rename plugins/catalog-backend/src/{ingestion/processors => modules/azure}/AzureDevOpsDiscoveryProcessor.test.ts (98%) rename plugins/catalog-backend/src/{ingestion/processors => modules/azure}/AzureDevOpsDiscoveryProcessor.ts (95%) rename plugins/catalog-backend/src/{providers => modules/azure}/index.ts (79%) rename plugins/catalog-backend/src/{ingestion/processors/azure => modules/azure/lib}/azure.test.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors/azure => modules/azure/lib}/azure.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors/azure => modules/azure/lib}/index.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors => modules/bitbucket}/BitbucketDiscoveryProcessor.test.ts (99%) rename plugins/catalog-backend/src/{ingestion/processors => modules/bitbucket}/BitbucketDiscoveryProcessor.ts (99%) create mode 100644 plugins/catalog-backend/src/modules/bitbucket/index.ts rename plugins/catalog-backend/src/{ingestion/processors/bitbucket => modules/bitbucket/lib}/BitbucketRepositoryParser.test.ts (94%) rename plugins/catalog-backend/src/{ingestion/processors/bitbucket => modules/bitbucket/lib}/BitbucketRepositoryParser.ts (62%) rename plugins/catalog-backend/src/{ingestion/processors/bitbucket => modules/bitbucket/lib}/client.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors/bitbucket => modules/bitbucket/lib}/index.ts (99%) rename plugins/catalog-backend/src/{ingestion/processors/bitbucket => modules/bitbucket/lib}/types.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors => modules/codeowners}/CodeOwnersProcessor.test.ts (98%) rename plugins/catalog-backend/src/{ingestion/processors => modules/codeowners}/CodeOwnersProcessor.ts (95%) create mode 100644 plugins/catalog-backend/src/modules/codeowners/index.ts rename plugins/catalog-backend/src/{ingestion/processors/codeowners => modules/codeowners/lib}/index.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors/codeowners => modules/codeowners/lib}/read.test.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors/codeowners => modules/codeowners/lib}/read.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors/codeowners => modules/codeowners/lib}/resolve.test.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors/codeowners => modules/codeowners/lib}/resolve.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors/codeowners => modules/codeowners/lib}/scm.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors => modules/core}/AnnotateLocationEntityProcessor.test.ts (99%) rename plugins/catalog-backend/src/{ingestion/processors => modules/core}/AnnotateLocationEntityProcessor.ts (96%) rename plugins/catalog-backend/src/{ingestion/processors => modules/core}/AnnotateScmSlugEntityProcessor.test.ts (98%) rename plugins/catalog-backend/src/{ingestion/processors => modules/core}/AnnotateScmSlugEntityProcessor.ts (97%) rename plugins/catalog-backend/src/{ingestion/processors => modules/core}/BuiltinKindsEntityProcessor.test.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors => modules/core}/BuiltinKindsEntityProcessor.ts (97%) rename plugins/catalog-backend/src/{providers => modules/core}/ConfigLocationEntityProvider.test.ts (98%) rename plugins/catalog-backend/src/{providers => modules/core}/ConfigLocationEntityProvider.ts (90%) rename plugins/catalog-backend/src/{providers => modules/core}/DefaultLocationStore.test.ts (98%) rename plugins/catalog-backend/src/{providers => modules/core}/DefaultLocationStore.ts (93%) rename plugins/catalog-backend/src/{ingestion/processors => modules/core}/FileReaderProcessor.test.ts (94%) rename plugins/catalog-backend/src/{ingestion/processors => modules/core}/FileReaderProcessor.ts (92%) rename plugins/catalog-backend/src/{ingestion/processors => modules/core}/LocationEntityProcessor.test.ts (98%) rename plugins/catalog-backend/src/{ingestion/processors => modules/core}/LocationEntityProcessor.ts (92%) rename plugins/catalog-backend/src/{ingestion/processors => modules/core}/PlaceholderProcessor.test.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors => modules/core}/PlaceholderProcessor.ts (99%) rename plugins/catalog-backend/src/{ingestion/processors => modules/core}/StaticLocationProcessor.ts (91%) rename plugins/catalog-backend/src/{ingestion/processors => modules/core}/UrlReaderProcessor.test.ts (98%) rename plugins/catalog-backend/src/{ingestion/processors => modules/core}/UrlReaderProcessor.ts (95%) create mode 100644 plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt rename plugins/catalog-backend/src/{ingestion/processors => modules/core}/__fixtures__/fileReaderProcessor/component.yaml (100%) rename plugins/catalog-backend/src/{ingestion/processors => modules/core}/__fixtures__/fileReaderProcessor/dir/api.yaml (100%) rename plugins/catalog-backend/src/{ingestion/processors => modules/core}/__fixtures__/fileReaderProcessor/test.txt (100%) rename plugins/catalog-backend/src/{ingestion/processors => modules/core}/index.ts (63%) rename plugins/catalog-backend/src/{ingestion/providers => modules/github}/GitHubOrgEntityProvider.test.ts (97%) rename plugins/catalog-backend/src/{ingestion/providers => modules/github}/GitHubOrgEntityProvider.ts (97%) rename plugins/catalog-backend/src/{ingestion/processors => modules/github}/GithubDiscoveryProcessor.test.ts (99%) rename plugins/catalog-backend/src/{ingestion/processors => modules/github}/GithubDiscoveryProcessor.ts (96%) rename plugins/catalog-backend/src/{ingestion/processors => modules/github}/GithubMultiOrgReaderProcessor.ts (95%) rename plugins/catalog-backend/src/{ingestion/processors => modules/github}/GithubOrgReaderProcessor.test.ts (99%) rename plugins/catalog-backend/src/{ingestion/processors => modules/github}/GithubOrgReaderProcessor.ts (92%) create mode 100644 plugins/catalog-backend/src/modules/github/index.ts rename plugins/catalog-backend/src/{ingestion/processors/github => modules/github/lib}/config.test.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors/github => modules/github/lib}/config.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors/github => modules/github/lib}/github.test.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors/github => modules/github/lib}/github.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors/github => modules/github/lib}/index.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors/github => modules/github/lib}/util.test.ts (99%) rename plugins/catalog-backend/src/{ingestion/processors/github => modules/github/lib}/util.ts (99%) rename plugins/catalog-backend/src/{ingestion/processors => modules/gitlab}/GitLabDiscoveryProcessor.test.ts (99%) rename plugins/catalog-backend/src/{ingestion/processors => modules/gitlab}/GitLabDiscoveryProcessor.ts (92%) create mode 100644 plugins/catalog-backend/src/modules/gitlab/index.ts rename plugins/catalog-backend/src/{ingestion/processors/gitlab => modules/gitlab/lib}/client.test.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors/gitlab => modules/gitlab/lib}/client.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors/gitlab => modules/gitlab/lib}/index.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors/gitlab => modules/gitlab/lib}/types.ts (100%) create mode 100644 plugins/catalog-backend/src/modules/index.ts rename plugins/catalog-backend/src/{ingestion/processors => modules}/util/org.test.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors => modules}/util/org.ts (100%) rename plugins/catalog-backend/src/{ingestion/processors => modules}/util/parse.test.ts (91%) rename plugins/catalog-backend/src/{ingestion/processors => modules}/util/parse.ts (87%) diff --git a/.changeset/lemon-needles-applaud.md b/.changeset/lemon-needles-applaud.md new file mode 100644 index 0000000000..a77d3fd33c --- /dev/null +++ b/.changeset/lemon-needles-applaud.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +**DEPRECATED**: The `results` export, and instead adding `processingResult` with the same shape and purpose. diff --git a/.changeset/long-weeks-thank.md b/.changeset/long-weeks-thank.md new file mode 100644 index 0000000000..a6c3a0719f --- /dev/null +++ b/.changeset/long-weeks-thank.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Internal restructuring to collect the various provider files in a `modules` folder while waiting to be externalized diff --git a/.changeset/old-waves-wash.md b/.changeset/old-waves-wash.md new file mode 100644 index 0000000000..34ba0760b5 --- /dev/null +++ b/.changeset/old-waves-wash.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-catalog-backend-module-aws': patch +'@backstage/plugin-catalog-backend-module-ldap': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Use the new `processingResult` export from the catalog backend diff --git a/docs/features/software-catalog/external-integrations.md b/docs/features/software-catalog/external-integrations.md index 59caf9475c..9f592e7cca 100644 --- a/docs/features/software-catalog/external-integrations.md +++ b/docs/features/software-catalog/external-integrations.md @@ -57,7 +57,7 @@ The recommended way of instantiating the catalog backend classes is to use the `CatalogBuilder`, as illustrated in the [example backend here](https://github.com/backstage/backstage/blob/master/packages/backend/src/plugins/catalog.ts). We will create a new -[`EntityProvider`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/providers/types.ts) +[`EntityProvider`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/api/provider.ts) subclass that can be added to this catalog builder. Let's make a simple provider that can refresh a set of entities based on a @@ -355,7 +355,7 @@ The recommended way of instantiating the catalog backend classes is to use the `CatalogBuilder`, as illustrated in the [example backend here](https://github.com/backstage/backstage/blob/master/packages/backend/src/plugins/catalog.ts). We will create a new -[`CatalogProcessor`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/ingestion/processors/types.ts) +[`CatalogProcessor`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/api/processor.ts) subclass that can be added to this catalog builder. It is up to you where you put the code for this new processor class. For quick diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts index e5e72d5fa8..1018e49a41 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts @@ -20,7 +20,7 @@ import { CatalogProcessor, CatalogProcessorEmit, LocationSpec, - results, + processingResult, } from '@backstage/plugin-catalog-backend'; import AWS, { Credentials, Organizations } from 'aws-sdk'; import { Account, ListAccountsResponse } from 'aws-sdk/clients/organizations'; @@ -112,7 +112,7 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { return true; }) .forEach(entity => { - emit(results.entity(location, entity)); + emit(processingResult.entity(location, entity)); }); return true; diff --git a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts index 6c31d5a27d..bec24a1744 100644 --- a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts @@ -28,7 +28,7 @@ import { CatalogProcessor, CatalogProcessorEmit, LocationSpec, - results, + processingResult, } from '@backstage/plugin-catalog-backend'; /** @@ -119,10 +119,10 @@ export class LdapOrgReaderProcessor implements CatalogProcessor { // Done! for (const group of groups) { - emit(results.entity(location, group)); + emit(processingResult.entity(location, group)); } for (const user of users) { - emit(results.entity(location, user)); + emit(processingResult.entity(location, user)); } return true; diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts index 4db3a9b1a0..942bde19eb 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts @@ -19,7 +19,7 @@ import { CatalogProcessor, CatalogProcessorEmit, LocationSpec, - results, + processingResult, } from '@backstage/plugin-catalog-backend'; import { Logger } from 'winston'; import { @@ -125,10 +125,10 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { // Done! for (const group of groups) { - emit(results.entity(location, group)); + emit(processingResult.entity(location, group)); } for (const user of users) { - emit(results.entity(location, user)); + emit(processingResult.entity(location, user)); } return true; diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index c9e08964b6..00e09d1169 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -524,7 +524,7 @@ export type EntitiesSearchFilter = { values?: string[]; }; -// @public (undocumented) +// @public @deprecated (undocumented) function entity( atLocation: LocationSpec, newEntity: Entity, @@ -641,7 +641,7 @@ export class FileReaderProcessor implements CatalogProcessor { ): Promise; } -// @public (undocumented) +// @public @deprecated (undocumented) function generalError( atLocation: LocationSpec, message: string, @@ -776,13 +776,13 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { ): Promise; } -// @public (undocumented) +// @public @deprecated (undocumented) function inputError( atLocation: LocationSpec, message: string, ): CatalogProcessorResult; -// @public (undocumented) +// @public @deprecated (undocumented) function location_2( newLocation: LocationSpec, optional?: boolean, @@ -876,7 +876,7 @@ export interface LocationStore { listLocations(): Promise; } -// @public (undocumented) +// @public @deprecated (undocumented) function notFoundError( atLocation: LocationSpec, message: string, @@ -963,6 +963,31 @@ export type PlaceholderResolverResolveUrl = ( base: string, ) => string; +// @public +export const processingResult: Readonly<{ + readonly notFoundError: ( + atLocation: LocationSpec, + message: string, + ) => CatalogProcessorResult; + readonly inputError: ( + atLocation: LocationSpec, + message: string, + ) => CatalogProcessorResult; + readonly generalError: ( + atLocation: LocationSpec, + message: string, + ) => CatalogProcessorResult; + readonly location: ( + newLocation: LocationSpec, + optional?: boolean | undefined, + ) => CatalogProcessorResult; + readonly entity: ( + atLocation: LocationSpec, + newEntity: Entity, + ) => CatalogProcessorResult; + readonly relation: (spec: EntityRelationSpec) => CatalogProcessorResult; +}>; + // @public export type RecursivePartial = { [P in keyof T]?: T[P] extends (infer U)[] @@ -986,7 +1011,7 @@ export interface RefreshService { refresh(options: RefreshOptions): Promise; } -// @public (undocumented) +// @public @deprecated (undocumented) function relation(spec: EntityRelationSpec): CatalogProcessorResult; declare namespace results { diff --git a/plugins/catalog-backend/src/api/common.ts b/plugins/catalog-backend/src/api/common.ts new file mode 100644 index 0000000000..1fd09aec32 --- /dev/null +++ b/plugins/catalog-backend/src/api/common.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { EntityName } from '@backstage/catalog-model'; + +/** + * Holds the entity location information. + * + * @remarks + * + * `presence` flag: when using repo importer plugin, location is being created before the component yaml file is merged to the main branch. + * This flag is then set to indicate that the file can be not present. + * default value: 'required'. + * + * @public + */ +export type LocationSpec = { + type: string; + target: string; + presence?: 'optional' | 'required'; +}; + +/** + * Holds the relation data for entities. + * + * @public + */ +export type EntityRelationSpec = { + /** + * The source entity of this relation. + */ + source: EntityName; + + /** + * The type of the relation. + */ + type: string; + + /** + * The target entity of this relation. + */ + target: EntityName; +}; diff --git a/plugins/catalog-backend/src/ingestion/processors/results.ts b/plugins/catalog-backend/src/api/deprecatedResult.ts similarity index 69% rename from plugins/catalog-backend/src/ingestion/processors/results.ts rename to plugins/catalog-backend/src/api/deprecatedResult.ts index 01583678df..ecacc62c65 100644 --- a/plugins/catalog-backend/src/ingestion/processors/results.ts +++ b/plugins/catalog-backend/src/api/deprecatedResult.ts @@ -16,10 +16,15 @@ import { InputError, NotFoundError } from '@backstage/errors'; import { Entity } from '@backstage/catalog-model'; -import { CatalogProcessorResult, LocationSpec } from './types'; -import { EntityRelationSpec } from '../../processing/types'; +import { CatalogProcessorResult } from './processor'; +import { EntityRelationSpec, LocationSpec } from './common'; -/** @public */ +// NOTE: This entire file is deprecated and should be eventually removed along with the `result` export + +/** + * @public + * @deprecated import the processingResult symbol instead and use its fields + */ export function notFoundError( atLocation: LocationSpec, message: string, @@ -31,7 +36,10 @@ export function notFoundError( }; } -/** @public */ +/** + * @public + * @deprecated import the processingResult symbol instead and use its fields + */ export function inputError( atLocation: LocationSpec, message: string, @@ -43,7 +51,10 @@ export function inputError( }; } -/** @public */ +/** + * @public + * @deprecated import the processingResult symbol instead and use its fields + */ export function generalError( atLocation: LocationSpec, message: string, @@ -51,7 +62,10 @@ export function generalError( return { type: 'error', location: atLocation, error: new Error(message) }; } -/** @public */ +/** + * @public + * @deprecated import the processingResult symbol instead and use its fields + */ export function location( newLocation: LocationSpec, optional?: boolean, @@ -59,7 +73,10 @@ export function location( return { type: 'location', location: newLocation, optional }; } -/** @public */ +/** + * @public + * @deprecated import the processingResult symbol instead and use its fields + */ export function entity( atLocation: LocationSpec, newEntity: Entity, @@ -67,7 +84,10 @@ export function entity( return { type: 'entity', location: atLocation, entity: newEntity }; } -/** @public */ +/** + * @public + * @deprecated import the processingResult symbol instead and use its fields + */ export function relation(spec: EntityRelationSpec): CatalogProcessorResult { return { type: 'relation', relation: spec }; } diff --git a/plugins/catalog-backend/src/api/index.ts b/plugins/catalog-backend/src/api/index.ts new file mode 100644 index 0000000000..3f1c627743 --- /dev/null +++ b/plugins/catalog-backend/src/api/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 * as results from './deprecatedResult'; + +export { results }; + +export { processingResult } from './processingResult'; +export type { EntityRelationSpec, LocationSpec } from './common'; +export type { + CatalogProcessor, + CatalogProcessorParser, + CatalogProcessorCache, + CatalogProcessorEmit, + CatalogProcessorLocationResult, + CatalogProcessorEntityResult, + CatalogProcessorRelationResult, + CatalogProcessorErrorResult, + CatalogProcessorResult, +} from './processor'; +export type { + EntityProvider, + EntityProviderConnection, + EntityProviderMutation, +} from './provider'; diff --git a/plugins/catalog-backend/src/api/processingResult.ts b/plugins/catalog-backend/src/api/processingResult.ts new file mode 100644 index 0000000000..5ad908dc8b --- /dev/null +++ b/plugins/catalog-backend/src/api/processingResult.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { InputError, NotFoundError } from '@backstage/errors'; +import { Entity } from '@backstage/catalog-model'; +import { CatalogProcessorResult } from './processor'; +import { EntityRelationSpec, LocationSpec } from './common'; + +/** + * Factory functions for the standard processing result types. + * + * @public + */ +export const processingResult = Object.freeze({ + notFoundError( + atLocation: LocationSpec, + message: string, + ): CatalogProcessorResult { + return { + type: 'error', + location: atLocation, + error: new NotFoundError(message), + }; + }, + + inputError( + atLocation: LocationSpec, + message: string, + ): CatalogProcessorResult { + return { + type: 'error', + location: atLocation, + error: new InputError(message), + }; + }, + + generalError( + atLocation: LocationSpec, + message: string, + ): CatalogProcessorResult { + return { type: 'error', location: atLocation, error: new Error(message) }; + }, + + location( + newLocation: LocationSpec, + optional?: boolean, + ): CatalogProcessorResult { + return { type: 'location', location: newLocation, optional }; + }, + + entity(atLocation: LocationSpec, newEntity: Entity): CatalogProcessorResult { + return { type: 'entity', location: atLocation, entity: newEntity }; + }, + + relation(spec: EntityRelationSpec): CatalogProcessorResult { + return { type: 'relation', relation: spec }; + }, +} as const); diff --git a/plugins/catalog-backend/src/ingestion/processors/types.ts b/plugins/catalog-backend/src/api/processor.ts similarity index 92% rename from plugins/catalog-backend/src/ingestion/processors/types.ts rename to plugins/catalog-backend/src/api/processor.ts index 9b3bb05e2d..12a008edc7 100644 --- a/plugins/catalog-backend/src/ingestion/processors/types.ts +++ b/plugins/catalog-backend/src/api/processor.ts @@ -16,24 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/types'; -import { EntityRelationSpec } from '../../processing/types'; - -/** - * Holds the entity location information. - * - * @remarks - * - * `presence` flag: when using repo importer plugin, location is being created before the component yaml file is merged to the main branch. - * This flag is then set to indicate that the file can be not present. - * default value: 'required'. - * - * @public - */ -export type LocationSpec = { - type: string; - target: string; - presence?: 'optional' | 'required'; -}; +import { EntityRelationSpec, LocationSpec } from './common'; /** * @public diff --git a/plugins/catalog-backend/src/providers/types.ts b/plugins/catalog-backend/src/api/provider.ts similarity index 96% rename from plugins/catalog-backend/src/providers/types.ts rename to plugins/catalog-backend/src/api/provider.ts index 226c021e3c..fa7659ee32 100644 --- a/plugins/catalog-backend/src/providers/types.ts +++ b/plugins/catalog-backend/src/api/provider.ts @@ -44,7 +44,7 @@ export interface EntityProviderConnection { * @public */ export interface EntityProvider { - /** Unique name provider name used internally for caching. */ + /** Unique provider name used internally for caching. */ getProviderName(): string; /** Connect is called upon initialization by the catalog engine. */ connect(connection: EntityProviderConnection): Promise; diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index 08201323ea..1affb14b45 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -17,7 +17,8 @@ import { Entity } from '@backstage/catalog-model'; import { JsonObject } from '@backstage/types'; import { DateTime } from 'luxon'; -import { DeferredEntity, EntityRelationSpec } from '../processing/types'; +import { EntityRelationSpec } from '../api'; +import { DeferredEntity } from '../processing/types'; /** * An abstraction for transactions of the underlying database technology. diff --git a/plugins/catalog-backend/src/index.ts b/plugins/catalog-backend/src/index.ts index efdaa4b8c5..4c1e753300 100644 --- a/plugins/catalog-backend/src/index.ts +++ b/plugins/catalog-backend/src/index.ts @@ -20,11 +20,12 @@ * @packageDocumentation */ +export * from './api'; export * from './catalog'; export * from './ingestion'; +export * from './modules'; export * from './search'; export * from './util'; export * from './processing'; -export * from './providers'; export * from './service'; export * from './permissions'; diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts index d29a445a8c..13430f51dd 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts @@ -17,7 +17,7 @@ import { Entity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { DefaultCatalogRulesEnforcer } from './CatalogRules'; -import { LocationSpec } from './processors'; +import { LocationSpec } from '../api'; const entity = { user: { diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index b4ad394168..e4bfdb8d0f 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -17,7 +17,7 @@ import { Config } from '@backstage/config'; import { Entity } from '@backstage/catalog-model'; import path from 'path'; -import { LocationSpec } from './processors'; +import { LocationSpec } from '../api'; /** * Rules to apply to catalog entities. diff --git a/plugins/catalog-backend/src/ingestion/index.ts b/plugins/catalog-backend/src/ingestion/index.ts index fcb634914b..1f4729b041 100644 --- a/plugins/catalog-backend/src/ingestion/index.ts +++ b/plugins/catalog-backend/src/ingestion/index.ts @@ -16,8 +16,6 @@ export { DefaultCatalogRulesEnforcer } from './CatalogRules'; export type { CatalogRule, CatalogRulesEnforcer } from './CatalogRules'; -export * from './processors'; -export * from './providers'; export type { AnalyzeLocationEntityField, AnalyzeLocationExistingEntity, diff --git a/plugins/catalog-backend/src/ingestion/types.ts b/plugins/catalog-backend/src/ingestion/types.ts index eccf583e4b..37d12d12b1 100644 --- a/plugins/catalog-backend/src/ingestion/types.ts +++ b/plugins/catalog-backend/src/ingestion/types.ts @@ -16,7 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { RecursivePartial } from '../util/RecursivePartial'; -import { LocationSpec } from './processors'; +import { LocationSpec } from '../api'; /** @public */ export type LocationAnalyzer = { diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsS3DiscoveryProcessor.test.ts b/plugins/catalog-backend/src/modules/aws/AwsS3DiscoveryProcessor.test.ts similarity index 89% rename from plugins/catalog-backend/src/ingestion/processors/AwsS3DiscoveryProcessor.test.ts rename to plugins/catalog-backend/src/modules/aws/AwsS3DiscoveryProcessor.test.ts index 064a2ad724..249de71591 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsS3DiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/aws/AwsS3DiscoveryProcessor.test.ts @@ -13,11 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { getVoidLogger, UrlReaders } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { AwsS3DiscoveryProcessor } from './AwsS3DiscoveryProcessor'; -import { CatalogProcessorEntityResult, CatalogProcessorResult } from './types'; -import { defaultEntityDataParser } from './util/parse'; +import { + CatalogProcessorEntityResult, + CatalogProcessorResult, +} from '../../api'; +import { defaultEntityDataParser } from '../util/parse'; import AWSMock from 'aws-sdk-mock'; import aws from 'aws-sdk'; import path from 'path'; @@ -36,10 +40,7 @@ AWSMock.mock( 'getObject', Buffer.from( require('fs').readFileSync( - path.resolve( - __dirname, - '__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt', - ), + path.resolve(__dirname, '__fixtures__/awsS3-mock-object.txt'), ), ), ); diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsS3DiscoveryProcessor.ts b/plugins/catalog-backend/src/modules/aws/AwsS3DiscoveryProcessor.ts similarity index 92% rename from plugins/catalog-backend/src/ingestion/processors/AwsS3DiscoveryProcessor.ts rename to plugins/catalog-backend/src/modules/aws/AwsS3DiscoveryProcessor.ts index 0dc44cc157..737f8f7c2f 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsS3DiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/modules/aws/AwsS3DiscoveryProcessor.ts @@ -17,13 +17,13 @@ import { UrlReader } from '@backstage/backend-common'; import { isError } from '@backstage/errors'; import limiterFactory from 'p-limit'; -import * as result from './results'; import { CatalogProcessor, CatalogProcessorEmit, CatalogProcessorParser, LocationSpec, -} from './types'; + processingResult, +} from '../../api'; /** @public */ export class AwsS3DiscoveryProcessor implements CatalogProcessor { @@ -58,10 +58,10 @@ export class AwsS3DiscoveryProcessor implements CatalogProcessor { if (isError(error) && error.name === 'NotFoundError') { if (!optional) { - emit(result.notFoundError(location, message)); + emit(processingResult.notFoundError(location, message)); } } else { - emit(result.generalError(location, message)); + emit(processingResult.generalError(location, message)); } } return true; diff --git a/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt b/plugins/catalog-backend/src/modules/aws/__fixtures__/awsS3-mock-object.txt similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt rename to plugins/catalog-backend/src/modules/aws/__fixtures__/awsS3-mock-object.txt diff --git a/plugins/catalog-backend/src/ingestion/providers/index.ts b/plugins/catalog-backend/src/modules/aws/index.ts similarity index 83% rename from plugins/catalog-backend/src/ingestion/providers/index.ts rename to plugins/catalog-backend/src/modules/aws/index.ts index cd1bc5cb36..9477d4104d 100644 --- a/plugins/catalog-backend/src/ingestion/providers/index.ts +++ b/plugins/catalog-backend/src/modules/aws/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * Copyright 2022 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,4 +14,4 @@ * limitations under the License. */ -export { GitHubOrgEntityProvider } from './GitHubOrgEntityProvider'; +export { AwsS3DiscoveryProcessor } from './AwsS3DiscoveryProcessor'; diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/modules/azure/AzureDevOpsDiscoveryProcessor.test.ts similarity index 98% rename from plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts rename to plugins/catalog-backend/src/modules/azure/AzureDevOpsDiscoveryProcessor.test.ts index a406feaa74..450a0482ed 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/azure/AzureDevOpsDiscoveryProcessor.test.ts @@ -16,14 +16,14 @@ import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { codeSearch } from './azure'; +import { codeSearch } from './lib'; import { AzureDevOpsDiscoveryProcessor, parseUrl, } from './AzureDevOpsDiscoveryProcessor'; -import { LocationSpec } from './types'; +import { LocationSpec } from '../../api'; -jest.mock('./azure'); +jest.mock('./lib'); const mockCodeSearch = codeSearch as jest.MockedFunction; describe('AzureDevOpsDiscoveryProcessor', () => { diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts b/plugins/catalog-backend/src/modules/azure/AzureDevOpsDiscoveryProcessor.ts similarity index 95% rename from plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts rename to plugins/catalog-backend/src/modules/azure/AzureDevOpsDiscoveryProcessor.ts index bc74621f1d..8e36e810b8 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/modules/azure/AzureDevOpsDiscoveryProcessor.ts @@ -20,9 +20,13 @@ import { ScmIntegrations, } from '@backstage/integration'; import { Logger } from 'winston'; -import * as results from './results'; -import { CatalogProcessor, CatalogProcessorEmit, LocationSpec } from './types'; -import { codeSearch } from './azure'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '../../api'; +import { codeSearch } from './lib'; /** * Extracts repositories out of an Azure DevOps org. @@ -102,7 +106,7 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { for (const file of files) { emit( - results.location({ + processingResult.location({ type: 'url', target: `${baseUrl}/${org}/${project}/_git/${file.repository.name}?path=${file.path}`, // Not all locations may actually exist, since the user defined them as a wildcard pattern. diff --git a/plugins/catalog-backend/src/providers/index.ts b/plugins/catalog-backend/src/modules/azure/index.ts similarity index 79% rename from plugins/catalog-backend/src/providers/index.ts rename to plugins/catalog-backend/src/modules/azure/index.ts index 3b4a1c4b7e..9c76336804 100644 --- a/plugins/catalog-backend/src/providers/index.ts +++ b/plugins/catalog-backend/src/modules/azure/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * Copyright 2022 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,8 +14,4 @@ * limitations under the License. */ -export type { - EntityProvider, - EntityProviderConnection, - EntityProviderMutation, -} from './types'; +export { AzureDevOpsDiscoveryProcessor } from './AzureDevOpsDiscoveryProcessor'; diff --git a/plugins/catalog-backend/src/ingestion/processors/azure/azure.test.ts b/plugins/catalog-backend/src/modules/azure/lib/azure.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/azure/azure.test.ts rename to plugins/catalog-backend/src/modules/azure/lib/azure.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/azure/azure.ts b/plugins/catalog-backend/src/modules/azure/lib/azure.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/azure/azure.ts rename to plugins/catalog-backend/src/modules/azure/lib/azure.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/azure/index.ts b/plugins/catalog-backend/src/modules/azure/lib/index.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/azure/index.ts rename to plugins/catalog-backend/src/modules/azure/lib/index.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/modules/bitbucket/BitbucketDiscoveryProcessor.test.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts rename to plugins/catalog-backend/src/modules/bitbucket/BitbucketDiscoveryProcessor.test.ts index 4a261386c0..857f999a70 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/bitbucket/BitbucketDiscoveryProcessor.test.ts @@ -13,18 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { getVoidLogger } from '@backstage/backend-common'; import { BitbucketDiscoveryProcessor } from './BitbucketDiscoveryProcessor'; import { ConfigReader } from '@backstage/config'; -import { - BitbucketRepository20, - PagedResponse, - PagedResponse20, -} from './bitbucket'; -import { LocationSpec } from './types'; -import { results } from './index'; import { RequestHandler, rest } from 'msw'; import { setupServer } from 'msw/node'; +import { BitbucketRepository20, PagedResponse, PagedResponse20 } from './lib'; +import { LocationSpec, processingResult } from '../../api'; const server = setupServer(); @@ -755,7 +751,7 @@ describe('BitbucketDiscoveryProcessor', () => { }), { parser: async function* customRepositoryParser({}) { - yield results.location({ + yield processingResult.location({ type: 'custom-location-type', target: 'custom-target', presence: 'optional', diff --git a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts b/plugins/catalog-backend/src/modules/bitbucket/BitbucketDiscoveryProcessor.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts rename to plugins/catalog-backend/src/modules/bitbucket/BitbucketDiscoveryProcessor.ts index 25f52b7dd7..daf24e2427 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/modules/bitbucket/BitbucketDiscoveryProcessor.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Logger } from 'winston'; import { Config } from '@backstage/config'; @@ -28,13 +29,13 @@ import { paginated20, BitbucketRepository, BitbucketRepository20, -} from './bitbucket'; +} from './lib'; import { CatalogProcessor, CatalogProcessorEmit, CatalogProcessorResult, LocationSpec, -} from './types'; +} from '../../api'; const DEFAULT_BRANCH = 'master'; const DEFAULT_CATALOG_LOCATION = '/catalog-info.yaml'; diff --git a/plugins/catalog-backend/src/modules/bitbucket/index.ts b/plugins/catalog-backend/src/modules/bitbucket/index.ts new file mode 100644 index 0000000000..0e39083520 --- /dev/null +++ b/plugins/catalog-backend/src/modules/bitbucket/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { BitbucketDiscoveryProcessor } from './BitbucketDiscoveryProcessor'; +export type { BitbucketRepositoryParser } from './lib'; diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts b/plugins/catalog-backend/src/modules/bitbucket/lib/BitbucketRepositoryParser.test.ts similarity index 94% rename from plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts rename to plugins/catalog-backend/src/modules/bitbucket/lib/BitbucketRepositoryParser.test.ts index e010865041..9f2d8b2daa 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts +++ b/plugins/catalog-backend/src/modules/bitbucket/lib/BitbucketRepositoryParser.test.ts @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { processingResult } from '../../../api'; import { defaultRepositoryParser } from './BitbucketRepositoryParser'; -import { results } from '../index'; describe('BitbucketRepositoryParser', () => { describe('defaultRepositoryParser', () => { @@ -23,7 +24,7 @@ describe('BitbucketRepositoryParser', () => { 'https://bitbucket.mycompany.com/projects/project-key/repos/repo-slug/browse'; const path = '/catalog-info.yaml'; const expected = [ - results.location({ + processingResult.location({ type: 'url', target: `${browseUrl}${path}`, presence: 'optional', diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts b/plugins/catalog-backend/src/modules/bitbucket/lib/BitbucketRepositoryParser.ts similarity index 62% rename from plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts rename to plugins/catalog-backend/src/modules/bitbucket/lib/BitbucketRepositoryParser.ts index 6671735f57..c72e560c21 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts +++ b/plugins/catalog-backend/src/modules/bitbucket/lib/BitbucketRepositoryParser.ts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { CatalogProcessorResult } from '../types'; -import { results } from '../index'; -import { Logger } from 'winston'; + import { BitbucketIntegration } from '@backstage/integration'; +import { Logger } from 'winston'; +import { CatalogProcessorResult, processingResult } from '../../../api'; /** * @public @@ -28,17 +28,14 @@ export type BitbucketRepositoryParser = (options: { logger: Logger; }) => AsyncIterable; -export const defaultRepositoryParser = async function* defaultRepositoryParser({ - target, -}: { - target: string; -}) { - yield results.location({ - type: 'url', - target: target, - // Not all locations may actually exist, since the user defined them as a wildcard pattern. - // Thus, we emit them as optional and let the downstream processor find them while not outputting - // an error if it couldn't. - presence: 'optional', - }); -}; +export const defaultRepositoryParser = + async function* defaultRepositoryParser(options: { target: string }) { + yield processingResult.location({ + type: 'url', + target: options.target, + // Not all locations may actually exist, since the user defined them as a wildcard pattern. + // Thus, we emit them as optional and let the downstream processor find them while not outputting + // an error if it couldn't. + presence: 'optional', + }); + }; diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts b/plugins/catalog-backend/src/modules/bitbucket/lib/client.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts rename to plugins/catalog-backend/src/modules/bitbucket/lib/client.ts index 0c132bb61b..65cd28d5ab 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts +++ b/plugins/catalog-backend/src/modules/bitbucket/lib/client.ts @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import fetch from 'node-fetch'; +import fetch from 'node-fetch'; import { BitbucketIntegrationConfig, getBitbucketRequestOptions, diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/index.ts b/plugins/catalog-backend/src/modules/bitbucket/lib/index.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/bitbucket/index.ts rename to plugins/catalog-backend/src/modules/bitbucket/lib/index.ts index 4b28c7f115..a819bb3c64 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/index.ts +++ b/plugins/catalog-backend/src/modules/bitbucket/lib/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { BitbucketClient, paginated, paginated20 } from './client'; export { defaultRepositoryParser } from './BitbucketRepositoryParser'; export type { PagedResponse, PagedResponse20 } from './client'; diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/types.ts b/plugins/catalog-backend/src/modules/bitbucket/lib/types.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/bitbucket/types.ts rename to plugins/catalog-backend/src/modules/bitbucket/lib/types.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts similarity index 98% rename from plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts rename to plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts index 8d6d1f8ff7..8cf250de2a 100644 --- a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.test.ts @@ -17,7 +17,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { CodeOwnersProcessor } from './CodeOwnersProcessor'; -import { LocationSpec } from './types'; +import { LocationSpec } from '../../api'; const mockCodeOwnersText = () => ` * @acme/team-foo @acme/team-bar diff --git a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts similarity index 95% rename from plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts rename to plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts index 93e3c1618a..6f6aee7161 100644 --- a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts +++ b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts @@ -22,11 +22,10 @@ import { ScmIntegrations, } from '@backstage/integration'; import { Logger } from 'winston'; -import { findCodeOwnerByTarget } from './codeowners'; -import { CatalogProcessor, LocationSpec } from './types'; +import { CatalogProcessor, LocationSpec } from '../../api'; +import { findCodeOwnerByTarget } from './lib'; const ALLOWED_KINDS = ['API', 'Component', 'Domain', 'Resource', 'System']; - const ALLOWED_LOCATION_TYPES = ['url']; /** @public */ diff --git a/plugins/catalog-backend/src/modules/codeowners/index.ts b/plugins/catalog-backend/src/modules/codeowners/index.ts new file mode 100644 index 0000000000..6b7569ff07 --- /dev/null +++ b/plugins/catalog-backend/src/modules/codeowners/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { CodeOwnersProcessor } from './CodeOwnersProcessor'; diff --git a/plugins/catalog-backend/src/ingestion/processors/codeowners/index.ts b/plugins/catalog-backend/src/modules/codeowners/lib/index.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/codeowners/index.ts rename to plugins/catalog-backend/src/modules/codeowners/lib/index.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/codeowners/read.test.ts b/plugins/catalog-backend/src/modules/codeowners/lib/read.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/codeowners/read.test.ts rename to plugins/catalog-backend/src/modules/codeowners/lib/read.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/codeowners/read.ts b/plugins/catalog-backend/src/modules/codeowners/lib/read.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/codeowners/read.ts rename to plugins/catalog-backend/src/modules/codeowners/lib/read.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/codeowners/resolve.test.ts b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/codeowners/resolve.test.ts rename to plugins/catalog-backend/src/modules/codeowners/lib/resolve.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/codeowners/resolve.ts b/plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/codeowners/resolve.ts rename to plugins/catalog-backend/src/modules/codeowners/lib/resolve.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/codeowners/scm.ts b/plugins/catalog-backend/src/modules/codeowners/lib/scm.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/codeowners/scm.ts rename to plugins/catalog-backend/src/modules/codeowners/lib/scm.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts b/plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.test.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.test.ts index 5f5b20d1c2..3911f7f743 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.test.ts @@ -17,7 +17,7 @@ import { Entity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; -import { LocationSpec } from './types'; +import { LocationSpec } from '../../api'; import { AnnotateLocationEntityProcessor } from './AnnotateLocationEntityProcessor'; describe('AnnotateLocationEntityProcessor', () => { diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.ts similarity index 96% rename from plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts rename to plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.ts index b8c31ed5a2..4af0b8cf61 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.ts @@ -25,7 +25,11 @@ import { } from '@backstage/catalog-model'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { identity, merge, pickBy } from 'lodash'; -import { CatalogProcessor, CatalogProcessorEmit, LocationSpec } from './types'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, +} from '../../api'; /** @public */ export class AnnotateLocationEntityProcessor implements CatalogProcessor { diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts b/plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.test.ts similarity index 98% rename from plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.test.ts index 311dd13789..1e553a44f8 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.test.ts @@ -16,7 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { AnnotateScmSlugEntityProcessor } from './AnnotateScmSlugEntityProcessor'; -import { LocationSpec } from './types'; +import { LocationSpec } from '../../api'; describe('AnnotateScmSlugEntityProcessor', () => { describe('github', () => { diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.ts similarity index 97% rename from plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts rename to plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.ts index 4d3b3aff6c..e44db4d44d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateScmSlugEntityProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.ts @@ -21,7 +21,7 @@ import { } from '@backstage/integration'; import parseGitUrl from 'git-url-parse'; import { identity, merge, pickBy } from 'lodash'; -import { CatalogProcessor, LocationSpec } from './types'; +import { CatalogProcessor, LocationSpec } from '../../api'; const GITHUB_ACTIONS_ANNOTATION = 'github.com/project-slug'; diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts similarity index 97% rename from plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts rename to plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts index 89b0bb1bb5..548cdf3115 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts @@ -52,8 +52,12 @@ import { TemplateEntityV1beta2, templateEntityV1beta2Validator, } from '@backstage/plugin-scaffolder-common'; -import * as result from './results'; -import { CatalogProcessor, CatalogProcessorEmit, LocationSpec } from './types'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '../../api'; /** @public */ export class BuiltinKindsEntityProcessor implements CatalogProcessor { @@ -107,7 +111,7 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { for (const target of [targets].flat()) { const targetRef = parseEntityRef(target, context); emit( - result.relation({ + processingResult.relation({ source: selfRef, type: outgoingRelation, target: { @@ -118,7 +122,7 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { }), ); emit( - result.relation({ + processingResult.relation({ source: { kind: targetRef.kind, namespace: targetRef.namespace, diff --git a/plugins/catalog-backend/src/providers/ConfigLocationEntityProvider.test.ts b/plugins/catalog-backend/src/modules/core/ConfigLocationEntityProvider.test.ts similarity index 98% rename from plugins/catalog-backend/src/providers/ConfigLocationEntityProvider.test.ts rename to plugins/catalog-backend/src/modules/core/ConfigLocationEntityProvider.test.ts index 0663ad8d04..d61342e3d0 100644 --- a/plugins/catalog-backend/src/providers/ConfigLocationEntityProvider.test.ts +++ b/plugins/catalog-backend/src/modules/core/ConfigLocationEntityProvider.test.ts @@ -17,7 +17,7 @@ import { ConfigReader } from '@backstage/config'; import path from 'path'; import { ConfigLocationEntityProvider } from './ConfigLocationEntityProvider'; -import { EntityProviderConnection } from './types'; +import { EntityProviderConnection } from '../../api'; describe('ConfigLocationEntityProvider', () => { it('should apply mutation with the correct paths in the config', async () => { diff --git a/plugins/catalog-backend/src/providers/ConfigLocationEntityProvider.ts b/plugins/catalog-backend/src/modules/core/ConfigLocationEntityProvider.ts similarity index 90% rename from plugins/catalog-backend/src/providers/ConfigLocationEntityProvider.ts rename to plugins/catalog-backend/src/modules/core/ConfigLocationEntityProvider.ts index 389cd8d9ff..9529a5b608 100644 --- a/plugins/catalog-backend/src/providers/ConfigLocationEntityProvider.ts +++ b/plugins/catalog-backend/src/modules/core/ConfigLocationEntityProvider.ts @@ -16,9 +16,9 @@ import { Config } from '@backstage/config'; import path from 'path'; -import { getEntityLocationRef } from '../processing/util'; -import { EntityProvider, EntityProviderConnection } from './types'; -import { locationSpecToLocationEntity } from '../util/conversion'; +import { getEntityLocationRef } from '../../processing/util'; +import { EntityProvider, EntityProviderConnection } from '../../api'; +import { locationSpecToLocationEntity } from '../../util/conversion'; export class ConfigLocationEntityProvider implements EntityProvider { constructor(private readonly config: Config) {} diff --git a/plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts b/plugins/catalog-backend/src/modules/core/DefaultLocationStore.test.ts similarity index 98% rename from plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts rename to plugins/catalog-backend/src/modules/core/DefaultLocationStore.test.ts index b9d129de5f..dfbc37f6c9 100644 --- a/plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts +++ b/plugins/catalog-backend/src/modules/core/DefaultLocationStore.test.ts @@ -15,7 +15,7 @@ */ import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { v4 as uuid } from 'uuid'; -import { applyDatabaseMigrations } from '../database/migrations'; +import { applyDatabaseMigrations } from '../../database/migrations'; import { DefaultLocationStore } from './DefaultLocationStore'; describe('DefaultLocationStore', () => { diff --git a/plugins/catalog-backend/src/providers/DefaultLocationStore.ts b/plugins/catalog-backend/src/modules/core/DefaultLocationStore.ts similarity index 93% rename from plugins/catalog-backend/src/providers/DefaultLocationStore.ts rename to plugins/catalog-backend/src/modules/core/DefaultLocationStore.ts index 9d5971508f..d637f4eda0 100644 --- a/plugins/catalog-backend/src/providers/DefaultLocationStore.ts +++ b/plugins/catalog-backend/src/modules/core/DefaultLocationStore.ts @@ -18,11 +18,11 @@ import { Location } from '@backstage/catalog-client'; import { ConflictError, NotFoundError } from '@backstage/errors'; import { Knex } from 'knex'; import { v4 as uuid } from 'uuid'; -import { DbLocationsRow } from '../database/tables'; -import { getEntityLocationRef } from '../processing/util'; -import { EntityProvider, EntityProviderConnection } from './types'; -import { locationSpecToLocationEntity } from '../util/conversion'; -import { LocationInput, LocationStore } from '../service'; +import { DbLocationsRow } from '../../database/tables'; +import { getEntityLocationRef } from '../../processing/util'; +import { EntityProvider, EntityProviderConnection } from '../../api'; +import { locationSpecToLocationEntity } from '../../util/conversion'; +import { LocationInput, LocationStore } from '../../service'; export class DefaultLocationStore implements LocationStore, EntityProvider { private _connection: EntityProviderConnection | undefined; diff --git a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/FileReaderProcessor.test.ts similarity index 94% rename from plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/FileReaderProcessor.test.ts index c2cce66bc6..a3eb5e4542 100644 --- a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/FileReaderProcessor.test.ts @@ -19,12 +19,16 @@ import { CatalogProcessorEntityResult, CatalogProcessorErrorResult, CatalogProcessorResult, -} from './types'; +} from '../../api'; import path from 'path'; -import { defaultEntityDataParser } from './util/parse'; +import { defaultEntityDataParser } from '../util/parse'; describe('FileReaderProcessor', () => { - const fixturesRoot = path.join(__dirname, '__fixtures__/fileReaderProcessor'); + const fixturesRoot = path.join( + __dirname, + '__fixtures__', + 'fileReaderProcessor', + ); it('should load from file', async () => { const processor = new FileReaderProcessor(); diff --git a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts b/plugins/catalog-backend/src/modules/core/FileReaderProcessor.ts similarity index 92% rename from plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts rename to plugins/catalog-backend/src/modules/core/FileReaderProcessor.ts index 996beff24a..66c6479fa7 100644 --- a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/FileReaderProcessor.ts @@ -18,13 +18,13 @@ import fs from 'fs-extra'; import g from 'glob'; import path from 'path'; import { promisify } from 'util'; -import * as result from './results'; import { CatalogProcessor, CatalogProcessorEmit, CatalogProcessorParser, LocationSpec, -} from './types'; + processingResult, +} from '../../api'; const glob = promisify(g); @@ -65,11 +65,11 @@ export class FileReaderProcessor implements CatalogProcessor { } } else if (!optional) { const message = `${location.type} ${location.target} does not exist`; - emit(result.notFoundError(location, message)); + emit(processingResult.notFoundError(location, message)); } } catch (e) { const message = `${location.type} ${location.target} could not be read, ${e}`; - emit(result.generalError(location, message)); + emit(processingResult.generalError(location, message)); } return true; diff --git a/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.test.ts b/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.test.ts similarity index 98% rename from plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/LocationEntityProcessor.test.ts index 83f1ca1925..92ba1fcebf 100644 --- a/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.test.ts @@ -21,7 +21,7 @@ import { } from '@backstage/integration'; import path from 'path'; import { toAbsoluteUrl } from './LocationEntityProcessor'; -import { LocationSpec } from './types'; +import { LocationSpec } from '../../api'; describe('LocationEntityProcessor', () => { describe('toAbsoluteUrl', () => { diff --git a/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.ts similarity index 92% rename from plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.ts rename to plugins/catalog-backend/src/modules/core/LocationEntityProcessor.ts index 533a366f57..c168f54bf1 100644 --- a/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.ts @@ -17,8 +17,12 @@ import { Entity, LocationEntity } from '@backstage/catalog-model'; import { ScmIntegrationRegistry } from '@backstage/integration'; import path from 'path'; -import * as result from './results'; -import { CatalogProcessor, CatalogProcessorEmit, LocationSpec } from './types'; +import { + processingResult, + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, +} from '../../api'; export function toAbsoluteUrl( integrations: ScmIntegrationRegistry, @@ -62,7 +66,7 @@ export class LocationEntityProcessor implements CatalogProcessor { const type = locationEntity.spec.type || location.type; if (type === 'file' && location.target.endsWith(path.sep)) { emit( - result.inputError( + processingResult.inputError( location, `LocationEntityProcessor cannot handle ${type} type location with target ${location.target} that ends with a path separator`, ), @@ -83,7 +87,7 @@ export class LocationEntityProcessor implements CatalogProcessor { location, maybeRelativeTarget, ); - emit(result.location({ type, target })); + emit(processingResult.location({ type, target })); } } diff --git a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts rename to plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts index 0d572a024d..ddd5f951db 100644 --- a/plugins/catalog-backend/src/ingestion/processors/PlaceholderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts @@ -19,7 +19,7 @@ import { Entity } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/types'; import { ScmIntegrationRegistry } from '@backstage/integration'; import yaml from 'yaml'; -import { CatalogProcessor, LocationSpec } from './types'; +import { CatalogProcessor, LocationSpec } from '../../api'; /** @public */ export type PlaceholderResolverRead = (url: string) => Promise; diff --git a/plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts b/plugins/catalog-backend/src/modules/core/StaticLocationProcessor.ts similarity index 91% rename from plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts rename to plugins/catalog-backend/src/modules/core/StaticLocationProcessor.ts index 1c2c27f425..e22d011695 100644 --- a/plugins/catalog-backend/src/ingestion/processors/StaticLocationProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/StaticLocationProcessor.ts @@ -15,8 +15,11 @@ */ import { Config } from '@backstage/config'; -import * as result from './results'; -import { CatalogProcessorEmit, LocationSpec } from './types'; +import { + processingResult, + CatalogProcessorEmit, + LocationSpec, +} from '../../api'; /** * @deprecated no longer in use, replaced by the ConfigLocationEntityProvider. @@ -48,7 +51,7 @@ export class StaticLocationProcessor implements StaticLocationProcessor { } for (const staticLocation of this.staticLocations) { - emit(result.location(staticLocation)); + emit(processingResult.location(staticLocation)); } return true; diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts similarity index 98% rename from plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts rename to plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts index b7dc0084f2..a96d392c52 100644 --- a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts @@ -28,9 +28,9 @@ import { CatalogProcessorEntityResult, CatalogProcessorErrorResult, CatalogProcessorResult, -} from './types'; +} from '../../api'; +import { defaultEntityDataParser } from '../util/parse'; import { UrlReaderProcessor } from './UrlReaderProcessor'; -import { defaultEntityDataParser } from './util/parse'; describe('UrlReaderProcessor', () => { const mockApiOrigin = 'http://localhost'; diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts similarity index 95% rename from plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts rename to plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts index c2be454bcf..7f27bcd19d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts @@ -20,7 +20,6 @@ import { assertError } from '@backstage/errors'; import parseGitUrl from 'git-url-parse'; import limiterFactory from 'p-limit'; import { Logger } from 'winston'; -import * as result from './results'; import { CatalogProcessor, CatalogProcessorCache, @@ -29,7 +28,8 @@ import { CatalogProcessorParser, CatalogProcessorResult, LocationSpec, -} from './types'; + processingResult, +} from '../../api'; const CACHE_KEY = 'v1'; @@ -102,10 +102,10 @@ export class UrlReaderProcessor implements CatalogProcessor { } } else if (error.name === 'NotFoundError') { if (!optional) { - emit(result.notFoundError(location, message)); + emit(processingResult.notFoundError(location, message)); } } else { - emit(result.generalError(location, message)); + emit(processingResult.generalError(location, message)); } } diff --git a/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt b/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt new file mode 100644 index 0000000000..7470c0e8a3 --- /dev/null +++ b/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/awsS3/awsS3-mock-object.txt @@ -0,0 +1 @@ +site_name: Test diff --git a/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/component.yaml b/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/component.yaml similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/component.yaml rename to plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/component.yaml diff --git a/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/dir/api.yaml b/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/dir/api.yaml similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/dir/api.yaml rename to plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/dir/api.yaml diff --git a/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/test.txt b/plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/test.txt similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/test.txt rename to plugins/catalog-backend/src/modules/core/__fixtures__/fileReaderProcessor/test.txt diff --git a/plugins/catalog-backend/src/ingestion/processors/index.ts b/plugins/catalog-backend/src/modules/core/index.ts similarity index 63% rename from plugins/catalog-backend/src/ingestion/processors/index.ts rename to plugins/catalog-backend/src/modules/core/index.ts index 84a92fb1ba..9d582bfb34 100644 --- a/plugins/catalog-backend/src/ingestion/processors/index.ts +++ b/plugins/catalog-backend/src/modules/core/index.ts @@ -14,20 +14,10 @@ * limitations under the License. */ -import * as results from './results'; - export { AnnotateLocationEntityProcessor } from './AnnotateLocationEntityProcessor'; export { AnnotateScmSlugEntityProcessor } from './AnnotateScmSlugEntityProcessor'; -export { AwsS3DiscoveryProcessor } from './AwsS3DiscoveryProcessor'; -export { BitbucketDiscoveryProcessor } from './BitbucketDiscoveryProcessor'; export { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor'; -export { CodeOwnersProcessor } from './CodeOwnersProcessor'; export { FileReaderProcessor } from './FileReaderProcessor'; -export { GithubDiscoveryProcessor } from './GithubDiscoveryProcessor'; -export { AzureDevOpsDiscoveryProcessor } from './AzureDevOpsDiscoveryProcessor'; -export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor'; -export { GithubMultiOrgReaderProcessor } from './GithubMultiOrgReaderProcessor'; -export { GitLabDiscoveryProcessor } from './GitLabDiscoveryProcessor'; export { LocationEntityProcessor } from './LocationEntityProcessor'; export type { LocationEntityProcessorOptions } from './LocationEntityProcessor'; export { PlaceholderProcessor } from './PlaceholderProcessor'; @@ -39,10 +29,5 @@ export type { PlaceholderResolverResolveUrl, } from './PlaceholderProcessor'; export { StaticLocationProcessor } from './StaticLocationProcessor'; -export * from './types'; export { UrlReaderProcessor } from './UrlReaderProcessor'; -export { parseEntityYaml } from './util/parse'; -export { results }; - -export type { BitbucketRepositoryParser } from './bitbucket'; -export type { GithubMultiOrgConfig } from './github'; +export { parseEntityYaml } from '../util/parse'; diff --git a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.test.ts b/plugins/catalog-backend/src/modules/github/GitHubOrgEntityProvider.test.ts similarity index 97% rename from plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.test.ts rename to plugins/catalog-backend/src/modules/github/GitHubOrgEntityProvider.test.ts index e7aec1520d..dd3bf490fb 100644 --- a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.test.ts +++ b/plugins/catalog-backend/src/modules/github/GitHubOrgEntityProvider.test.ts @@ -20,10 +20,12 @@ import { GithubCredentialsProvider, GitHubIntegrationConfig, } from '@backstage/integration'; -import { GitHubOrgEntityProvider } from '.'; -import { EntityProviderConnection } from '../../providers'; -import { withLocations } from './GitHubOrgEntityProvider'; import { graphql } from '@octokit/graphql'; +import { EntityProviderConnection } from '../../api'; +import { + GitHubOrgEntityProvider, + withLocations, +} from './GitHubOrgEntityProvider'; jest.mock('@octokit/graphql'); diff --git a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts b/plugins/catalog-backend/src/modules/github/GitHubOrgEntityProvider.ts similarity index 97% rename from plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts rename to plugins/catalog-backend/src/modules/github/GitHubOrgEntityProvider.ts index a20f7fdb0b..76389968ae 100644 --- a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts +++ b/plugins/catalog-backend/src/modules/github/GitHubOrgEntityProvider.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { ANNOTATION_LOCATION, ANNOTATION_ORIGIN_LOCATION, @@ -29,13 +30,13 @@ import { import { graphql } from '@octokit/graphql'; import { merge } from 'lodash'; import { Logger } from 'winston'; -import { EntityProvider, EntityProviderConnection } from '../../providers'; +import { EntityProvider, EntityProviderConnection } from '../../api'; import { getOrganizationTeams, getOrganizationUsers, parseGitHubOrgUrl, -} from '../processors/github'; -import { assignGroupsToUsers, buildOrgHierarchy } from '../processors/util/org'; +} from './lib'; +import { assignGroupsToUsers, buildOrgHierarchy } from '../util/org'; // TODO: Consider supporting an (optional) webhook that reacts on org changes /** @public */ diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/modules/github/GithubDiscoveryProcessor.test.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts rename to plugins/catalog-backend/src/modules/github/GithubDiscoveryProcessor.test.ts index 55e5560451..5440539b27 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/github/GithubDiscoveryProcessor.test.ts @@ -15,16 +15,16 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { GithubDiscoveryProcessor, parseUrl } from './GithubDiscoveryProcessor'; -import { getOrganizationRepositories } from './github'; -import { LocationSpec } from './types'; import { ConfigReader } from '@backstage/config'; import { - ScmIntegrations, DefaultGithubCredentialsProvider, + ScmIntegrations, } from '@backstage/integration'; +import { LocationSpec } from '../../api'; +import { GithubDiscoveryProcessor, parseUrl } from './GithubDiscoveryProcessor'; +import { getOrganizationRepositories } from './lib'; -jest.mock('./github'); +jest.mock('./lib'); const mockGetOrganizationRepositories = getOrganizationRepositories as jest.MockedFunction< typeof getOrganizationRepositories diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts b/plugins/catalog-backend/src/modules/github/GithubDiscoveryProcessor.ts similarity index 96% rename from plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts rename to plugins/catalog-backend/src/modules/github/GithubDiscoveryProcessor.ts index e790134011..4aff470f52 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/modules/github/GithubDiscoveryProcessor.ts @@ -23,9 +23,13 @@ import { } from '@backstage/integration'; import { graphql } from '@octokit/graphql'; import { Logger } from 'winston'; -import { getOrganizationRepositories } from './github'; -import * as results from './results'; -import { CatalogProcessor, CatalogProcessorEmit, LocationSpec } from './types'; +import { getOrganizationRepositories } from './lib'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '../../api'; /** * Extracts repositories out of a GitHub org. @@ -141,7 +145,7 @@ export class GithubDiscoveryProcessor implements CatalogProcessor { const path = `/blob/${branchName}${catalogPath}`; emit( - results.location({ + processingResult.location({ type: 'url', target: `${repository.url}${path}`, // Not all locations may actually exist, since the user defined them as a wildcard pattern. diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts b/plugins/catalog-backend/src/modules/github/GithubMultiOrgReaderProcessor.ts similarity index 95% rename from plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts rename to plugins/catalog-backend/src/modules/github/GithubMultiOrgReaderProcessor.ts index 383b82c019..339119137a 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/github/GithubMultiOrgReaderProcessor.ts @@ -30,10 +30,14 @@ import { getOrganizationUsers, GithubMultiOrgConfig, readGithubMultiOrgConfig, -} from './github'; -import * as results from './results'; -import { CatalogProcessor, CatalogProcessorEmit, LocationSpec } from './types'; -import { buildOrgHierarchy } from './util/org'; +} from './lib'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '../../api'; +import { buildOrgHierarchy } from '../util/org'; /** * @alpha @@ -158,7 +162,7 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { buildOrgHierarchy(groups); for (const group of groups) { - emit(results.entity(location, group)); + emit(processingResult.entity(location, group)); } } catch (e) { this.logger.error( @@ -169,7 +173,7 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { const allUsers = Array.from(allUsersMap.values()); for (const user of allUsers) { - emit(results.entity(location, user)); + emit(processingResult.entity(location, user)); } return true; diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts b/plugins/catalog-backend/src/modules/github/GithubOrgReaderProcessor.test.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts rename to plugins/catalog-backend/src/modules/github/GithubOrgReaderProcessor.test.ts index c01db88d3a..9ce278ac44 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/github/GithubOrgReaderProcessor.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { @@ -21,7 +22,7 @@ import { } from '@backstage/integration'; import { graphql } from '@octokit/graphql'; import { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor'; -import { LocationSpec } from './types'; +import { LocationSpec } from '../../api'; jest.mock('@octokit/graphql'); diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts b/plugins/catalog-backend/src/modules/github/GithubOrgReaderProcessor.ts similarity index 92% rename from plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts rename to plugins/catalog-backend/src/modules/github/GithubOrgReaderProcessor.ts index 8b5914cc80..e85e7b874d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/github/GithubOrgReaderProcessor.ts @@ -28,10 +28,14 @@ import { getOrganizationTeams, getOrganizationUsers, parseGitHubOrgUrl, -} from './github'; -import * as results from './results'; -import { CatalogProcessor, CatalogProcessorEmit, LocationSpec } from './types'; -import { assignGroupsToUsers, buildOrgHierarchy } from './util/org'; +} from './lib'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '../../api'; +import { assignGroupsToUsers, buildOrgHierarchy } from '../util/org'; type GraphQL = typeof graphql; @@ -106,10 +110,10 @@ export class GithubOrgReaderProcessor implements CatalogProcessor { // Done! for (const group of groups) { - emit(results.entity(location, group)); + emit(processingResult.entity(location, group)); } for (const user of users) { - emit(results.entity(location, user)); + emit(processingResult.entity(location, user)); } return true; diff --git a/plugins/catalog-backend/src/modules/github/index.ts b/plugins/catalog-backend/src/modules/github/index.ts new file mode 100644 index 0000000000..7958818096 --- /dev/null +++ b/plugins/catalog-backend/src/modules/github/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { GithubDiscoveryProcessor } from './GithubDiscoveryProcessor'; +export { GithubMultiOrgReaderProcessor } from './GithubMultiOrgReaderProcessor'; +export { GitHubOrgEntityProvider } from './GitHubOrgEntityProvider'; +export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor'; +export type { GithubMultiOrgConfig } from './lib'; diff --git a/plugins/catalog-backend/src/ingestion/processors/github/config.test.ts b/plugins/catalog-backend/src/modules/github/lib/config.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/github/config.test.ts rename to plugins/catalog-backend/src/modules/github/lib/config.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/github/config.ts b/plugins/catalog-backend/src/modules/github/lib/config.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/github/config.ts rename to plugins/catalog-backend/src/modules/github/lib/config.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/github/github.test.ts b/plugins/catalog-backend/src/modules/github/lib/github.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/github/github.test.ts rename to plugins/catalog-backend/src/modules/github/lib/github.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/github/github.ts b/plugins/catalog-backend/src/modules/github/lib/github.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/github/github.ts rename to plugins/catalog-backend/src/modules/github/lib/github.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/github/index.ts b/plugins/catalog-backend/src/modules/github/lib/index.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/github/index.ts rename to plugins/catalog-backend/src/modules/github/lib/index.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/github/util.test.ts b/plugins/catalog-backend/src/modules/github/lib/util.test.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/github/util.test.ts rename to plugins/catalog-backend/src/modules/github/lib/util.test.ts index 02f54e0675..c73c11bd2d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/util.test.ts +++ b/plugins/catalog-backend/src/modules/github/lib/util.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { parseGitHubOrgUrl } from './util'; describe('parseGitHubOrgUrl', () => { diff --git a/plugins/catalog-backend/src/ingestion/processors/github/util.ts b/plugins/catalog-backend/src/modules/github/lib/util.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/github/util.ts rename to plugins/catalog-backend/src/modules/github/lib/util.ts index d8df376038..a38225ff98 100644 --- a/plugins/catalog-backend/src/ingestion/processors/github/util.ts +++ b/plugins/catalog-backend/src/modules/github/lib/util.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export function parseGitHubOrgUrl(urlString: string): { org: string } { const path = new URL(urlString).pathname.substr(1).split('/'); diff --git a/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/modules/gitlab/GitLabDiscoveryProcessor.test.ts similarity index 99% rename from plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.test.ts rename to plugins/catalog-backend/src/modules/gitlab/GitLabDiscoveryProcessor.test.ts index 8261690e8f..cc0f368aee 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/gitlab/GitLabDiscoveryProcessor.test.ts @@ -19,8 +19,8 @@ import { getVoidLogger } from '@backstage/backend-common'; import { GitLabDiscoveryProcessor, parseUrl } from './GitLabDiscoveryProcessor'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; -import { GitLabProject } from './gitlab'; -import { LocationSpec } from './types'; +import { GitLabProject } from './lib'; +import { LocationSpec } from '../../api'; const server = setupServer(); diff --git a/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.ts b/plugins/catalog-backend/src/modules/gitlab/GitLabDiscoveryProcessor.ts similarity index 92% rename from plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.ts rename to plugins/catalog-backend/src/modules/gitlab/GitLabDiscoveryProcessor.ts index b534d50044..d3916f45d1 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/modules/gitlab/GitLabDiscoveryProcessor.ts @@ -20,9 +20,13 @@ import { ScmIntegrations, } from '@backstage/integration'; import { Logger } from 'winston'; -import * as results from './results'; -import { CatalogProcessor, CatalogProcessorEmit, LocationSpec } from './types'; -import { GitLabClient, GitLabProject, paginated } from './gitlab'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '../../api'; +import { GitLabClient, GitLabProject, paginated } from './lib'; import { CacheClient, CacheManager, @@ -95,12 +99,12 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { page: 1, }); - const result: Result = { + const res: Result = { scanned: 0, matches: [], }; for await (const project of projects) { - result.scanned++; + res.scanned++; if (project.archived) { continue; @@ -110,14 +114,14 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { continue; } - result.matches.push(project); + res.matches.push(project); } - for (const project of result.matches) { + for (const project of res.matches) { const project_branch = branch === '*' ? project.default_branch : branch; emit( - results.location({ + processingResult.location({ type: 'url', // The format expected by the GitLabUrlReader: // https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath @@ -133,7 +137,7 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1); this.logger.debug( - `Read ${result.scanned} GitLab repositories in ${duration} seconds`, + `Read ${res.scanned} GitLab repositories in ${duration} seconds`, ); return true; diff --git a/plugins/catalog-backend/src/modules/gitlab/index.ts b/plugins/catalog-backend/src/modules/gitlab/index.ts new file mode 100644 index 0000000000..f42a901f8f --- /dev/null +++ b/plugins/catalog-backend/src/modules/gitlab/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { GitLabDiscoveryProcessor } from './GitLabDiscoveryProcessor'; diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts b/plugins/catalog-backend/src/modules/gitlab/lib/client.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts rename to plugins/catalog-backend/src/modules/gitlab/lib/client.test.ts index b249137740..a7c0d5161f 100644 --- a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts +++ b/plugins/catalog-backend/src/modules/gitlab/lib/client.test.ts @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { ConfigReader } from '@backstage/config'; import { setupRequestMockHandlers } from '@backstage/test-utils'; import { readGitLabIntegrationConfig } from '@backstage/integration'; import { getVoidLogger } from '@backstage/backend-common'; import { rest } from 'msw'; import { setupServer, SetupServerApi } from 'msw/node'; - import { GitLabClient, paginated } from './client'; const server = setupServer(); diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts b/plugins/catalog-backend/src/modules/gitlab/lib/client.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts rename to plugins/catalog-backend/src/modules/gitlab/lib/client.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/index.ts b/plugins/catalog-backend/src/modules/gitlab/lib/index.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/gitlab/index.ts rename to plugins/catalog-backend/src/modules/gitlab/lib/index.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/types.ts b/plugins/catalog-backend/src/modules/gitlab/lib/types.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/gitlab/types.ts rename to plugins/catalog-backend/src/modules/gitlab/lib/types.ts diff --git a/plugins/catalog-backend/src/modules/index.ts b/plugins/catalog-backend/src/modules/index.ts new file mode 100644 index 0000000000..6acf057ca3 --- /dev/null +++ b/plugins/catalog-backend/src/modules/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 './aws'; +export * from './azure'; +export * from './bitbucket'; +export * from './codeowners'; +export * from './core'; +export * from './github'; +export * from './gitlab'; diff --git a/plugins/catalog-backend/src/ingestion/processors/util/org.test.ts b/plugins/catalog-backend/src/modules/util/org.test.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/util/org.test.ts rename to plugins/catalog-backend/src/modules/util/org.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/util/org.ts b/plugins/catalog-backend/src/modules/util/org.ts similarity index 100% rename from plugins/catalog-backend/src/ingestion/processors/util/org.ts rename to plugins/catalog-backend/src/modules/util/org.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/util/parse.test.ts b/plugins/catalog-backend/src/modules/util/parse.test.ts similarity index 91% rename from plugins/catalog-backend/src/ingestion/processors/util/parse.test.ts rename to plugins/catalog-backend/src/modules/util/parse.test.ts index a42d160725..e87de1c9a3 100644 --- a/plugins/catalog-backend/src/ingestion/processors/util/parse.test.ts +++ b/plugins/catalog-backend/src/modules/util/parse.test.ts @@ -15,7 +15,7 @@ */ import { parseEntityYaml } from './parse'; -import * as result from '../results'; +import { processingResult } from '../../api'; const testLoc = { target: 'my-loc-target', @@ -47,7 +47,7 @@ describe('parseEntityYaml', () => { ); expect(results).toEqual([ - result.entity(testLoc, { + processingResult.entity(testLoc, { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { @@ -92,7 +92,7 @@ describe('parseEntityYaml', () => { ); expect(results).toEqual([ - result.entity(testLoc, { + processingResult.entity(testLoc, { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { @@ -102,7 +102,7 @@ describe('parseEntityYaml', () => { type: 'website', }, }), - result.entity(testLoc, { + processingResult.entity(testLoc, { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { @@ -137,7 +137,7 @@ describe('parseEntityYaml', () => { ); expect(results).toEqual([ - result.entity(testLoc, { + processingResult.entity(testLoc, { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { @@ -157,7 +157,7 @@ describe('parseEntityYaml', () => { // Parse errors are always per document expect(results).toEqual([ - result.generalError( + processingResult.generalError( testLoc, 'YAML error at my-loc-type:my-loc-target, YAMLSemanticError: Plain value cannot start with reserved character `', ), @@ -186,7 +186,7 @@ describe('parseEntityYaml', () => { ); expect(results).toEqual([ - result.entity(testLoc, { + processingResult.entity(testLoc, { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', metadata: { @@ -196,7 +196,7 @@ describe('parseEntityYaml', () => { type: 'website', }, }), - result.generalError( + processingResult.generalError( testLoc, 'YAML error at my-loc-type:my-loc-target, YAMLSemanticError: Nested mappings are not allowed in compact mappings', ), @@ -209,7 +209,10 @@ describe('parseEntityYaml', () => { ); expect(results).toEqual([ - result.generalError(testLoc, 'Expected object at root, got string'), + processingResult.generalError( + testLoc, + 'Expected object at root, got string', + ), ]); }); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/util/parse.ts b/plugins/catalog-backend/src/modules/util/parse.ts similarity index 87% rename from plugins/catalog-backend/src/ingestion/processors/util/parse.ts rename to plugins/catalog-backend/src/modules/util/parse.ts index 0beaf1cd4c..65fad5986f 100644 --- a/plugins/catalog-backend/src/ingestion/processors/util/parse.ts +++ b/plugins/catalog-backend/src/modules/util/parse.ts @@ -17,12 +17,12 @@ import { Entity, stringifyLocationRef } from '@backstage/catalog-model'; import lodash from 'lodash'; import yaml from 'yaml'; -import * as result from '../results'; import { CatalogProcessorParser, CatalogProcessorResult, LocationSpec, -} from '../types'; + processingResult, +} from '../../api'; /** @public */ export function* parseEntityYaml( @@ -35,7 +35,7 @@ export function* parseEntityYaml( } catch (e) { const loc = stringifyLocationRef(location); const message = `Failed to parse YAML at ${loc}, ${e}`; - yield result.generalError(location, message); + yield processingResult.generalError(location, message); return; } @@ -43,17 +43,17 @@ export function* parseEntityYaml( if (document.errors?.length) { const loc = stringifyLocationRef(location); const message = `YAML error at ${loc}, ${document.errors[0]}`; - yield result.generalError(location, message); + yield processingResult.generalError(location, message); } else { const json = document.toJSON(); if (lodash.isPlainObject(json)) { - yield result.entity(location, json as Entity); + yield processingResult.entity(location, json as Entity); } else if (json === null) { // Ignore null values, these happen if there is an empty document in the // YAML file, for example if --- is added to the end of the file. } else { const message = `Expected object at root, got ${typeof json}`; - yield result.generalError(location, message); + yield processingResult.generalError(location, message); } } } diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts index 0853484408..6e494d784e 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts @@ -29,11 +29,11 @@ import { CatalogProcessorEmit, CatalogProcessorParser, LocationSpec, - results, -} from '../ingestion'; + processingResult, +} from '../api'; import { CatalogRulesEnforcer } from '../ingestion/CatalogRules'; import { DefaultCatalogProcessingOrchestrator } from './DefaultCatalogProcessingOrchestrator'; -import { defaultEntityDataParser } from '../ingestion/processors/util/parse'; +import { defaultEntityDataParser } from '../modules/util/parse'; import { ConfigReader } from '@backstage/config'; class FooBarProcessor implements CatalogProcessor { @@ -51,7 +51,7 @@ class FooBarProcessor implements CatalogProcessor { ) { if (await cache.get('emit')) { emit( - results.entity( + processingResult.entity( { type: 'url', target: './new-place' }, { apiVersion: 'my-api/v1', @@ -63,7 +63,7 @@ class FooBarProcessor implements CatalogProcessor { ), ); emit( - results.relation({ + processingResult.relation({ type: 'my-type', source: { kind: 'foobar', name: 'my-source', namespace: 'default' }, target: { kind: 'foobar', name: 'my-target', namespace: 'default' }, @@ -211,7 +211,7 @@ describe('DefaultCatalogProcessingOrchestrator', () => { getProcessorName: jest.fn(), validateEntityKind: jest.fn(async () => true), readLocation: jest.fn(async (_l, _o, emit) => { - emit(results.entity({ type: 't', target: 't' }, entity)); + emit(processingResult.entity({ type: 't', target: 't' }, entity)); return true; }), }; diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts index 9649bc937b..c050779a8b 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts @@ -36,8 +36,8 @@ import { CatalogProcessor, CatalogProcessorParser, LocationSpec, -} from '../ingestion/processors'; -import * as results from '../ingestion/processors/results'; + processingResult, +} from '../api'; import { CatalogProcessingOrchestrator, EntityProcessingRequest, @@ -178,13 +178,13 @@ export class DefaultCatalogProcessingOrchestrator entity: Entity, context: Context, ): Promise { - let result = entity; + let res = entity; for (const processor of this.options.processors) { if (processor.preProcessEntity) { try { - result = await processor.preProcessEntity( - result, + res = await processor.preProcessEntity( + res, context.location, context.collector.onEmit, context.originLocation, @@ -199,7 +199,7 @@ export class DefaultCatalogProcessingOrchestrator } } - return result; + return res; } /** @@ -295,7 +295,7 @@ export class DefaultCatalogProcessingOrchestrator for (const maybeRelativeTarget of targets) { if (type === 'file' && maybeRelativeTarget.endsWith(path.sep)) { context.collector.onEmit( - results.inputError( + processingResult.inputError( context.location, `LocationEntityProcessor cannot handle ${type} type location with target ${context.location.target} that ends with a path separator`, ), @@ -351,13 +351,13 @@ export class DefaultCatalogProcessingOrchestrator entity: Entity, context: Context, ): Promise { - let result = entity; + let res = entity; for (const processor of this.options.processors) { if (processor.postProcessEntity) { try { - result = await processor.postProcessEntity( - result, + res = await processor.postProcessEntity( + res, context.location, context.collector.onEmit, context.cache.forProcessor(processor), @@ -371,6 +371,6 @@ export class DefaultCatalogProcessingOrchestrator } } - return result; + return res; } } diff --git a/plugins/catalog-backend/src/processing/ProcessorCacheManager.test.ts b/plugins/catalog-backend/src/processing/ProcessorCacheManager.test.ts index 71631b13d8..602557a224 100644 --- a/plugins/catalog-backend/src/processing/ProcessorCacheManager.test.ts +++ b/plugins/catalog-backend/src/processing/ProcessorCacheManager.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { CatalogProcessor } from '../ingestion/processors'; +import { CatalogProcessor } from '../api'; import { ProcessorCacheManager } from './ProcessorCacheManager'; class MyProcessor implements CatalogProcessor { diff --git a/plugins/catalog-backend/src/processing/ProcessorCacheManager.ts b/plugins/catalog-backend/src/processing/ProcessorCacheManager.ts index ab95fcedb7..e54641f572 100644 --- a/plugins/catalog-backend/src/processing/ProcessorCacheManager.ts +++ b/plugins/catalog-backend/src/processing/ProcessorCacheManager.ts @@ -15,8 +15,7 @@ */ import { JsonObject, JsonValue } from '@backstage/types'; -import { CatalogProcessor } from '../ingestion/processors'; -import { CatalogProcessorCache } from '../ingestion/processors/types'; +import { CatalogProcessor, CatalogProcessorCache } from '../api'; import { isObject } from './util'; class SingleProcessorSubCache implements CatalogProcessorCache { diff --git a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts index 351e68882b..c3cc6e288b 100644 --- a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts +++ b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts @@ -22,9 +22,9 @@ import { } from '@backstage/catalog-model'; import { assertError } from '@backstage/errors'; import { Logger } from 'winston'; -import { CatalogProcessorResult } from '../ingestion'; +import { CatalogProcessorResult, EntityRelationSpec } from '../api'; import { locationSpecToLocationEntity } from '../util/conversion'; -import { DeferredEntity, EntityRelationSpec } from './types'; +import { DeferredEntity } from './types'; import { getEntityLocationRef, getEntityOriginLocationRef, diff --git a/plugins/catalog-backend/src/processing/connectEntityProviders.ts b/plugins/catalog-backend/src/processing/connectEntityProviders.ts index 02bdc2aee3..d7015ea52e 100644 --- a/plugins/catalog-backend/src/processing/connectEntityProviders.ts +++ b/plugins/catalog-backend/src/processing/connectEntityProviders.ts @@ -23,7 +23,7 @@ import { EntityProvider, EntityProviderConnection, EntityProviderMutation, -} from '../providers/types'; +} from '../api'; class Connection implements EntityProviderConnection { readonly validateEntityEnvelope = entityEnvelopeSchemaValidator(); diff --git a/plugins/catalog-backend/src/processing/index.ts b/plugins/catalog-backend/src/processing/index.ts index 83f8a9f566..dc8b07eceb 100644 --- a/plugins/catalog-backend/src/processing/index.ts +++ b/plugins/catalog-backend/src/processing/index.ts @@ -19,7 +19,6 @@ export type { CatalogProcessingEngine, EntityProcessingRequest, EntityProcessingResult, - EntityRelationSpec, DeferredEntity, } from './types'; export { DefaultCatalogProcessingOrchestrator } from './DefaultCatalogProcessingOrchestrator'; diff --git a/plugins/catalog-backend/src/processing/types.ts b/plugins/catalog-backend/src/processing/types.ts index b732df297e..484cd73939 100644 --- a/plugins/catalog-backend/src/processing/types.ts +++ b/plugins/catalog-backend/src/processing/types.ts @@ -14,30 +14,9 @@ * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { JsonObject } from '@backstage/types'; - -/** - * Holds the relation data for entities. - * - * @public - */ -export type EntityRelationSpec = { - /** - * The source entity of this relation. - */ - source: EntityName; - - /** - * The type of the relation. - */ - type: string; - - /** - * The target entity of this relation. - */ - target: EntityName; -}; +import { EntityRelationSpec } from '../api'; /** * The request to process an entity. diff --git a/plugins/catalog-backend/src/processing/util.ts b/plugins/catalog-backend/src/processing/util.ts index 94634f794a..b1948cf559 100644 --- a/plugins/catalog-backend/src/processing/util.ts +++ b/plugins/catalog-backend/src/processing/util.ts @@ -27,7 +27,7 @@ import { JsonObject, JsonValue } from '@backstage/types'; import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; import path from 'path'; -import { LocationSpec } from '../ingestion'; +import { LocationSpec } from '../api'; export function isLocationEntity(entity: Entity): entity is LocationEntity { return entity.kind === 'Location'; diff --git a/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts b/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts index 819451d854..17dfb58c54 100644 --- a/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts +++ b/plugins/catalog-backend/src/service/AuthorizedRefreshService.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { NotAllowedError } from '@backstage/errors'; import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common'; import { diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 9004cb4c56..f62377decb 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -38,12 +38,15 @@ import { Router } from 'express'; import lodash, { keyBy } from 'lodash'; import { EntitiesCatalog, EntitiesSearchFilter } from '../catalog'; +import { + CatalogProcessor, + CatalogProcessorParser, + EntityProvider, +} from '../api'; import { AnnotateLocationEntityProcessor, BitbucketDiscoveryProcessor, BuiltinKindsEntityProcessor, - CatalogProcessor, - CatalogProcessorParser, CodeOwnersProcessor, FileReaderProcessor, AzureDevOpsDiscoveryProcessor, @@ -53,23 +56,22 @@ import { PlaceholderProcessor, PlaceholderResolver, UrlReaderProcessor, -} from '../ingestion'; +} from '../modules'; +import { ConfigLocationEntityProvider } from '../modules/core/ConfigLocationEntityProvider'; +import { DefaultLocationStore } from '../modules/core/DefaultLocationStore'; import { RepoLocationAnalyzer } from '../ingestion/LocationAnalyzer'; import { jsonPlaceholderResolver, textPlaceholderResolver, yamlPlaceholderResolver, -} from '../ingestion/processors/PlaceholderProcessor'; -import { defaultEntityDataParser } from '../ingestion/processors/util/parse'; +} from '../modules/core/PlaceholderProcessor'; +import { defaultEntityDataParser } from '../modules/util/parse'; import { LocationAnalyzer } from '../ingestion/types'; -import { EntityProvider } from '../providers/types'; import { CatalogProcessingEngine } from '../processing/types'; -import { ConfigLocationEntityProvider } from '../providers/ConfigLocationEntityProvider'; import { DefaultProcessingDatabase } from '../database/DefaultProcessingDatabase'; import { applyDatabaseMigrations } from '../database/migrations'; import { DefaultCatalogProcessingEngine } from '../processing/DefaultCatalogProcessingEngine'; import { DefaultLocationService } from './DefaultLocationService'; -import { DefaultLocationStore } from '../providers/DefaultLocationStore'; import { DefaultEntitiesCatalog } from './DefaultEntitiesCatalog'; import { DefaultCatalogProcessingOrchestrator } from '../processing/DefaultCatalogProcessingOrchestrator'; import { Stitcher } from '../stitching/Stitcher'; diff --git a/plugins/catalog-backend/src/service/DefaultLocationService.ts b/plugins/catalog-backend/src/service/DefaultLocationService.ts index c6b5eaf162..7a1b923068 100644 --- a/plugins/catalog-backend/src/service/DefaultLocationService.ts +++ b/plugins/catalog-backend/src/service/DefaultLocationService.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Entity, ANNOTATION_LOCATION, diff --git a/plugins/catalog-backend/src/util/conversion.ts b/plugins/catalog-backend/src/util/conversion.ts index 75ed4f5e96..63771d1028 100644 --- a/plugins/catalog-backend/src/util/conversion.ts +++ b/plugins/catalog-backend/src/util/conversion.ts @@ -23,7 +23,7 @@ import { stringifyLocationRef, } from '@backstage/catalog-model'; import { createHash } from 'crypto'; -import { LocationSpec } from '../ingestion'; +import { LocationSpec } from '../api'; export function locationSpecToMetadataName(location: LocationSpec) { const hash = createHash('sha1') diff --git a/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts b/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts index 43559fcd5f..5fcd89841e 100644 --- a/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts +++ b/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts @@ -25,7 +25,7 @@ import { CatalogProcessor, CatalogProcessorEmit, LocationSpec, - results, + processingResult, } from '@backstage/plugin-catalog-backend'; import { TemplateEntityV1beta3, @@ -70,7 +70,7 @@ export class ScaffolderEntitiesProcessor implements CatalogProcessor { defaultNamespace: selfRef.namespace, }); emit( - results.relation({ + processingResult.relation({ source: selfRef, type: RELATION_OWNED_BY, target: { @@ -81,7 +81,7 @@ export class ScaffolderEntitiesProcessor implements CatalogProcessor { }), ); emit( - results.relation({ + processingResult.relation({ source: { kind: targetRef.kind, namespace: targetRef.namespace, From 79b9d8a8611860a01ffa686bcf8e864ccfa7f637 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Tue, 1 Mar 2022 14:02:24 +0000 Subject: [PATCH 067/353] permission-common: add apidocs for permission properties Signed-off-by: Mike Lewis --- .changeset/nice-dragons-collect.md | 5 +++++ plugins/permission-common/src/types/permission.ts | 14 ++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 .changeset/nice-dragons-collect.md diff --git a/.changeset/nice-dragons-collect.md b/.changeset/nice-dragons-collect.md new file mode 100644 index 0000000000..040b030539 --- /dev/null +++ b/.changeset/nice-dragons-collect.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-common': patch +--- + +Add api doc comments to Permission type properties. diff --git a/plugins/permission-common/src/types/permission.ts b/plugins/permission-common/src/types/permission.ts index 805aefa3de..16934d8771 100644 --- a/plugins/permission-common/src/types/permission.ts +++ b/plugins/permission-common/src/types/permission.ts @@ -37,8 +37,22 @@ export type PermissionAttributes = { * @public */ export type Permission = { + /** + * The name of the permission. + */ name: string; + /** + * {@link PermissionAttributes} which describe characteristics of the permission, to help + * policy authors make consistent decisions for similar permissions without referring to them + * all by name. + */ attributes: PermissionAttributes; + /** + * Some permissions can be authorized based on characteristics of a resource + * such a catalog entity. For these permissions, the resourceType field + * denotes the type of the resource whose resourceRef should be passed when + * authorizing. + */ resourceType?: string; }; From 194011ce84cb3a9643b29620818e806e37553128 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 1 Mar 2022 10:40:48 +0100 Subject: [PATCH 068/353] chore: fixing up the types to actually be correct and use the returnValue types Signed-off-by: blam --- .changeset/swift-roses-hug.md | 2 +- plugins/scaffolder/api-report.md | 38 +++++++----- .../components/TemplatePage/TemplatePage.tsx | 2 +- plugins/scaffolder/src/extensions/default.ts | 58 +++++++++---------- plugins/scaffolder/src/extensions/index.tsx | 15 +++++ plugins/scaffolder/src/extensions/types.ts | 6 +- plugins/scaffolder/src/index.ts | 8 ++- 7 files changed, 77 insertions(+), 52 deletions(-) diff --git a/.changeset/swift-roses-hug.md b/.changeset/swift-roses-hug.md index 47a84e3247..0e6d76ed87 100644 --- a/.changeset/swift-roses-hug.md +++ b/.changeset/swift-roses-hug.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder': minor --- **BREAKING**: Removing the exports of the raw components that back the `CustomFieldExtensions`. diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index eed65578d3..a385e13534 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -28,16 +28,13 @@ import { ScmIntegrationRegistry } from '@backstage/integration'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; -// Warning: (ae-forgotten-export) The symbol "FieldExtensionComponent" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "createScaffolderFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export function createScaffolderFieldExtension< TReturnValue = unknown, TInputProps = unknown, >( options: FieldExtensionOptions, -): Extension>; +): Extension>; // @public export type CustomFieldValidator = ( @@ -52,14 +49,16 @@ export type CustomFieldValidator = ( // // @public (undocumented) export const EntityNamePickerFieldExtension: FieldExtensionComponent< - FieldExtensionComponentProps + string, + {} >; // Warning: (ae-missing-release-tag) "EntityPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const EntityPickerFieldExtension: FieldExtensionComponent< - FieldExtensionComponentProps + string, + EntityPickerUiOptions >; // Warning: (ae-missing-release-tag) "EntityPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -76,7 +75,8 @@ export interface EntityPickerUiOptions { // @public export const EntityTagsPickerFieldExtension: FieldExtensionComponent< - FieldExtensionComponentProps + string[], + EntityTagsPickerUiOptions >; // Warning: (ae-missing-release-tag) "EntityTagsPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -93,6 +93,9 @@ export interface EntityTagsPickerUiOptions { // @public export const FavouriteTemplate: (props: Props) => JSX.Element; +// @public +export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null; + // @public export interface FieldExtensionComponentProps< TFieldReturnValue, @@ -107,10 +110,12 @@ export interface FieldExtensionComponentProps< // @public export type FieldExtensionOptions< TFieldReturnValue = unknown, - TProps = FieldProps, + TInputProps = unknown, > = { name: string; - component: (props: TProps) => JSX.Element | null; + component: ( + props: FieldExtensionComponentProps, + ) => JSX.Element | null; validation?: CustomFieldValidator; }; @@ -150,7 +155,8 @@ export type LogEvent = { // // @public (undocumented) export const OwnedEntityPickerFieldExtension: FieldExtensionComponent< - FieldExtensionComponentProps + string, + OwnedEntityPickerUiOptions >; // Warning: (ae-missing-release-tag) "OwnedEntityPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -167,7 +173,8 @@ export interface OwnedEntityPickerUiOptions { // // @public (undocumented) export const OwnerPickerFieldExtension: FieldExtensionComponent< - FieldExtensionComponentProps + string, + OwnerPickerUiOptions >; // Warning: (ae-missing-release-tag) "OwnerPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -193,7 +200,8 @@ export const repoPickerValidation: ( // // @public (undocumented) export const RepoUrlPickerFieldExtension: FieldExtensionComponent< - FieldExtensionComponentProps + string, + RepoUrlPickerUiOptions >; // Warning: (ae-missing-release-tag) "RepoUrlPickerUiOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -291,9 +299,7 @@ export class ScaffolderClient implements ScaffolderApi { streamLogs(options: ScaffolderStreamLogsOptions): Observable; } -// Warning: (ae-missing-release-tag) "ScaffolderFieldExtensions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const ScaffolderFieldExtensions: React_2.ComponentType; // Warning: (ae-missing-release-tag) "ScaffolderGetIntegrationsListOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index e69c4bab5c..5310a4c79f 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -109,7 +109,7 @@ export const createValidator = ( export const TemplatePage = ({ customFieldExtensions = [], }: { - customFieldExtensions?: FieldExtensionOptions[]; + customFieldExtensions?: FieldExtensionOptions[]; }) => { const apiHolder = useApiHolder(); const secretsContext = useContext(SecretsContext); diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index b3bb37065e..8081dcb0ca 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -20,35 +20,33 @@ import { EntityTagsPicker } from '../components/fields/EntityTagsPicker/EntityTa import { OwnerPicker } from '../components/fields/OwnerPicker/OwnerPicker'; import { RepoUrlPicker } from '../components/fields/RepoUrlPicker/RepoUrlPicker'; import { repoPickerValidation } from '../components/fields/RepoUrlPicker/validation'; -import { FieldExtensionOptions } from './types'; import { OwnedEntityPicker } from '../components/fields/OwnedEntityPicker/OwnedEntityPicker'; -export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS: FieldExtensionOptions[] = - [ - { - component: EntityPicker, - name: 'EntityPicker', - }, - { - component: EntityNamePicker, - name: 'EntityNamePicker', - validation: entityNamePickerValidation, - }, - { - component: EntityTagsPicker, - name: 'EntityTagsPicker', - }, - { - component: RepoUrlPicker, - name: 'RepoUrlPicker', - validation: repoPickerValidation, - }, - { - component: OwnerPicker, - name: 'OwnerPicker', - }, - { - component: OwnedEntityPicker, - name: 'OwnedEntityPicker', - }, - ]; +export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ + { + component: EntityPicker, + name: 'EntityPicker', + }, + { + component: EntityNamePicker, + name: 'EntityNamePicker', + validation: entityNamePickerValidation, + }, + { + component: EntityTagsPicker, + name: 'EntityTagsPicker', + }, + { + component: RepoUrlPicker, + name: 'RepoUrlPicker', + validation: repoPickerValidation, + }, + { + component: OwnerPicker, + name: 'OwnerPicker', + }, + { + component: OwnedEntityPicker, + name: 'OwnedEntityPicker', + }, +]; diff --git a/plugins/scaffolder/src/extensions/index.tsx b/plugins/scaffolder/src/extensions/index.tsx index 75a166beff..60b9022053 100644 --- a/plugins/scaffolder/src/extensions/index.tsx +++ b/plugins/scaffolder/src/extensions/index.tsx @@ -25,8 +25,18 @@ import { Extension, attachComponentData } from '@backstage/core-plugin-api'; export const FIELD_EXTENSION_WRAPPER_KEY = 'scaffolder.extensions.wrapper.v1'; export const FIELD_EXTENSION_KEY = 'scaffolder.extensions.field.v1'; +/** + * A type used to wrap up the FieldExtension to embed the ReturnValue and the InputProps + * + * @public + */ export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null; +/** + * Method for creating field extensions that can be used in the scaffolder + * frontend form. + * @public + */ export function createScaffolderFieldExtension< TReturnValue = unknown, TInputProps = unknown, @@ -48,6 +58,11 @@ export function createScaffolderFieldExtension< }; } +/** + * The Wrapping component for defining fields extensions inside + * + * @public + */ export const ScaffolderFieldExtensions: React.ComponentType = (): JSX.Element | null => null; diff --git a/plugins/scaffolder/src/extensions/types.ts b/plugins/scaffolder/src/extensions/types.ts index 574ce7ea64..be8dc14b0a 100644 --- a/plugins/scaffolder/src/extensions/types.ts +++ b/plugins/scaffolder/src/extensions/types.ts @@ -35,10 +35,12 @@ export type CustomFieldValidator = ( */ export type FieldExtensionOptions< TFieldReturnValue = unknown, - TProps = FieldProps, + TInputProps = unknown, > = { name: string; - component: (props: TProps) => JSX.Element | null; + component: ( + props: FieldExtensionComponentProps, + ) => JSX.Element | null; validation?: CustomFieldValidator; }; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 9776549091..f33cf289f4 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -40,7 +40,12 @@ export { createScaffolderFieldExtension, ScaffolderFieldExtensions, } from './extensions'; - +export type { + CustomFieldValidator, + FieldExtensionOptions, + FieldExtensionComponentProps, + FieldExtensionComponent, +} from './extensions'; export { EntityPickerFieldExtension, EntityNamePickerFieldExtension, @@ -52,5 +57,4 @@ export { scaffolderPlugin, } from './plugin'; export * from './components'; -export * from './extensions'; export type { TaskPageProps } from './components/TaskPage'; From 8e1df15a95a0e89246a99c18e059e51fee882842 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 1 Mar 2022 15:32:04 +0100 Subject: [PATCH 069/353] chore: fix up the interface a little bit Signed-off-by: blam --- plugins/catalog-react/src/hooks/useEntity.tsx | 51 ++++++------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/plugins/catalog-react/src/hooks/useEntity.tsx b/plugins/catalog-react/src/hooks/useEntity.tsx index e46ee680eb..8ff5df9349 100644 --- a/plugins/catalog-react/src/hooks/useEntity.tsx +++ b/plugins/catalog-react/src/hooks/useEntity.tsx @@ -27,8 +27,8 @@ import { catalogApiRef } from '../api'; import { useEntityCompoundName } from './useEntityCompoundName'; /** @public */ -export type EntityLoadingStatus = { - entity?: Entity; +export type EntityLoadingStatus = { + entity?: TEntity; loading: boolean; error?: Error; refresh?: VoidFunction; @@ -129,40 +129,21 @@ export const useEntityFromUrl = (): EntityLoadingStatus => { return { entity, loading, error, refresh }; }; -/** - * @public - * - * The response shape for {@link useEntity} - */ -export interface UseEntityResponse { - entity: T; - /** @deprecated use {@link useAsyncEntity} instead */ - loading: boolean; - /** @deprecated use {@link useAsyncEntity} instead */ - error?: Error; - /** @deprecated use {@link useAsyncEntity} instead */ - refresh?: VoidFunction; -} - -/** - * @public - * - * The response shape for {@link useAsyncEntity} - */ -export interface UseAsyncEntityResponse { - entity?: T; - loading: boolean; - error?: Error; - refresh?: VoidFunction; -} - /** * Grab the current entity from the context, throws if the entity has not yet been loaded * or is not available. * * @public */ -export function useEntity(): UseEntityResponse { +export function useEntity(): { + entity: TEntity; + /** @deprecated use {@link useAsyncEntity} instead */ + loading: boolean; + /** @deprecated use {@link useAsyncEntity} instead */ + error?: Error; + /** @deprecated use {@link useAsyncEntity} instead */ + refresh?: VoidFunction; +} { const versionedHolder = useVersionedContext<{ 1: EntityLoadingStatus }>('entity-context'); @@ -178,7 +159,7 @@ export function useEntity(): UseEntityResponse { if (!value.entity) { // Once we have removed the additional fields from being returned we can drop this deprecation // and move to the error instead. - // throw new Error('useEntity hook is being called outside of an EntityPage where the entity has not been loaded. If this is intentional, please use useAsyncEntity instead.'); + // throw new Error('useEntity hook is being called outside of an EntityLayout where the entity has not been loaded. If this is intentional, please use useAsyncEntity instead.'); // eslint-disable-next-line no-console console.warn( @@ -187,7 +168,7 @@ export function useEntity(): UseEntityResponse { } const { entity, loading, error, refresh } = value; - return { entity: entity as T, loading, error, refresh }; + return { entity: entity as TEntity, loading, error, refresh }; } /** @@ -196,8 +177,8 @@ export function useEntity(): UseEntityResponse { * @public */ export function useAsyncEntity< - T extends Entity = Entity, ->(): UseAsyncEntityResponse { + TEntity extends Entity = Entity, +>(): EntityLoadingStatus { const versionedHolder = useVersionedContext<{ 1: EntityLoadingStatus }>('entity-context'); @@ -210,5 +191,5 @@ export function useAsyncEntity< } const { entity, loading, error, refresh } = value; - return { entity: entity as T, loading, error, refresh }; + return { entity: entity as TEntity, loading, error, refresh }; } From 141af65385d3b6c0bea94f6c29ee4ab2f22e7baf Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 1 Mar 2022 15:34:47 +0100 Subject: [PATCH 070/353] chore: fixing the api-report and clean up the types Signed-off-by: blam --- plugins/catalog-react/api-report.md | 35 +++++-------------- plugins/catalog-react/src/hooks/index.ts | 2 -- plugins/catalog-react/src/hooks/useEntity.tsx | 2 +- 3 files changed, 9 insertions(+), 30 deletions(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 0103ac2f0e..1a94af6052 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -225,8 +225,8 @@ export const EntityListProvider: ({ }: PropsWithChildren<{}>) => JSX.Element; // @public (undocumented) -export type EntityLoadingStatus = { - entity?: Entity; +export type EntityLoadingStatus = { + entity?: TEntity; loading: boolean; error?: Error; refresh?: VoidFunction; @@ -525,23 +525,16 @@ export type UnregisterEntityDialogProps = { // @public export function useAsyncEntity< - T extends Entity = Entity, ->(): UseAsyncEntityResponse; + TEntity extends Entity = Entity, +>(): EntityLoadingStatus; // @public -export interface UseAsyncEntityResponse { - // (undocumented) - entity?: T; - // (undocumented) - error?: Error; - // (undocumented) +export function useEntity(): { + entity: TEntity; loading: boolean; - // (undocumented) + error?: Error; refresh?: VoidFunction; -} - -// @public -export function useEntity(): UseEntityResponse; +}; // @public @deprecated export const useEntityCompoundName: () => { @@ -583,18 +576,6 @@ export function useEntityPermission(permission: Permission): { error?: Error; }; -// @public -export interface UseEntityResponse { - // (undocumented) - entity: T; - // @deprecated (undocumented) - error?: Error; - // @deprecated (undocumented) - loading: boolean; - // @deprecated (undocumented) - refresh?: VoidFunction; -} - // @public export function useEntityTypeFilter(): { loading: boolean; diff --git a/plugins/catalog-react/src/hooks/index.ts b/plugins/catalog-react/src/hooks/index.ts index 8e0fc48e72..da23d54ad7 100644 --- a/plugins/catalog-react/src/hooks/index.ts +++ b/plugins/catalog-react/src/hooks/index.ts @@ -24,8 +24,6 @@ export type { EntityLoadingStatus, EntityProviderProps, AsyncEntityProviderProps, - UseEntityResponse, - UseAsyncEntityResponse, } from './useEntity'; export { useEntityCompoundName } from './useEntityCompoundName'; export { diff --git a/plugins/catalog-react/src/hooks/useEntity.tsx b/plugins/catalog-react/src/hooks/useEntity.tsx index 8ff5df9349..f70adef708 100644 --- a/plugins/catalog-react/src/hooks/useEntity.tsx +++ b/plugins/catalog-react/src/hooks/useEntity.tsx @@ -27,7 +27,7 @@ import { catalogApiRef } from '../api'; import { useEntityCompoundName } from './useEntityCompoundName'; /** @public */ -export type EntityLoadingStatus = { +export type EntityLoadingStatus = { entity?: TEntity; loading: boolean; error?: Error; From 81273e95cffc127ff75273c752519fa3247add41 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Tue, 1 Mar 2022 13:07:15 +0000 Subject: [PATCH 071/353] catalog-common: mark permission-related exports as alpha Marks all strictly permission-related exports in catalog-common as alpha. Signed-off-by: Mike Lewis --- .changeset/nice-walls-reply.md | 14 ++++++++++++++ plugins/catalog-common/api-report.md | 16 ++++++++-------- plugins/catalog-common/package.json | 8 +++++--- plugins/catalog-common/src/permissions.ts | 18 ++++++++++-------- 4 files changed, 37 insertions(+), 19 deletions(-) create mode 100644 .changeset/nice-walls-reply.md diff --git a/.changeset/nice-walls-reply.md b/.changeset/nice-walls-reply.md new file mode 100644 index 0000000000..72b389c130 --- /dev/null +++ b/.changeset/nice-walls-reply.md @@ -0,0 +1,14 @@ +--- +'@backstage/plugin-catalog-common': minor +--- + +Mark permission-related exports as alpha. This means that the exports below should now be imported from `@backstage/plugin-catalog-common/alpha` instead of `@backstage/plugin-catalog-common`. + +- `RESOURCE_TYPE_CATALOG_ENTITY` +- `catalogEntityReadPermission` +- `catalogEntityCreatePermission` +- `catalogEntityDeletePermission` +- `catalogEntityRefreshPermission` +- `catalogLocationReadPermission` +- `catalogLocationCreatePermission` +- `catalogLocationDeletePermission` diff --git a/plugins/catalog-common/api-report.md b/plugins/catalog-common/api-report.md index 9826a8ddef..57f8c3983e 100644 --- a/plugins/catalog-common/api-report.md +++ b/plugins/catalog-common/api-report.md @@ -5,27 +5,27 @@ ```ts import { Permission } from '@backstage/plugin-permission-common'; -// @public +// @alpha export const catalogEntityCreatePermission: Permission; -// @public +// @alpha export const catalogEntityDeletePermission: Permission; -// @public +// @alpha export const catalogEntityReadPermission: Permission; -// @public +// @alpha export const catalogEntityRefreshPermission: Permission; -// @public +// @alpha export const catalogLocationCreatePermission: Permission; -// @public +// @alpha export const catalogLocationDeletePermission: Permission; -// @public +// @alpha export const catalogLocationReadPermission: Permission; -// @public (undocumented) +// @alpha export const RESOURCE_TYPE_CATALOG_ENTITY = 'catalog-entity'; ``` diff --git a/plugins/catalog-common/package.json b/plugins/catalog-common/package.json index 9a52d22798..acf4ed4aaa 100644 --- a/plugins/catalog-common/package.json +++ b/plugins/catalog-common/package.json @@ -10,7 +10,8 @@ "access": "public", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "backstage": { "role": "common-library" @@ -25,7 +26,7 @@ "backstage" ], "scripts": { - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -39,6 +40,7 @@ "@backstage/cli": "^0.14.0" }, "files": [ - "dist" + "dist", + "alpha" ] } diff --git a/plugins/catalog-common/src/permissions.ts b/plugins/catalog-common/src/permissions.ts index 52c225e441..8e1b036470 100644 --- a/plugins/catalog-common/src/permissions.ts +++ b/plugins/catalog-common/src/permissions.ts @@ -17,8 +17,10 @@ import { Permission } from '@backstage/plugin-permission-common'; /** + * Permission resource type which corresponds to catalog entities. + * * {@link https://backstage.io/docs/features/software-catalog/software-catalog-overview} - * @public + * @alpha */ export const RESOURCE_TYPE_CATALOG_ENTITY = 'catalog-entity'; @@ -28,7 +30,7 @@ export const RESOURCE_TYPE_CATALOG_ENTITY = 'catalog-entity'; * * If this permission is not authorized, it will appear that the entity does not * exist in the catalog — both in the frontend and in API responses. - * @public + * @alpha */ export const catalogEntityReadPermission: Permission = { name: 'catalog.entity.read', @@ -42,7 +44,7 @@ export const catalogEntityReadPermission: Permission = { * This permission is used to authorize actions that involve creating a new * catalog entity. This includes registering an existing component into the * catalog. - * @public + * @alpha */ export const catalogEntityCreatePermission: Permission = { name: 'catalog.entity.create', @@ -55,7 +57,7 @@ export const catalogEntityCreatePermission: Permission = { /** * This permission is used to designate actions that involve removing one or * more entities from the catalog. - * @public + * @alpha */ export const catalogEntityDeletePermission: Permission = { name: 'catalog.entity.delete', @@ -68,7 +70,7 @@ export const catalogEntityDeletePermission: Permission = { /** * This permission is used to designate refreshing one or more entities from the * catalog. - * @public + * @alpha */ export const catalogEntityRefreshPermission: Permission = { name: 'catalog.entity.refresh', @@ -84,7 +86,7 @@ export const catalogEntityRefreshPermission: Permission = { * * If this permission is not authorized, it will appear that the location does * not exist in the catalog — both in the frontend and in API responses. - * @public + * @alpha */ export const catalogLocationReadPermission: Permission = { name: 'catalog.location.read', @@ -96,7 +98,7 @@ export const catalogLocationReadPermission: Permission = { /** * This permission is used to designate actions that involve creating catalog * locations. - * @public + * @alpha */ export const catalogLocationCreatePermission: Permission = { name: 'catalog.location.create', @@ -108,7 +110,7 @@ export const catalogLocationCreatePermission: Permission = { /** * This permission is used to designate actions that involve deleting locations * from the catalog. - * @public + * @alpha */ export const catalogLocationDeletePermission: Permission = { name: 'catalog.location.delete', From 617a132871334cd992cb7e597da54822ae655039 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Tue, 1 Mar 2022 13:45:13 +0000 Subject: [PATCH 072/353] create-app: import catalogEntityCreatePermission from /alpha Signed-off-by: Mike Lewis --- .changeset/gorgeous-actors-shave.md | 12 ++++++++++++ .../templates/default-app/packages/app/src/App.tsx | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 .changeset/gorgeous-actors-shave.md diff --git a/.changeset/gorgeous-actors-shave.md b/.changeset/gorgeous-actors-shave.md new file mode 100644 index 0000000000..bcf5f43018 --- /dev/null +++ b/.changeset/gorgeous-actors-shave.md @@ -0,0 +1,12 @@ +--- +'@backstage/create-app': patch +--- + +Update import location of catalogEntityCreatePermission. + +To apply this change to an existing app, make the following change to `packages/app/src/App.tsx`: + +```diff +-import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; ++import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; +``` diff --git a/packages/create-app/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx index 6f00993273..f4ff424926 100644 --- a/packages/create-app/templates/default-app/packages/app/src/App.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/App.tsx @@ -30,7 +30,7 @@ import { createApp } from '@backstage/app-defaults'; import { FlatRoutes } from '@backstage/core-app-api'; import { CatalogGraphPage } from '@backstage/plugin-catalog-graph'; import { PermissionedRoute } from '@backstage/plugin-permission-react'; -import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; +import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; const app = createApp({ apis, From 40559d7a3b952907b837780b538bfa5d2eb1dbbc Mon Sep 17 00:00:00 2001 From: MT Lewis Date: Tue, 1 Mar 2022 14:32:39 +0000 Subject: [PATCH 073/353] Update .changeset/nice-walls-reply.md Co-authored-by: Johan Haals Signed-off-by: Mike Lewis --- .changeset/nice-walls-reply.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/nice-walls-reply.md b/.changeset/nice-walls-reply.md index 72b389c130..f6d1fb7562 100644 --- a/.changeset/nice-walls-reply.md +++ b/.changeset/nice-walls-reply.md @@ -2,7 +2,7 @@ '@backstage/plugin-catalog-common': minor --- -Mark permission-related exports as alpha. This means that the exports below should now be imported from `@backstage/plugin-catalog-common/alpha` instead of `@backstage/plugin-catalog-common`. +**Breaking**: Mark permission-related exports as alpha. This means that the exports below should now be imported from `@backstage/plugin-catalog-common/alpha` instead of `@backstage/plugin-catalog-common`. - `RESOURCE_TYPE_CATALOG_ENTITY` - `catalogEntityReadPermission` From 5c592573007545ce2771e3ff9ec3a05b0ca48b55 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Tue, 1 Mar 2022 14:40:08 +0000 Subject: [PATCH 074/353] catalog-backend: add 'breaking' prefix to existing changeset Signed-off-by: Mike Lewis --- .changeset/chilled-dolls-agree.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/chilled-dolls-agree.md b/.changeset/chilled-dolls-agree.md index 0468bdfee0..750307bf53 100644 --- a/.changeset/chilled-dolls-agree.md +++ b/.changeset/chilled-dolls-agree.md @@ -2,7 +2,7 @@ '@backstage/plugin-catalog-backend': minor --- -Mark permission-related exports as alpha. This means that the exports below should now be imported from `@backstage/plugin-catalog-backend/alpha` instead of `@backstage/plugin-catalog-backend`. +**Breaking**: Mark permission-related exports as alpha. This means that the exports below should now be imported from `@backstage/plugin-catalog-backend/alpha` instead of `@backstage/plugin-catalog-backend`. - `catalogConditions` - `createCatalogPolicyDecision` From 1f5b25eeabf1ee614bc58231b6c78785ef3110e9 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Tue, 1 Mar 2022 15:01:24 +0000 Subject: [PATCH 075/353] permission-common: changeset formatting Signed-off-by: Mike Lewis --- .changeset/nice-dragons-collect.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/nice-dragons-collect.md b/.changeset/nice-dragons-collect.md index 040b030539..b3d9efefd9 100644 --- a/.changeset/nice-dragons-collect.md +++ b/.changeset/nice-dragons-collect.md @@ -2,4 +2,4 @@ '@backstage/plugin-permission-common': patch --- -Add api doc comments to Permission type properties. +Add api doc comments to `Permission` type properties. From c1cb49008b14a68c918bc80c12b21b5fead790ad Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Tue, 1 Mar 2022 16:20:29 +0000 Subject: [PATCH 076/353] Refactor the error to AirbrakeApi Signed-off-by: Karan Shah --- plugins/airbrake/src/api/AirbrakeApi.ts | 6 ++++++ plugins/airbrake/src/api/ProductionApi.ts | 8 +------- plugins/airbrake/src/api/mock/MockApi.ts | 7 +++++-- .../EntityAirbrakeWidget/EntityAirbrakeWidget.tsx | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/plugins/airbrake/src/api/AirbrakeApi.ts b/plugins/airbrake/src/api/AirbrakeApi.ts index a29aa3eca7..672b1f92f4 100644 --- a/plugins/airbrake/src/api/AirbrakeApi.ts +++ b/plugins/airbrake/src/api/AirbrakeApi.ts @@ -24,3 +24,9 @@ export const airbrakeApiRef = createApiRef({ export interface AirbrakeApi { fetchGroups(projectId: string): Promise; } + +export class NoProjectIdError extends Error { + constructor() { + super('Project ID is not present'); + } +} diff --git a/plugins/airbrake/src/api/ProductionApi.ts b/plugins/airbrake/src/api/ProductionApi.ts index abd8c8863f..7d1d88a2a4 100644 --- a/plugins/airbrake/src/api/ProductionApi.ts +++ b/plugins/airbrake/src/api/ProductionApi.ts @@ -15,15 +15,9 @@ */ import { Groups } from './airbrakeGroups'; -import { AirbrakeApi } from './AirbrakeApi'; +import { AirbrakeApi, NoProjectIdError } from './AirbrakeApi'; import { DiscoveryApi } from '@backstage/core-plugin-api'; -export class NoProjectIdError extends Error { - constructor() { - super('Project ID is not present'); - } -} - export class ProductionAirbrakeApi implements AirbrakeApi { constructor(private readonly discoveryApi: DiscoveryApi) {} diff --git a/plugins/airbrake/src/api/mock/MockApi.ts b/plugins/airbrake/src/api/mock/MockApi.ts index dbb1d5c049..f362fbbcaa 100644 --- a/plugins/airbrake/src/api/mock/MockApi.ts +++ b/plugins/airbrake/src/api/mock/MockApi.ts @@ -15,7 +15,7 @@ */ import { Groups } from '../airbrakeGroups'; -import { AirbrakeApi } from '../AirbrakeApi'; +import { AirbrakeApi, NoProjectIdError } from '../AirbrakeApi'; import mockGroupsData from './airbrakeGroupsApiMock.json'; export class MockAirbrakeApi implements AirbrakeApi { @@ -25,7 +25,10 @@ export class MockAirbrakeApi implements AirbrakeApi { this.waitTimeInMillis = waitTimeInMillis; } - fetchGroups(): Promise { + fetchGroups(projectId: string): Promise { + if (!projectId) { + return Promise.reject(new NoProjectIdError()); + } return new Promise(resolve => { setTimeout(() => resolve(mockGroupsData), this.waitTimeInMillis); }); diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx index affb822f64..6ec820efa6 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx @@ -29,7 +29,7 @@ import { ErrorApi, errorApiRef, useApi } from '@backstage/core-plugin-api'; import { airbrakeApiRef } from '../../api'; import useAsync from 'react-use/lib/useAsync'; import { AIRBRAKE_PROJECT_ID_ANNOTATION, useProjectId } from '../useProjectId'; -import { NoProjectIdError } from '../../api/ProductionApi'; +import { NoProjectIdError } from '../../api/AirbrakeApi'; const useStyles = makeStyles(() => ({ multilineText: { From 0df6077ab5f6c25baa5ebd151871a01ab0bbe869 Mon Sep 17 00:00:00 2001 From: Francesco Saltori Date: Tue, 1 Mar 2022 17:14:14 +0100 Subject: [PATCH 077/353] Autoremove containers launched by DockerContainerRunner Signed-off-by: Francesco Saltori --- .changeset/grumpy-apes-repeat.md | 5 +++++ .../backend-common/src/util/DockerContainerRunner.test.ts | 2 ++ packages/backend-common/src/util/DockerContainerRunner.ts | 1 + 3 files changed, 8 insertions(+) create mode 100644 .changeset/grumpy-apes-repeat.md diff --git a/.changeset/grumpy-apes-repeat.md b/.changeset/grumpy-apes-repeat.md new file mode 100644 index 0000000000..bc47b0d422 --- /dev/null +++ b/.changeset/grumpy-apes-repeat.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +DockerContainerRunner.runContainer now automatically removes the container when its execution terminates diff --git a/packages/backend-common/src/util/DockerContainerRunner.test.ts b/packages/backend-common/src/util/DockerContainerRunner.test.ts index 8833e4518b..3208679d1f 100644 --- a/packages/backend-common/src/util/DockerContainerRunner.test.ts +++ b/packages/backend-common/src/util/DockerContainerRunner.test.ts @@ -115,6 +115,7 @@ describe('DockerContainerRunner', () => { Env: envVarsArray, WorkingDir: workingDir, HostConfig: { + AutoRemove: true, Binds: expect.arrayContaining([ `${path.join(rootDir, 'input')}:/input`, `${path.join(rootDir, 'output')}:/output`, @@ -207,6 +208,7 @@ describe('DockerContainerRunner', () => { logStream, expect.objectContaining({ HostConfig: { + AutoRemove: true, Binds: [], }, Volumes: {}, diff --git a/packages/backend-common/src/util/DockerContainerRunner.ts b/packages/backend-common/src/util/DockerContainerRunner.ts index 522328c2ec..4d4684c814 100644 --- a/packages/backend-common/src/util/DockerContainerRunner.ts +++ b/packages/backend-common/src/util/DockerContainerRunner.ts @@ -105,6 +105,7 @@ export class DockerContainerRunner implements ContainerRunner { await this.dockerClient.run(imageName, args, logStream, { Volumes, HostConfig: { + AutoRemove: true, Binds, }, ...(workingDir ? { WorkingDir: workingDir } : {}), From 8dc290872dcf820d6ba6e7fe2c779081fbcd8ef3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 1 Mar 2022 17:54:56 +0100 Subject: [PATCH 078/353] docs/local-dev: add docs for the experimental type build Signed-off-by: Patrik Oldsberg --- docs/local-dev/cli-build-system.md | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/docs/local-dev/cli-build-system.md b/docs/local-dev/cli-build-system.md index 75f0606752..f57efc1255 100644 --- a/docs/local-dev/cli-build-system.md +++ b/docs/local-dev/cli-build-system.md @@ -552,3 +552,36 @@ The following is an excerpt of a typical setup of an isomorphic library package: }, "files": ["dist"], ``` + +## Experimental Type Build + +The Backstage CLI has an experimental feature where multiple different type definition files can be generated for different release stages. The release stages are marked in the [TSDoc](https://tsdoc.org/) for each individual export, using either `@public`, `@alpha`, or `@beta`. Rather than just building a single `index.d.ts` file, the build process will instead output `index.d.ts`, `index.beta.d.ts`, and `index.alpha.d.ts`. Each of these files will have exports from more unstable release stages stripped, meaning that `index.d.ts` will omit all exports marked with `@alpha` or `@beta`, while `index.beta.d.ts` will omit all exports marked with `@alpha`. + +This feature is aimed at projects that publish to package registries and wish to maintain different levels of API stability within each package. There is no need to use this within a single monorepo, as it has no effect due to only applying to built and published packages. + +In order for the experimental type build to work, `@microsoft/api-extractor` must be installed in your project, as it is an optional peer dependency of the Backstage CLI. There are then three steps that need to be taken for each package where you want to enable this feature: + +- Add the `--experimental-type-build` flag to the `"build"` script of the package. +- Add either one or both of `"alphaTypes"` and `"betaTypes"` to the `"publishConfig"` of the package: + ```json + "publishConfig": { + ... + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts", + "betaTypes": "dist/index.beta.d.ts" + }, + ``` +- Add either one or both of `"alpha"` and `"beta"` to the `"files"` of the package: + ```json + "files": [ + "dist", + "alpha", + "beta" + ] + ``` + +Once this setup is complete, users of the published packages will only be able to access the stable API via the main package entry point, for example `@acme/my-plugin`. Exports marked with `@alpha` or `@beta` will only be available via the `/alpha` entry point, for example `@acme/my-plugin/alpha`, and exports marked with `@beta` will only be available via `/beta`. This does not apply within the monorepo that contains the package. There all exports still have to be imported via the main entry point. + +Note that these different entry points are only separated during type checking. At runtime they all share the same code which contains the exports from all releases stages. + +An example of this setup can be seen in the [`@backstage/catalog-model`](https://github.com/backstage/backstage/blob/da0675bf9f28ed1460f03635a22d3c26abd14707/packages/catalog-model/package.json#L14) package, which has enabled `alpha` type exports. With this setup, exports marked as `@alpha` are only available for import via `@backstage/catalog-model/alpha`. The `@backstage/catalog-model` package currently does not have any exports marked as `@beta`, or a `/beta` entry point. From 1bd6e019abd3e4e7dd845ec575d97b73e9a135b0 Mon Sep 17 00:00:00 2001 From: su-gupta Date: Tue, 1 Mar 2022 12:15:18 -0500 Subject: [PATCH 079/353] change to md files Signed-off-by: su-gupta --- contrib/search/confluence/ConfluenceCollator | 87 ----------------- .../search/confluence/ConfluenceCollator.md | 95 +++++++++++++++++++ ...ltListItem => ConfluenceResultListItem.md} | 19 +++- contrib/search/confluence/README.md | 32 ++++++- 4 files changed, 138 insertions(+), 95 deletions(-) delete mode 100644 contrib/search/confluence/ConfluenceCollator create mode 100644 contrib/search/confluence/ConfluenceCollator.md rename contrib/search/confluence/{ConfluenceResultListItem => ConfluenceResultListItem.md} (72%) diff --git a/contrib/search/confluence/ConfluenceCollator b/contrib/search/confluence/ConfluenceCollator deleted file mode 100644 index 5d6668b953..0000000000 --- a/contrib/search/confluence/ConfluenceCollator +++ /dev/null @@ -1,87 +0,0 @@ -import { DocumentCollator } from '@backstage/search-common'; -import fetch from 'cross-fetch' - -export class ConfluenceCollator implements DocumentCollator { - public readonly type: string = 'confluence'; - - async execute() { - - const ConfluenceUrlBase = 'https://{CONFLUENCE-ORG-NAME}.atlassian.net/wiki/rest/api' - - async function getConfluenceData(requestUrl: string) { - var emptyJson = {} - try { - const res = await fetch(requestUrl, { - method: 'get', - headers: { - 'Authorization': `Basic ${process.env.CONFLUENCE_TOKEN}` - }, - }); - if (res.ok) { - return await res.json(); - } - } catch (err) { - console.error(err); - } - return emptyJson - } - - async function getSpaces(): Promise { - const data = await getConfluenceData(`${ConfluenceUrlBase}/space?&limit=1000&type=global&status=current`); - let spacesList = [] - if (data["results"]) { - const results = data["results"]; - for (const result of results) { - spacesList.push(result["key"]) - } - } - return spacesList - } - - async function getDocumentsFromSpaces(spaces: string[]): Promise { - let documentsList = [] - for (var space of spaces) { - let next = true - let requestUrl = `${ConfluenceUrlBase}/content?limit=1000&status=current&spaceKey=${space}` - while (next) { - const data = await getConfluenceData(requestUrl) - if (data["results"]) { - const results = data["results"] - for (const result of results) { - documentsList.push(result["_links"]["self"]) - } - if (data["_links"]["next"]) { - requestUrl = data["_links"]["base"] + data["_links"]["next"] - } else { - next = false - } - } else { - break - } - } - } - return documentsList - } - - async function getDocumentInfo(documents: string[]) { - let documentInfo = [] - for (var documentUrl of documents) { - const data = await getConfluenceData(documentUrl + '?expand=body.storage') - if (data["status"] && data["status"]=="current") { - const documentMetaData = { - title: data["title"], - text: data["body"]["storage"]["value"], - location: data["_links"]["base"] + data["_links"]["webui"], - } - documentInfo.push(documentMetaData) - } - } - return documentInfo - } - - const spacesList = await getSpaces(); - const documentsList = await getDocumentsFromSpaces(spacesList); - const documentMetaDataList = await getDocumentInfo(documentsList); - return documentMetaDataList - } -} diff --git a/contrib/search/confluence/ConfluenceCollator.md b/contrib/search/confluence/ConfluenceCollator.md new file mode 100644 index 0000000000..f636ae88ea --- /dev/null +++ b/contrib/search/confluence/ConfluenceCollator.md @@ -0,0 +1,95 @@ +ConfluenceCollator.ts reference + +```ts +import { DocumentCollator } from '@backstage/search-common'; +import fetch from 'cross-fetch'; + +export class ConfluenceCollator implements DocumentCollator { + public readonly type: string = 'confluence'; + + async execute() { + const ConfluenceUrlBase = + 'https://{CONFLUENCE-ORG-NAME}.atlassian.net/wiki/rest/api'; + + async function getConfluenceData(requestUrl: string) { + var emptyJson = {}; + try { + const res = await fetch(requestUrl, { + method: 'get', + headers: { + Authorization: `Basic ${process.env.CONFLUENCE_TOKEN}`, + }, + }); + if (res.ok) { + return await res.json(); + } + } catch (err) { + console.error(err); + } + return emptyJson; + } + + async function getSpaces(): Promise { + const data = await getConfluenceData( + `${ConfluenceUrlBase}/space?&limit=1000&type=global&status=current`, + ); + let spacesList = []; + if (data['results']) { + const results = data['results']; + for (const result of results) { + spacesList.push(result['key']); + } + } + return spacesList; + } + + async function getDocumentsFromSpaces(spaces: string[]): Promise { + let documentsList = []; + for (var space of spaces) { + let next = true; + let requestUrl = `${ConfluenceUrlBase}/content?limit=1000&status=current&spaceKey=${space}`; + while (next) { + const data = await getConfluenceData(requestUrl); + if (data['results']) { + const results = data['results']; + for (const result of results) { + documentsList.push(result['_links']['self']); + } + if (data['_links']['next']) { + requestUrl = data['_links']['base'] + data['_links']['next']; + } else { + next = false; + } + } else { + break; + } + } + } + return documentsList; + } + + async function getDocumentInfo(documents: string[]) { + let documentInfo = []; + for (var documentUrl of documents) { + const data = await getConfluenceData( + documentUrl + '?expand=body.storage', + ); + if (data['status'] && data['status'] == 'current') { + const documentMetaData = { + title: data['title'], + text: data['body']['storage']['value'], + location: data['_links']['base'] + data['_links']['webui'], + }; + documentInfo.push(documentMetaData); + } + } + return documentInfo; + } + + const spacesList = await getSpaces(); + const documentsList = await getDocumentsFromSpaces(spacesList); + const documentMetaDataList = await getDocumentInfo(documentsList); + return documentMetaDataList; + } +} +``` diff --git a/contrib/search/confluence/ConfluenceResultListItem b/contrib/search/confluence/ConfluenceResultListItem.md similarity index 72% rename from contrib/search/confluence/ConfluenceResultListItem rename to contrib/search/confluence/ConfluenceResultListItem.md index cd6d13bdea..ba7c4f91a9 100644 --- a/contrib/search/confluence/ConfluenceResultListItem +++ b/contrib/search/confluence/ConfluenceResultListItem.md @@ -1,3 +1,6 @@ +ConfluenceResultListItem.tsx reference + +```tsx import React from 'react'; import { Link } from '@backstage/core-components'; import { IndexableDocument } from '@backstage/search-common'; @@ -17,26 +20,31 @@ export const ConfluenceResultListItem = ({ result }: Props) => { const chars = []; let isTag = false; for (const c of result.text.substring(0, 500)) { - if (c === "<") { + if (c === '<') { isTag = true; continue; } - if (c === ">") { + if (c === '>') { isTag = false; - chars.push(" ") + chars.push(' '); continue; } if (!isTag) { chars.push(c); } } - const excerpt = chars.join("").substring(0, 80) + (result.text.length > 80 ? "..." : ""); + const excerpt = + chars.join('').substring(0, 80) + (result.text.length > 80 ? '...' : ''); return ( - + { ); }; +``` diff --git a/contrib/search/confluence/README.md b/contrib/search/confluence/README.md index 5ebd275560..80beb9c169 100644 --- a/contrib/search/confluence/README.md +++ b/contrib/search/confluence/README.md @@ -1,6 +1,32 @@ # Confluence These files help you add Confluence as a source to the Backstage Search plugin. -To do so, add both files in this directory under the packages/backend/src/plugins/search/ pathway in your Backstage app as TypeScript files. -Then, update your packages/app/src/components/search/SearchPage.tsx and packages/backend/src/plugins/search.ts -to include the new Search source. +To do so, add both files in this directory under the packages/backend/src/plugins/search/ pathway in your Backstage app. +Then, add the following code to your packages/app/src/components/search/SearchPage.tsx: + +```tsx +import { ConfluenceResultListItem } from './ConfluenceResultListItem'; +``` + +```tsx +case 'confluence': + return ( + + ); +``` + +and the following to packages/backend/src/plugins/search.ts: + +```ts +import { ConfluenceCollator } from './search/ConfluenceCollator'; +``` + +```ts +indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + collator: new ConfluenceCollator(), +}); +``` From 0c8ba31d72670d9ff5eda3ca726e061fff088115 Mon Sep 17 00:00:00 2001 From: Harry Hogg Date: Tue, 1 Mar 2022 17:46:39 +0000 Subject: [PATCH 080/353] plugin-auth-backend: Added validation to ensure any custom auth resolvers are using EntityRefs for subject claims Signed-off-by: Harry Hogg --- .changeset/tasty-poems-raise.md | 5 +++ .../src/identity/TokenFactory.test.ts | 39 ++++++++++++++++--- .../auth-backend/src/identity/TokenFactory.ts | 10 +++++ 3 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 .changeset/tasty-poems-raise.md diff --git a/.changeset/tasty-poems-raise.md b/.changeset/tasty-poems-raise.md new file mode 100644 index 0000000000..47556cb3ad --- /dev/null +++ b/.changeset/tasty-poems-raise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': minor +--- + +Added validation to TokenFactory.issueToken that ensure any sub claim given is a valid entityRef. This will affect any custom resolver functions given to auth providers. diff --git a/plugins/auth-backend/src/identity/TokenFactory.test.ts b/plugins/auth-backend/src/identity/TokenFactory.test.ts index f5f06f1209..4b1c1804df 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.test.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.test.ts @@ -18,6 +18,7 @@ import { MemoryKeyStore } from './MemoryKeyStore'; import { TokenFactory } from './TokenFactory'; import { getVoidLogger } from '@backstage/backend-common'; import { JWKS, JSONWebKey, JWT } from 'jose'; +import { stringifyEntityRef } from '@backstage/catalog-model'; const logger = getVoidLogger(); @@ -28,6 +29,12 @@ function jwtKid(jwt: string): string { return header.kid; } +const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: 'default', + name: 'JackFrost', +}); + describe('TokenFactory', () => { it('should issue valid tokens signed by a listed key', async () => { const keyDurationSeconds = 5; @@ -39,7 +46,7 @@ describe('TokenFactory', () => { }); await expect(factory.listPublicKeys()).resolves.toEqual({ keys: [] }); - const token = await factory.issueToken({ claims: { sub: 'foo' } }); + const token = await factory.issueToken({ claims: { sub: entityRef } }); const { keys } = await factory.listPublicKeys(); const keyStore = JWKS.asKeyStore({ @@ -53,7 +60,7 @@ describe('TokenFactory', () => { expect(payload).toEqual({ iss: 'my-issuer', aud: 'backstage', - sub: 'foo', + sub: entityRef, iat: expect.any(Number), exp: expect.any(Number), }); @@ -71,8 +78,12 @@ describe('TokenFactory', () => { logger, }); - const token1 = await factory.issueToken({ claims: { sub: 'foo' } }); - const token2 = await factory.issueToken({ claims: { sub: 'foo' } }); + const token1 = await factory.issueToken({ + claims: { sub: entityRef }, + }); + const token2 = await factory.issueToken({ + claims: { sub: entityRef }, + }); expect(jwtKid(token1)).toBe(jwtKid(token2)); await expect(factory.listPublicKeys()).resolves.toEqual({ @@ -89,7 +100,9 @@ describe('TokenFactory', () => { keys: [], }); - const token3 = await factory.issueToken({ claims: { sub: 'foo' } }); + const token3 = await factory.issueToken({ + claims: { sub: entityRef }, + }); expect(jwtKid(token3)).not.toBe(jwtKid(token2)); await expect(factory.listPublicKeys()).resolves.toEqual({ @@ -100,4 +113,20 @@ describe('TokenFactory', () => { ], }); }); + + it('should throw an error with a non entityRef sub claim', async () => { + const keyDurationSeconds = 5; + const factory = new TokenFactory({ + issuer: 'my-issuer', + keyStore: new MemoryKeyStore(), + keyDurationSeconds, + logger, + }); + + await expect(() => { + return factory.issueToken({ + claims: { sub: 'UserId' }, + }); + }).rejects.toThrowError(); + }); }); diff --git a/plugins/auth-backend/src/identity/TokenFactory.ts b/plugins/auth-backend/src/identity/TokenFactory.ts index 041a292d44..cb2a8b453d 100644 --- a/plugins/auth-backend/src/identity/TokenFactory.ts +++ b/plugins/auth-backend/src/identity/TokenFactory.ts @@ -19,6 +19,7 @@ import { JSONWebKey, JWK, JWS } from 'jose'; import { Logger } from 'winston'; import { v4 as uuid } from 'uuid'; import { DateTime } from 'luxon'; +import { parseEntityRef } from '@backstage/catalog-model'; const MS_IN_S = 1000; @@ -72,6 +73,15 @@ export class TokenFactory implements TokenIssuer { const iat = Math.floor(Date.now() / MS_IN_S); const exp = iat + this.keyDurationSeconds; + // Validate that the subject claim is a valid EntityRef + try { + parseEntityRef(sub); + } catch (error) { + throw new Error( + '"sub" claim provided by the auth resolver is not a valid EntityRef.', + ); + } + this.logger.info(`Issuing token for ${sub}, with entities ${ent ?? []}`); return JWS.sign({ iss, sub, aud, iat, exp, ent }, key, { From 70f29c54238823855f10f1a5863fa0d9f90a9f93 Mon Sep 17 00:00:00 2001 From: Hasan Oezdemir <21654050+nodify-at@users.noreply.github.com> Date: Tue, 1 Mar 2022 23:45:01 +0100 Subject: [PATCH 081/353] (feature): remove unused configuration and improved permission management, describe the permission integration in detail. Signed-off-by: Hasan Oezdemir <21654050+nodify-at@users.noreply.github.com> --- .changeset/orange-cobras-shave.md | 10 +++- .changeset/ten-fireants-march.md | 5 +- plugins/jenkins-backend/api-report.md | 50 +------------------ plugins/jenkins-backend/package.json | 3 -- plugins/jenkins-backend/src/index.ts | 1 - .../src/permissions/conditionExports.ts | 27 ---------- .../jenkins-backend/src/permissions/index.ts | 20 -------- .../src/permissions/rules/index.ts | 30 ----------- .../jenkins-backend/src/service/jenkinsApi.ts | 7 +-- plugins/jenkins-backend/src/service/router.ts | 21 ++++---- 10 files changed, 27 insertions(+), 147 deletions(-) delete mode 100644 plugins/jenkins-backend/src/permissions/conditionExports.ts delete mode 100644 plugins/jenkins-backend/src/permissions/index.ts delete mode 100644 plugins/jenkins-backend/src/permissions/rules/index.ts diff --git a/.changeset/orange-cobras-shave.md b/.changeset/orange-cobras-shave.md index 2b840dab48..9f54f6d48b 100644 --- a/.changeset/orange-cobras-shave.md +++ b/.changeset/orange-cobras-shave.md @@ -2,5 +2,11 @@ '@backstage/plugin-jenkins-backend': minor --- -Add permissions support for Jenkins backend and provide an integration router. You must pass a permission router -if you want to enable permissions in Jenkins plugin. +Jenkins plugin supports permissions now. We have added a new permission, so you can manage the permission for the users. +A new permission `jenkinsExecutePermission` is provided in `jenkins-common` package. This permission rule will be applied to check rebuild actions +if user is allowed to execute this action. + +> We use 'catalog-entity' as a resource type, so you need to integrate a policy to handle catalog-entity resources + +> You need to use this permission in your permission policy to check the user role/rights and return +> `AuthorizeResult.ALLOW` to allow rebuild action to logged user. (e.g: you can check if user or related group owns the entity) diff --git a/.changeset/ten-fireants-march.md b/.changeset/ten-fireants-march.md index 9376292196..8283eef5d6 100644 --- a/.changeset/ten-fireants-march.md +++ b/.changeset/ten-fireants-march.md @@ -2,4 +2,7 @@ '@backstage/plugin-jenkins': minor --- -Integrated the permission plugin, if enabled, the actions can be executed only by owned user (or assigned to related group) +Jenkins plugin supports permissions now. We have added a new permission, so you can manage the permission for the users. See relates notes for `jenkins-plugin` for more details. + +Rebuild action will be disabled if the user does not have necessary rights to execute rebuild action. A permission policy (defined in backend) must handle and check the identity rights +and return `AuthorizeResult.ALLOW` if user is allowed to execute rebuild action. diff --git a/plugins/jenkins-backend/api-report.md b/plugins/jenkins-backend/api-report.md index 41bebb1061..7af3968897 100644 --- a/plugins/jenkins-backend/api-report.md +++ b/plugins/jenkins-backend/api-report.md @@ -4,26 +4,11 @@ ```ts import { CatalogApi } from '@backstage/catalog-client'; -import { ConditionalPolicyDecision } from '@backstage/plugin-permission-node'; -import { Conditions } from '@backstage/plugin-permission-node'; import { Config } from '@backstage/config'; -import { EntitiesSearchFilter } from '@backstage/plugin-catalog-backend'; -import { Entity } from '@backstage/catalog-model'; import { EntityName } from '@backstage/catalog-model'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; -import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; -import { PermissionCondition } from '@backstage/plugin-permission-common'; -import { PermissionCriteria } from '@backstage/plugin-permission-common'; -import { PermissionRule } from '@backstage/plugin-permission-node'; -import { PluginEndpointDiscovery } from '@backstage/backend-common'; - -// Warning: (ae-missing-release-tag) "createJenkinsPermissionPolicy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const createJenkinsPermissionPolicy: ( - conditions: PermissionCriteria>, -) => ConditionalPolicyDecision; +import type { Logger as Logger_2 } from 'winston'; +import type { PermissionAuthorizer } from '@backstage/plugin-permission-common'; // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -50,17 +35,6 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { static readonly OLD_JENKINS_ANNOTATION = 'jenkins.io/github-folder'; } -// Warning: (ae-missing-release-tag) "jenkinsConditions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const jenkinsConditions: Conditions<{ - isEntityOwner: PermissionRule< - Entity, - EntitiesSearchFilter, - [claims: string[]] - >; -}>; - // Warning: (ae-missing-release-tag) "JenkinsConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -113,30 +87,10 @@ export interface JenkinsInstanceConfig { username: string; } -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// -// @public -export const jenkinsPermissionRules: { - isEntityOwner: PermissionRule< - Entity, - EntitiesSearchFilter, - [claims: string[]] - >; -}; - // Warning: (ae-missing-release-tag) "RouterOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export interface RouterOptions { - // (undocumented) - config?: Config; - // (undocumented) - discovery?: PluginEndpointDiscovery; - // (undocumented) - fetchApi?: { - fetch: typeof fetch; - }; // (undocumented) jenkinsInfoProvider: JenkinsInfoProvider; // (undocumented) diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index f0659f42d8..64278618af 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -31,11 +31,8 @@ "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", "@backstage/plugin-auth-node": "^0.1.3", - "@backstage/plugin-catalog-backend": "^0.22.0", - "@backstage/plugin-catalog-common": "^0.1.4", "@backstage/plugin-jenkins-common": "^0.1.0", "@backstage/plugin-permission-common": "^0.5.1", - "@backstage/plugin-permission-node": "^0.5.2", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", diff --git a/plugins/jenkins-backend/src/index.ts b/plugins/jenkins-backend/src/index.ts index c584842e07..c55335c52d 100644 --- a/plugins/jenkins-backend/src/index.ts +++ b/plugins/jenkins-backend/src/index.ts @@ -21,4 +21,3 @@ */ export * from './service'; -export * from './permissions'; diff --git a/plugins/jenkins-backend/src/permissions/conditionExports.ts b/plugins/jenkins-backend/src/permissions/conditionExports.ts deleted file mode 100644 index 197f2cb0ec..0000000000 --- a/plugins/jenkins-backend/src/permissions/conditionExports.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * 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 { jenkinsPermissionRules } from './rules'; -import { createConditionExports } from '@backstage/plugin-permission-node'; -import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common'; - -const { conditions, createPolicyDecision } = createConditionExports({ - pluginId: 'jenkins', - resourceType: RESOURCE_TYPE_CATALOG_ENTITY, - rules: jenkinsPermissionRules, -}); - -export const jenkinsConditions = conditions; -export const createJenkinsPermissionPolicy = createPolicyDecision; diff --git a/plugins/jenkins-backend/src/permissions/index.ts b/plugins/jenkins-backend/src/permissions/index.ts deleted file mode 100644 index 522f40aeee..0000000000 --- a/plugins/jenkins-backend/src/permissions/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * 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 { - createJenkinsPermissionPolicy, - jenkinsConditions, -} from './conditionExports'; -export * from './rules'; diff --git a/plugins/jenkins-backend/src/permissions/rules/index.ts b/plugins/jenkins-backend/src/permissions/rules/index.ts deleted file mode 100644 index 0cd07b26a8..0000000000 --- a/plugins/jenkins-backend/src/permissions/rules/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * 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 { permissionRules } from '@backstage/plugin-catalog-backend'; -/** - * - * Jenkins' permission rules can be used to defined different kind of rules to check if the user authorizes an action - * (or listing the jobs ever) - * - * Provided rules: - * {isEntityOwner} can be used to determine if a user or the group of the user - * owns an entity. - * - * @public - */ -export const jenkinsPermissionRules = { - isEntityOwner: permissionRules.isEntityOwner, -}; diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.ts b/plugins/jenkins-backend/src/service/jenkinsApi.ts index c0b1c63112..da16bbc553 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { JenkinsInfo } from './jenkinsInfoProvider'; +import type { JenkinsInfo } from './jenkinsInfoProvider'; import jenkins from 'jenkins'; -import { +import type { BackstageBuild, BackstageProject, JenkinsBuild, @@ -139,13 +139,14 @@ export class JenkinsApiImpl { async buildProject( jenkinsInfo: JenkinsInfo, jobFullName: string, + resourceRef?: string, options?: { token?: string }, ) { const client = await JenkinsApiImpl.getClient(jenkinsInfo); if (this.permissionApi) { const response = await this.permissionApi.authorize( - [{ permission: jenkinsExecutePermission }], + [{ permission: jenkinsExecutePermission, resourceRef }], { token: options?.token }, ); // permission api returns always at least one item, we need to check only one result since we do not expect any additional results diff --git a/plugins/jenkins-backend/src/service/router.ts b/plugins/jenkins-backend/src/service/router.ts index 89d755efed..dcef2c2e0a 100644 --- a/plugins/jenkins-backend/src/service/router.ts +++ b/plugins/jenkins-backend/src/service/router.ts @@ -14,25 +14,19 @@ * limitations under the License. */ -import { - errorHandler, - PluginEndpointDiscovery, -} from '@backstage/backend-common'; +import { errorHandler } from '@backstage/backend-common'; import express from 'express'; import Router from 'express-promise-router'; -import { Logger } from 'winston'; -import { JenkinsInfoProvider } from './jenkinsInfoProvider'; +import type { Logger } from 'winston'; +import type { JenkinsInfoProvider } from './jenkinsInfoProvider'; import { JenkinsApiImpl } from './jenkinsApi'; -import { Config } from '@backstage/config'; -import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; +import type { PermissionAuthorizer } from '@backstage/plugin-permission-common'; import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; +import { stringifyEntityRef } from '@backstage/catalog-model'; export interface RouterOptions { logger: Logger; jenkinsInfoProvider: JenkinsInfoProvider; - config?: Config; - discovery?: PluginEndpointDiscovery; - fetchApi?: { fetch: typeof fetch }; permissions?: PermissionAuthorizer; } @@ -125,7 +119,10 @@ export async function createRouter( request.header('authorization'), ); - await jenkinsApi.buildProject(jenkinsInfo, jobFullName, { token }); + const resourceRef = stringifyEntityRef({ kind, namespace, name }); + await jenkinsApi.buildProject(jenkinsInfo, jobFullName, resourceRef, { + token, + }); response.json({}); }, ); From 4368196fba0d145fdb7bb478301fb5f8e92a89b0 Mon Sep 17 00:00:00 2001 From: Nikolas Skoufis Date: Wed, 2 Mar 2022 10:06:23 +1100 Subject: [PATCH 082/353] Accept object in shouldBuild for future extensibility Signed-off-by: Nikolas Skoufis --- plugins/techdocs-backend/api-report.md | 7 ++++++- plugins/techdocs-backend/src/index.ts | 1 + .../src/service/DocsBuildStrategy.test.ts | 4 ++-- .../src/service/DocsBuildStrategy.ts | 13 +++++++++++-- plugins/techdocs-backend/src/service/index.ts | 5 ++++- plugins/techdocs-backend/src/service/router.ts | 2 +- 6 files changed, 25 insertions(+), 7 deletions(-) diff --git a/plugins/techdocs-backend/api-report.md b/plugins/techdocs-backend/api-report.md index 3ce3a2504b..2bf4664f77 100644 --- a/plugins/techdocs-backend/api-report.md +++ b/plugins/techdocs-backend/api-report.md @@ -45,7 +45,7 @@ export class DefaultTechDocsCollator implements DocumentCollator { // @public export interface DocsBuildStrategy { // (undocumented) - shouldBuild(entity: Entity): Promise; + shouldBuild(params: ShouldBuildParameters): Promise; } // @public @@ -76,6 +76,11 @@ export type RouterOptions = | RecommendedDeploymentOptions | OutOfTheBoxDeploymentOptions; +// @public +export type ShouldBuildParameters = { + entity: Entity; +}; + // @public export type TechDocsCollatorOptions = { discovery: PluginEndpointDiscovery; diff --git a/plugins/techdocs-backend/src/index.ts b/plugins/techdocs-backend/src/index.ts index 2a17bf2736..01acbea2cc 100644 --- a/plugins/techdocs-backend/src/index.ts +++ b/plugins/techdocs-backend/src/index.ts @@ -26,6 +26,7 @@ export type { RecommendedDeploymentOptions, OutOfTheBoxDeploymentOptions, DocsBuildStrategy, + ShouldBuildParameters, } from './service'; export { DefaultTechDocsCollator } from './search'; diff --git a/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts b/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts index 76b70e67e9..84bd960f8f 100644 --- a/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts +++ b/plugins/techdocs-backend/src/service/DocsBuildStrategy.test.ts @@ -46,7 +46,7 @@ describe('DefaultDocsBuildStrategy', () => { MockedConfigReader.prototype.getString.mockReturnValue('local'); - const result = await defaultDocsBuildStrategy.shouldBuild(entity); + const result = await defaultDocsBuildStrategy.shouldBuild({ entity }); expect(result).toBe(true); }); @@ -57,7 +57,7 @@ describe('DefaultDocsBuildStrategy', () => { MockedConfigReader.prototype.getString.mockReturnValue('external'); - const result = await defaultDocsBuildStrategy.shouldBuild(entity); + const result = await defaultDocsBuildStrategy.shouldBuild({ entity }); expect(result).toBe(false); }); diff --git a/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts b/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts index e0220d7a32..42a16234f9 100644 --- a/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts +++ b/plugins/techdocs-backend/src/service/DocsBuildStrategy.ts @@ -16,13 +16,22 @@ import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; +/** + * Parameters passed to the shouldBuild method on the DocsBuildStrategy interface + * + * @public + */ +export type ShouldBuildParameters = { + entity: Entity; +}; + /** * A strategy for when to build TechDocs locally, and when to skip building TechDocs (allowing for an external build) * * @public */ export interface DocsBuildStrategy { - shouldBuild(entity: Entity): Promise; + shouldBuild(params: ShouldBuildParameters): Promise; } export class DefaultDocsBuildStrategy { @@ -36,7 +45,7 @@ export class DefaultDocsBuildStrategy { return new DefaultDocsBuildStrategy(config); } - async shouldBuild(_: Entity): Promise { + async shouldBuild(_: ShouldBuildParameters): Promise { return this.config.getString('techdocs.builder') === 'local'; } } diff --git a/plugins/techdocs-backend/src/service/index.ts b/plugins/techdocs-backend/src/service/index.ts index 29db2d2038..7355a34e32 100644 --- a/plugins/techdocs-backend/src/service/index.ts +++ b/plugins/techdocs-backend/src/service/index.ts @@ -20,4 +20,7 @@ export type { RecommendedDeploymentOptions, OutOfTheBoxDeploymentOptions, } from './router'; -export type { DocsBuildStrategy } from './DocsBuildStrategy'; +export type { + DocsBuildStrategy, + ShouldBuildParameters, +} from './DocsBuildStrategy'; diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index 4d51ebd485..abe83b3d47 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -221,7 +221,7 @@ export async function createRouter( // techdocs-backend will only try to build documentation for an entity if techdocs.builder is set to 'local' // If set to 'external', it will assume that an external process (e.g. CI/CD pipeline // of the repository) is responsible for building and publishing documentation to the storage provider - const shouldBuild = await docsBuildStrategy.shouldBuild(entity); + const shouldBuild = await docsBuildStrategy.shouldBuild({ entity }); if (!shouldBuild) { // However, if caching is enabled, take the opportunity to check and // invalidate stale cache entries. From 63bb0a96449fd3ff9b2448596027c26446fd03d9 Mon Sep 17 00:00:00 2001 From: Nikolas Skoufis Date: Wed, 2 Mar 2022 10:09:28 +1100 Subject: [PATCH 083/353] Update comments and logs for shouldBuild Signed-off-by: Nikolas Skoufis --- plugins/techdocs-backend/src/service/router.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index abe83b3d47..183e26c95d 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -218,9 +218,11 @@ export async function createRouter( responseHandler = createEventStream(res); } - // techdocs-backend will only try to build documentation for an entity if techdocs.builder is set to 'local' - // If set to 'external', it will assume that an external process (e.g. CI/CD pipeline - // of the repository) is responsible for building and publishing documentation to the storage provider + // By default, techdocs-backend will only try to build documentation for an entity if techdocs.builder is set to + // 'local'. If set to 'external', it will assume that an external process (e.g. CI/CD pipeline + // of the repository) is responsible for building and publishing documentation to the storage provider. + // Altering the implementation of the injected docsBuildStrategy allows for more complex behaviours, based on + // either config or the properties of the entity (e.g. annotations, labels, spec fields etc.). const shouldBuild = await docsBuildStrategy.shouldBuild({ entity }); if (!shouldBuild) { // However, if caching is enabled, take the opportunity to check and @@ -253,7 +255,7 @@ export async function createRouter( responseHandler.error( new Error( - "Invalid configuration. 'techdocs.builder' was set to 'local' but no 'preparer' was provided to the router initialization.", + "Invalid configuration. 'docsBuildStrategy.shouldBuild returned 'true', but no 'preparer' was provided to the router initialization.", ), ); }); From 9a0510144f31ae354bc9906283f3614ac708c3a9 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Tue, 1 Mar 2022 18:21:32 -0500 Subject: [PATCH 084/353] refactor(redis): require protocol in connection string Signed-off-by: Phil Kuang --- .changeset/chilled-items-trade.md | 13 +++++++++++++ docs/overview/architecture-overview.md | 2 +- packages/backend-common/config.d.ts | 2 +- .../backend-common/src/cache/CacheManager.test.ts | 7 +++---- packages/backend-common/src/cache/CacheManager.ts | 2 +- 5 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 .changeset/chilled-items-trade.md diff --git a/.changeset/chilled-items-trade.md b/.changeset/chilled-items-trade.md new file mode 100644 index 0000000000..b069885498 --- /dev/null +++ b/.changeset/chilled-items-trade.md @@ -0,0 +1,13 @@ +--- +'@backstage/backend-common': minor +--- + +**BREAKING**: The connection string for `redis` cache store now requires a protocol prefix. + +```diff +backend: + cache: + store: redis +- connection: user:pass@cache.example.com:6379 ++ connection: redis://user:pass@cache.example.com:6379 +``` diff --git a/docs/overview/architecture-overview.md b/docs/overview/architecture-overview.md index 358dc53852..5b6b81cf10 100644 --- a/docs/overview/architecture-overview.md +++ b/docs/overview/architecture-overview.md @@ -310,7 +310,7 @@ backend: backend: cache: store: redis - connection: user:pass@cache.example.com:6379 + connection: redis://user:pass@cache.example.com:6379 ``` Contributions supporting other cache stores are welcome! diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index 57645d9fda..0c3e3132c0 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -137,7 +137,7 @@ export interface Config { | { store: 'redis'; /** - * A redis connection string in the form `user:pass@host:port`. + * A redis connection string in the form `redis://user:pass@host:port`. * @secret */ connection: string; diff --git a/packages/backend-common/src/cache/CacheManager.test.ts b/packages/backend-common/src/cache/CacheManager.test.ts index 660aa82bb0..c6cad2e49b 100644 --- a/packages/backend-common/src/cache/CacheManager.test.ts +++ b/packages/backend-common/src/cache/CacheManager.test.ts @@ -195,14 +195,13 @@ describe('CacheManager', () => { }); it('returns a Redis client when configured', () => { - const redisHostAndPort = '127.0.0.1:6379'; - const expectedHost = `redis://${redisHostAndPort}`; + const redisConnection = 'redis://127.0.0.1:6379'; const manager = CacheManager.fromConfig( new ConfigReader({ backend: { cache: { store: 'redis', - connection: redisHostAndPort, + connection: redisConnection, }, }, }), @@ -218,7 +217,7 @@ describe('CacheManager', () => { expect(mockCacheCalls[0][0].store).toBeInstanceOf(KeyvRedis); const redis = KeyvRedis as jest.Mock; const mockRedisCalls = redis.mock.calls.splice(-1); - expect(mockRedisCalls[0][0]).toEqual(expectedHost); + expect(mockRedisCalls[0][0]).toEqual(redisConnection); }); describe('connection errors', () => { diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts index d63ac6d940..d8652e42d8 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -133,7 +133,7 @@ export class CacheManager { return new Keyv({ namespace: pluginId, ttl: defaultTtl, - store: new KeyvRedis(`redis://${this.connection}`), + store: new KeyvRedis(this.connection), }); } From eed6b57cf84dd6fe896684a8693ba2a2c71c9cec Mon Sep 17 00:00:00 2001 From: Nikolas Skoufis Date: Wed, 2 Mar 2022 11:09:48 +1100 Subject: [PATCH 085/353] Update error message in tests Signed-off-by: Nikolas Skoufis --- plugins/techdocs-backend/src/service/router.test.ts | 4 ++-- plugins/techdocs-backend/src/service/router.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/techdocs-backend/src/service/router.test.ts b/plugins/techdocs-backend/src/service/router.test.ts index 521024252c..c7cda203bb 100644 --- a/plugins/techdocs-backend/src/service/router.test.ts +++ b/plugins/techdocs-backend/src/service/router.test.ts @@ -216,7 +216,7 @@ describe('createRouter', () => { expect(response.status).toBe(500); expect(response.text).toMatch( - /Invalid configuration\. 'techdocs\.builder' was set to 'local' but no 'preparer' was provided to the router initialization/, + /Invalid configuration\. docsBuildStrategy\.shouldBuild returned 'true', but no 'preparer' was provided to the router initialization./, ); expect(MockDocsSynchronizer.prototype.doSync).toBeCalledTimes(0); @@ -343,7 +343,7 @@ data: {"updated":false} expect(response.get('content-type')).toBe('text/event-stream'); expect(response.text).toEqual( `event: error -data: "Invalid configuration. 'techdocs.builder' was set to 'local' but no 'preparer' was provided to the router initialization." +data: "Invalid configuration. docsBuildStrategy.shouldBuild returned 'true', but no 'preparer' was provided to the router initialization." `, ); diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index 183e26c95d..73f212d049 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -255,7 +255,7 @@ export async function createRouter( responseHandler.error( new Error( - "Invalid configuration. 'docsBuildStrategy.shouldBuild returned 'true', but no 'preparer' was provided to the router initialization.", + "Invalid configuration. docsBuildStrategy.shouldBuild returned 'true', but no 'preparer' was provided to the router initialization.", ), ); }); From 5d98a4263eb94a316d0ed956baaad8ea6e254865 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Wed, 2 Mar 2022 00:15:26 +0000 Subject: [PATCH 086/353] Added working tests, one of which fails because of a bug in the code that needs to be fixed Signed-off-by: Karan Shah --- plugins/airbrake/src/api/ProductionApi.test.ts | 7 +++++++ .../EntityAirbrakeWidget.test.tsx | 14 +++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/plugins/airbrake/src/api/ProductionApi.test.ts b/plugins/airbrake/src/api/ProductionApi.test.ts index f09127b4c2..41aef38a27 100644 --- a/plugins/airbrake/src/api/ProductionApi.test.ts +++ b/plugins/airbrake/src/api/ProductionApi.test.ts @@ -20,6 +20,7 @@ import mockGroupsData from './mock/airbrakeGroupsApiMock.json'; import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/test-utils'; import { localDiscoveryApi } from './mock'; +import { NoProjectIdError } from './AirbrakeApi'; describe('The production Airbrake API', () => { const productionApi = new ProductionAirbrakeApi(localDiscoveryApi); @@ -53,4 +54,10 @@ describe('The production Airbrake API', () => { await expect(productionApi.fetchGroups('123456')).rejects.toThrow(); }); + + it('throws if project ID is empty', async () => { + await expect(productionApi.fetchGroups('')).rejects.toThrowError( + NoProjectIdError, + ); + }); }); diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx index ee14a17f9a..6211500b10 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx @@ -23,9 +23,9 @@ import { setupRequestMockHandlers, TestApiProvider, } from '@backstage/test-utils'; -import { createEntity } from '../../api'; import { airbrakeApiRef, + createEntity, localDiscoveryApi, MockAirbrakeApi, ProductionAirbrakeApi, @@ -52,15 +52,23 @@ describe('EntityAirbrakeContent', () => { } }); - it('states that the annotation is missing if no project ID annotation is provided', async () => { + it('states that the annotation is missing if no project ID annotation is provided but does not error', async () => { + const mockErrorApi = new MockErrorApi({ collect: true }); + const widget = await renderInTestApp( - + , ); await expect( widget.findByText('Missing Annotation'), ).resolves.toBeInTheDocument(); + expect(mockErrorApi.getErrors().length).toBe(0); }); it('states that an error occurred if the API call fails', async () => { From 0cd42252c98dffb9329498508cb9b712e5fc2be6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 04:08:55 +0000 Subject: [PATCH 087/353] chore(deps): bump @typescript-eslint/parser from 5.9.1 to 5.13.0 Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.9.1 to 5.13.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.13.0/packages/parser) --- updated-dependencies: - dependency-name: "@typescript-eslint/parser" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 74 +++++++++++++++++++++++++++---------------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/yarn.lock b/yarn.lock index 69a359daa7..bc2dad5f55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6710,15 +6710,23 @@ eslint-utils "^3.0.0" "@typescript-eslint/parser@^5.9.0": - version "5.9.1" - resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.9.1.tgz#b114011010a87e17b3265ca715e16c76a9834cef" - integrity sha512-PLYO0AmwD6s6n0ZQB5kqPgfvh73p0+VqopQQLuNfi7Lm0EpfKyDalchpVwkE+81k5HeiRrTV/9w1aNHzjD7C4g== + version "5.13.0" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.13.0.tgz#0394ed8f2f849273c0bf4b811994d177112ced5c" + integrity sha512-GdrU4GvBE29tm2RqWOM0P5QfCtgCyN4hXICj/X9ibKED16136l9ZpoJvCL5pSKtmJzA+NRDzQ312wWMejCVVfg== dependencies: - "@typescript-eslint/scope-manager" "5.9.1" - "@typescript-eslint/types" "5.9.1" - "@typescript-eslint/typescript-estree" "5.9.1" + "@typescript-eslint/scope-manager" "5.13.0" + "@typescript-eslint/types" "5.13.0" + "@typescript-eslint/typescript-estree" "5.13.0" debug "^4.3.2" +"@typescript-eslint/scope-manager@5.13.0": + version "5.13.0" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.13.0.tgz#cf6aff61ca497cb19f0397eea8444a58f46156b6" + integrity sha512-T4N8UvKYDSfVYdmJq7g2IPJYCRzwtp74KyDZytkR4OL3NRupvswvmJQJ4CX5tDSurW2cvCc1Ia1qM7d0jpa7IA== + dependencies: + "@typescript-eslint/types" "5.13.0" + "@typescript-eslint/visitor-keys" "5.13.0" + "@typescript-eslint/scope-manager@5.9.0": version "5.9.0" resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.9.0.tgz#02dfef920290c1dcd7b1999455a3eaae7a1a3117" @@ -6727,14 +6735,6 @@ "@typescript-eslint/types" "5.9.0" "@typescript-eslint/visitor-keys" "5.9.0" -"@typescript-eslint/scope-manager@5.9.1": - version "5.9.1" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.9.1.tgz#6c27be89f1a9409f284d95dfa08ee3400166fe69" - integrity sha512-8BwvWkho3B/UOtzRyW07ffJXPaLSUKFBjpq8aqsRvu6HdEuzCY57+ffT7QoV4QXJXWSU1+7g3wE4AlgImmQ9pQ== - dependencies: - "@typescript-eslint/types" "5.9.1" - "@typescript-eslint/visitor-keys" "5.9.1" - "@typescript-eslint/type-utils@5.9.0": version "5.9.0" resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.9.0.tgz#fd5963ead04bc9b7af9c3a8e534d8d39f1ce5f93" @@ -6744,15 +6744,28 @@ debug "^4.3.2" tsutils "^3.21.0" +"@typescript-eslint/types@5.13.0": + version "5.13.0" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.13.0.tgz#da1de4ae905b1b9ff682cab0bed6b2e3be9c04e5" + integrity sha512-LmE/KO6DUy0nFY/OoQU0XelnmDt+V8lPQhh8MOVa7Y5k2gGRd6U9Kp3wAjhB4OHg57tUO0nOnwYQhRRyEAyOyg== + "@typescript-eslint/types@5.9.0": version "5.9.0" resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.9.0.tgz#e5619803e39d24a03b3369506df196355736e1a3" integrity sha512-mWp6/b56Umo1rwyGCk8fPIzb9Migo8YOniBGPAQDNC6C52SeyNGN4gsVwQTAR+RS2L5xyajON4hOLwAGwPtUwg== -"@typescript-eslint/types@5.9.1": - version "5.9.1" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.9.1.tgz#1bef8f238a2fb32ebc6ff6d75020d9f47a1593c6" - integrity sha512-SsWegWudWpkZCwwYcKoDwuAjoZXnM1y2EbEerTHho19Hmm+bQ56QG4L4jrtCu0bI5STaRTvRTZmjprWlTw/5NQ== +"@typescript-eslint/typescript-estree@5.13.0": + version "5.13.0" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.13.0.tgz#b37c07b748ff030a3e93d87c842714e020b78141" + integrity sha512-Q9cQow0DeLjnp5DuEDjLZ6JIkwGx3oYZe+BfcNuw/POhtpcxMTy18Icl6BJqTSd+3ftsrfuVb7mNHRZf7xiaNA== + dependencies: + "@typescript-eslint/types" "5.13.0" + "@typescript-eslint/visitor-keys" "5.13.0" + debug "^4.3.2" + globby "^11.0.4" + is-glob "^4.0.3" + semver "^7.3.5" + tsutils "^3.21.0" "@typescript-eslint/typescript-estree@5.9.0": version "5.9.0" @@ -6767,18 +6780,13 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.9.1": - version "5.9.1" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.9.1.tgz#d5b996f49476495070d2b8dd354861cf33c005d6" - integrity sha512-gL1sP6A/KG0HwrahVXI9fZyeVTxEYV//6PmcOn1tD0rw8VhUWYeZeuWHwwhnewnvEMcHjhnJLOBhA9rK4vmb8A== +"@typescript-eslint/visitor-keys@5.13.0": + version "5.13.0" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.13.0.tgz#f45ff55bcce16403b221ac9240fbeeae4764f0fd" + integrity sha512-HLKEAS/qA1V7d9EzcpLFykTePmOQqOFim8oCvhY3pZgQ8Hi38hYpHd9e5GN6nQBFQNecNhws5wkS9Y5XIO0s/g== dependencies: - "@typescript-eslint/types" "5.9.1" - "@typescript-eslint/visitor-keys" "5.9.1" - debug "^4.3.2" - globby "^11.0.4" - is-glob "^4.0.3" - semver "^7.3.5" - tsutils "^3.21.0" + "@typescript-eslint/types" "5.13.0" + eslint-visitor-keys "^3.0.0" "@typescript-eslint/visitor-keys@5.9.0": version "5.9.0" @@ -6788,14 +6796,6 @@ "@typescript-eslint/types" "5.9.0" eslint-visitor-keys "^3.0.0" -"@typescript-eslint/visitor-keys@5.9.1": - version "5.9.1" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.9.1.tgz#f52206f38128dd4f675cf28070a41596eee985b7" - integrity sha512-Xh37pNz9e9ryW4TVdwiFzmr4hloty8cFj8GTWMXh3Z8swGwyQWeCcNgF0hm6t09iZd6eiZmIf4zHedQVP6TVtg== - dependencies: - "@typescript-eslint/types" "5.9.1" - eslint-visitor-keys "^3.0.0" - "@vscode/sqlite3@^5.0.7": version "5.0.7" resolved "https://registry.npmjs.org/@vscode/sqlite3/-/sqlite3-5.0.7.tgz#358df36bb0e9e735c54785e3e4b9b2dce1d32895" From b2ccb676c2881c7ea9ba2a888c8f68a4f9a01fcf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Mar 2022 04:09:23 +0000 Subject: [PATCH 088/353] chore(deps): bump raw-body from 2.5.0 to 2.5.1 Bumps [raw-body](https://github.com/stream-utils/raw-body) from 2.5.0 to 2.5.1. - [Release notes](https://github.com/stream-utils/raw-body/releases) - [Changelog](https://github.com/stream-utils/raw-body/blob/master/HISTORY.md) - [Commits](https://github.com/stream-utils/raw-body/compare/2.5.0...2.5.1) --- updated-dependencies: - dependency-name: raw-body dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 69a359daa7..427eb00081 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20645,9 +20645,9 @@ raw-body@2.4.3: unpipe "1.0.0" raw-body@^2.4.1: - version "2.5.0" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.0.tgz#865890d9435243e9fe6141feb4decf929a6e1525" - integrity sha512-XpyZ6O7PVu3ItMQl0LslfsRoKxMOxi3SzDkrOtxMES5AqLFpYjQCryxI4LGygUN2jL+RgFsPkMPPlG7cg/47+A== + version "2.5.1" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== dependencies: bytes "3.1.2" http-errors "2.0.0" From ac7b1161a6c403388bc1cbabe3947e0b7c78bd5a Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 1 Mar 2022 15:50:03 +0100 Subject: [PATCH 089/353] catalog-model: Remove deprecations Signed-off-by: Johan Haals --- .changeset/weak-news-reply.md | 23 ++ packages/catalog-model/api-report.md | 86 ----- .../catalog-model/src/entity/constants.ts | 36 -- packages/catalog-model/src/entity/index.ts | 12 - packages/catalog-model/src/entity/ref.test.ts | 319 +----------------- packages/catalog-model/src/entity/ref.ts | 77 ----- .../catalog-model/src/entity/util.test.ts | 198 ----------- packages/catalog-model/src/entity/util.ts | 133 -------- packages/catalog-model/src/index.ts | 2 +- .../catalog-model/src/location/annotation.ts | 24 -- .../catalog-model/src/location/helpers.ts | 27 -- packages/catalog-model/src/location/index.ts | 7 +- packages/catalog-model/src/location/types.ts | 10 - packages/catalog-model/src/types.ts | 11 - 14 files changed, 26 insertions(+), 939 deletions(-) create mode 100644 .changeset/weak-news-reply.md delete mode 100644 packages/catalog-model/src/entity/util.test.ts delete mode 100644 packages/catalog-model/src/entity/util.ts diff --git a/.changeset/weak-news-reply.md b/.changeset/weak-news-reply.md new file mode 100644 index 0000000000..3becabc2e6 --- /dev/null +++ b/.changeset/weak-news-reply.md @@ -0,0 +1,23 @@ +--- +'@backstage/catalog-model': minor +--- + +**Breaking**: The following changes are all breaking changes. + +Removed `EDIT_URL_ANNOTATION` and `VIEW_URL_ANNOTATION`, `LOCATION_ANNOTATION`, `ORIGIN_LOCATION_ANNOTATION`, `LOCATION_ANNOTATION`, `SOURCE_LOCATION_ANNOTATION`. All of these constants have been prefixed with ANNOTATION to be easier to find meaning `SOURCE_LOCATION_ANNOTATION` is available as `ANNOTATION_SOURCE_LOCATION`. + +Removed `parseLocationReference`, replaced by `parseLocationRef`. + +Removed `stringifyLocationReference`, replaced by `stringifyLocationRef`. + +Removed `Location` type which has been moved to `catalog-client`. + +Removed `ENTITY_DEFAULT_NAMESPACE`, replaced by `DEFAULT_NAMESPACE`. + +Removed `compareEntityToRef` compare using `stringifyEntityRef` instead. + +Removed `JSONSchema` type which should be imported from `json-schema` package instead. + +Removed utility methods: `entityHasChanges`, `generateEntityEtag`, `generateEntityUid`, `generateUpdatedEntity`. + +Removed `ENTITY_META_GENERATED_FIELDS` and `EntityRefContext`. diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 210d0187e2..4574129d6f 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -4,8 +4,6 @@ ```ts import { JsonObject } from '@backstage/types'; -import { JSONSchema7 } from 'json-schema'; -import { JsonValue } from '@backstage/types'; import { SerializedError } from '@backstage/errors'; // @alpha @@ -66,23 +64,6 @@ export class CommonValidatorFunctions { static isValidUrl(value: unknown): boolean; } -// @public @deprecated -export function compareEntityToRef( - entity: Entity, - ref: - | string - | { - kind?: string; - namespace?: string; - name: string; - } - | EntityName, - context?: { - defaultKind?: string; - defaultNamespace?: string; - }, -): boolean; - // @public interface ComponentEntityV1alpha1 extends Entity { // (undocumented) @@ -134,9 +115,6 @@ export { DomainEntityV1alpha1 }; // @public export const domainEntityV1alpha1Validator: KindValidator; -// @public @deprecated -export const EDIT_URL_ANNOTATION = 'backstage.io/edit-url'; - // @public export type Entity = { apiVersion: string; @@ -146,16 +124,6 @@ export type Entity = { relations?: EntityRelation[]; }; -// @public @deprecated -export const ENTITY_DEFAULT_NAMESPACE = 'default'; - -// @public @deprecated -export const ENTITY_META_GENERATED_FIELDS: readonly [ - 'uid', - 'etag', - 'generation', -]; - // @public export type EntityEnvelope = { apiVersion: string; @@ -171,9 +139,6 @@ export function entityEnvelopeSchemaValidator< T extends EntityEnvelope = EntityEnvelope, >(schema?: unknown): (data: unknown) => T; -// @public @deprecated -export function entityHasChanges(previous: Entity, next: Entity): boolean; - // @public export function entityKindSchemaValidator( schema: unknown, @@ -228,12 +193,6 @@ export type EntityRef = name: string; }; -// @public @deprecated -export type EntityRefContext = { - defaultKind?: string; - defaultNamespace?: string; -}; - // @public export type EntityRelation = { type: string; @@ -269,15 +228,6 @@ export class FieldFormatEntityPolicy implements EntityPolicy { enforce(entity: Entity): Promise; } -// @public @deprecated -export function generateEntityEtag(): string; - -// @public @deprecated -export function generateEntityUid(): string; - -// @public @deprecated -export function generateUpdatedEntity(previous: Entity, next: Entity): Entity; - // @public export function getEntityName(entity: Entity): EntityName; @@ -312,11 +262,6 @@ export { GroupEntityV1alpha1 }; // @public export const groupEntityV1alpha1Validator: KindValidator; -// @public @deprecated -export type JSONSchema = JSONSchema7 & { - [key in string]?: JsonValue; -}; - // @public export type KindValidator = { check(entity: Entity): Promise; @@ -342,15 +287,6 @@ export class KubernetesValidatorFunctions { static isValidObjectName(value: unknown): boolean; } -// @public @deprecated -type Location_2 = { - id: string; -} & LocationSpec; -export { Location_2 as Location }; - -// @public @deprecated -export const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; - // @public interface LocationEntityV1alpha1 extends Entity { // (undocumented) @@ -388,10 +324,6 @@ export class NoForeignRootFieldsEntityPolicy implements EntityPolicy { enforce(entity: Entity): Promise; } -// @public @deprecated -export const ORIGIN_LOCATION_ANNOTATION = - 'backstage.io/managed-by-origin-location'; - // @public @deprecated export function parseEntityName( ref: @@ -428,12 +360,6 @@ export function parseLocationRef(ref: string): { target: string; }; -// @public @deprecated -export function parseLocationReference(ref: string): { - type: string; - target: string; -}; - // @public export const RELATION_API_CONSUMED_BY = 'apiConsumedBy'; @@ -503,9 +429,6 @@ export class SchemaValidEntityPolicy implements EntityPolicy { enforce(entity: Entity): Promise; } -// @public @deprecated -export const SOURCE_LOCATION_ANNOTATION = 'backstage.io/source-location'; - // @public export function stringifyEntityRef( ref: @@ -523,12 +446,6 @@ export function stringifyLocationRef(ref: { target: string; }): string; -// @public @deprecated -export function stringifyLocationReference(ref: { - type: string; - target: string; -}): string; - // @public interface SystemEntityV1alpha1 extends Entity { // (undocumented) @@ -581,7 +498,4 @@ export type Validators = { isValidAnnotationValue(value: unknown): boolean; isValidTag(value: unknown): boolean; }; - -// @public @deprecated -export const VIEW_URL_ANNOTATION = 'backstage.io/view-url'; ``` diff --git a/packages/catalog-model/src/entity/constants.ts b/packages/catalog-model/src/entity/constants.ts index 6cef19cac9..5dc21683bd 100644 --- a/packages/catalog-model/src/entity/constants.ts +++ b/packages/catalog-model/src/entity/constants.ts @@ -14,14 +14,6 @@ * limitations under the License. */ -/** - * The namespace that entities without an explicit namespace fall into. - * - * @public - * @deprecated use {@link DEFAULT_NAMESPACE} instead. - */ -export const ENTITY_DEFAULT_NAMESPACE = 'default'; - /** * The namespace that entities without an explicit namespace fall into. * @@ -29,34 +21,6 @@ export const ENTITY_DEFAULT_NAMESPACE = 'default'; */ export const DEFAULT_NAMESPACE = 'default'; -/** - * The keys of EntityMeta that are auto-generated. - * - * @public - * @deprecated will be removed in a future release. - */ -export const ENTITY_META_GENERATED_FIELDS = [ - 'uid', - 'etag', - 'generation', -] as const; - -/** - * Annotation for linking to entity page from catalog pages. - * - * @public - * @deprecated use {@link ANNOTATION_VIEW_URL} instead. - */ -export const VIEW_URL_ANNOTATION = 'backstage.io/view-url'; - -/** - * Annotation for linking to entity edit page from catalog pages. - * - * @public - * @deprecated use {@link ANNOTATION_EDIT_URL} instead. - */ -export const EDIT_URL_ANNOTATION = 'backstage.io/edit-url'; - /** * Annotation for linking to entity page from catalog pages. * diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts index f619d0f39e..3a346b217c 100644 --- a/packages/catalog-model/src/entity/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -15,11 +15,7 @@ */ export { - EDIT_URL_ANNOTATION, - ENTITY_DEFAULT_NAMESPACE, DEFAULT_NAMESPACE, - ENTITY_META_GENERATED_FIELDS, - VIEW_URL_ANNOTATION, ANNOTATION_EDIT_URL, ANNOTATION_VIEW_URL, } from './constants'; @@ -38,16 +34,8 @@ export type { } from './EntityStatus'; export * from './policies'; export { - compareEntityToRef, getEntityName, parseEntityName, parseEntityRef, stringifyEntityRef, } from './ref'; -export type { EntityRefContext } from './ref'; -export { - entityHasChanges, - generateEntityEtag, - generateEntityUid, - generateUpdatedEntity, -} from './util'; diff --git a/packages/catalog-model/src/entity/ref.test.ts b/packages/catalog-model/src/entity/ref.test.ts index b0c7068fae..045c4710dc 100644 --- a/packages/catalog-model/src/entity/ref.test.ts +++ b/packages/catalog-model/src/entity/ref.test.ts @@ -15,8 +15,7 @@ */ import { DEFAULT_NAMESPACE } from './constants'; -import { Entity } from './Entity'; -import { compareEntityToRef, parseEntityName, parseEntityRef } from './ref'; +import { parseEntityName, parseEntityRef } from './ref'; describe('ref', () => { describe('parseEntityName', () => { @@ -313,320 +312,4 @@ describe('ref', () => { ).toThrow(/namespace/); }); }); - - describe('compareEntityToRef', () => { - const entityWithNamespace: Entity = { - apiVersion: 'a', - kind: 'K', - metadata: { - name: 'n', - namespace: 'ns', - }, - }; - const entityWithoutNamespace: Entity = { - apiVersion: 'a', - kind: 'K', - metadata: { - name: 'n', - }, - }; - - it('handles matching string refs', () => { - expect(compareEntityToRef(entityWithNamespace, 'K:ns/n')).toBe(true); - expect(compareEntityToRef(entityWithNamespace, 'k:nS/N')).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'K:n', { - defaultNamespace: 'ns', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'K:n', { - defaultNamespace: 'Ns', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'ns/n', { defaultKind: 'K' }), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'n', { - defaultKind: 'K', - defaultNamespace: 'ns', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'N', { - defaultKind: 'k', - defaultNamespace: 'nS', - }), - ).toBe(true); - - expect(compareEntityToRef(entityWithoutNamespace, 'K:default/n')).toBe( - true, - ); - expect(compareEntityToRef(entityWithoutNamespace, 'K:deFault/n')).toBe( - true, - ); - expect( - compareEntityToRef(entityWithoutNamespace, 'K:n', { - defaultNamespace: 'default', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithoutNamespace, 'K:n', { - defaultNamespace: 'deFault', - }), - ).toBe(true); - expect(compareEntityToRef(entityWithoutNamespace, 'K:default/n')).toBe( - true, - ); - expect(compareEntityToRef(entityWithoutNamespace, 'K:n')).toBe(true); - expect( - compareEntityToRef(entityWithoutNamespace, 'default/n', { - defaultKind: 'K', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithoutNamespace, 'n', { - defaultKind: 'K', - defaultNamespace: 'default', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithoutNamespace, 'n', { - defaultKind: 'K', - }), - ).toBe(true); - }); - - it('handles mismatching string refs', () => { - expect(compareEntityToRef(entityWithNamespace, 'X:ns/n')).toBe(false); - expect( - compareEntityToRef(entityWithoutNamespace, 'ns/n', { - defaultKind: 'X', - }), - ).toBe(false); - - expect(compareEntityToRef(entityWithNamespace, 'K:xx/n')).toBe(false); - expect( - compareEntityToRef(entityWithoutNamespace, 'K:n', { - defaultNamespace: 'xx', - }), - ).toBe(false); - - expect(compareEntityToRef(entityWithNamespace, 'K:ns/x')).toBe(false); - expect( - compareEntityToRef(entityWithoutNamespace, 'x', { - defaultKind: 'K', - defaultNamespace: 'ns', - }), - ).toBe(false); - }); - - it('handles matching compound refs', () => { - expect( - compareEntityToRef(entityWithNamespace, { - kind: 'K', - namespace: 'ns', - name: 'n', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, { - kind: 'k', - namespace: 'Ns', - name: 'N', - }), - ).toBe(true); - expect( - compareEntityToRef( - entityWithNamespace, - { kind: 'K', name: 'n' }, - { - defaultNamespace: 'ns', - }, - ), - ).toBe(true); - expect( - compareEntityToRef( - entityWithNamespace, - { namespace: 'ns', name: 'n' }, - { defaultKind: 'K' }, - ), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'n', { - defaultKind: 'K', - defaultNamespace: 'ns', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithNamespace, 'N', { - defaultKind: 'k', - defaultNamespace: 'nS', - }), - ).toBe(true); - - expect( - compareEntityToRef(entityWithoutNamespace, { - kind: 'K', - namespace: 'default', - name: 'n', - }), - ).toBe(true); - expect( - compareEntityToRef(entityWithoutNamespace, { - kind: 'k', - namespace: 'deFault', - name: 'N', - }), - ).toBe(true); - expect( - compareEntityToRef( - entityWithoutNamespace, - { kind: 'K', name: 'n' }, - { - defaultNamespace: 'default', - }, - ), - ).toBe(true); - expect( - compareEntityToRef(entityWithoutNamespace, { kind: 'K', name: 'n' }), - ).toBe(true); - expect( - compareEntityToRef( - entityWithoutNamespace, - { namespace: 'default', name: 'n' }, - { - defaultKind: 'K', - }, - ), - ).toBe(true); - expect( - compareEntityToRef( - entityWithoutNamespace, - { name: 'n' }, - { - defaultKind: 'K', - defaultNamespace: 'default', - }, - ), - ).toBe(true); - expect( - compareEntityToRef( - entityWithoutNamespace, - { name: 'N' }, - { - defaultKind: 'k', - defaultNamespace: 'defAult', - }, - ), - ).toBe(true); - expect( - compareEntityToRef( - entityWithoutNamespace, - { name: 'n' }, - { - defaultKind: 'K', - }, - ), - ).toBe(true); - }); - - it('handles mismatching compound refs', () => { - expect( - compareEntityToRef(entityWithNamespace, { - kind: 'X', - namespace: 'ns', - name: 'n', - }), - ).toBe(false); - expect( - compareEntityToRef( - entityWithNamespace, - { - namespace: 'ns', - name: 'n', - }, - { defaultKind: 'X' }, - ), - ).toBe(false); - expect( - compareEntityToRef(entityWithoutNamespace, { - kind: 'X', - namespace: 'default', - name: 'n', - }), - ).toBe(false); - expect( - compareEntityToRef( - entityWithoutNamespace, - { - namespace: 'default', - name: 'n', - }, - { defaultKind: 'X' }, - ), - ).toBe(false); - - expect( - compareEntityToRef(entityWithNamespace, { - kind: 'K', - namespace: 'xx', - name: 'n', - }), - ).toBe(false); - expect( - compareEntityToRef( - entityWithNamespace, - { - kind: 'K', - name: 'n', - }, - { defaultNamespace: 'xx' }, - ), - ).toBe(false); - expect( - compareEntityToRef(entityWithoutNamespace, { - kind: 'K', - namespace: 'xx', - name: 'n', - }), - ).toBe(false); - expect( - compareEntityToRef( - entityWithoutNamespace, - { - kind: 'K', - name: 'n', - }, - { defaultNamespace: 'xx' }, - ), - ).toBe(false); - - expect( - compareEntityToRef(entityWithNamespace, { - kind: 'K', - namespace: 'ns', - name: 'x', - }), - ).toBe(false); - expect( - compareEntityToRef(entityWithoutNamespace, { - kind: 'K', - namespace: 'default', - name: 'x', - }), - ).toBe(false); - expect( - compareEntityToRef( - entityWithoutNamespace, - { - kind: 'K', - name: 'x', - }, - { defaultNamespace: 'default' }, - ), - ).toBe(false); - }); - }); }); diff --git a/packages/catalog-model/src/entity/ref.ts b/packages/catalog-model/src/entity/ref.ts index 66974f5a01..f9bfcf38cd 100644 --- a/packages/catalog-model/src/entity/ref.ts +++ b/packages/catalog-model/src/entity/ref.ts @@ -53,19 +53,6 @@ export function getEntityName(entity: Entity): EntityName { }; } -/** - * The context of defaults that entity reference parsing happens within. - * - * @public - * @deprecated type inlined, will be removed in a future release. - */ -export type EntityRefContext = { - /** The default kind, if none is given in the reference */ - defaultKind?: string; - /** The default namespace, if none is given in the reference */ - defaultNamespace?: string; -}; - /** * Parses an entity reference, either on string or compound form, and always * returns a complete entity name including kind, namespace and name. @@ -203,67 +190,3 @@ export function stringifyEntityRef( 'en-US', )}/${name.toLocaleLowerCase('en-US')}`; } - -/** - * Compares an entity to either a string reference or a compound reference. - * - * @remarks - * - * The comparison is case insensitive, and all of kind, namespace, and name - * must match (after applying the optional context to the ref). - * - * @public - * @param entity - The entity to match - * @param ref - A string or compound entity ref - * @param context - An optional context of default kind and namespace, that apply - * to the ref if given - * @returns True if matching, false otherwise - * @deprecated compare using stringifyEntityRef instead. - */ -export function compareEntityToRef( - entity: Entity, - ref: - | string - | { kind?: string; namespace?: string; name: string } - | EntityName, - context?: { - /** The default kind, if none is given in the reference */ - defaultKind?: string; - /** The default namespace, if none is given in the reference */ - defaultNamespace?: string; - }, -): boolean { - const entityKind = entity.kind; - const entityNamespace = entity.metadata.namespace || DEFAULT_NAMESPACE; - const entityName = entity.metadata.name; - - let refKind: string | undefined; - let refNamespace: string | undefined; - let refName: string; - if (typeof ref === 'string') { - const parsed = parseRefString(ref); - refKind = parsed.kind || context?.defaultKind; - refNamespace = - parsed.namespace || context?.defaultNamespace || DEFAULT_NAMESPACE; - refName = parsed.name; - } else { - refKind = ref.kind || context?.defaultKind; - refNamespace = - ref.namespace || context?.defaultNamespace || DEFAULT_NAMESPACE; - refName = ref.name; - } - - if (!refKind || !refNamespace) { - throw new Error( - `Entity reference or context did not contain kind and namespace`, - ); - } - - return ( - entityKind.toLocaleLowerCase('en-US') === - refKind.toLocaleLowerCase('en-US') && - entityNamespace.toLocaleLowerCase('en-US') === - refNamespace.toLocaleLowerCase('en-US') && - entityName.toLocaleLowerCase('en-US') === refName.toLocaleLowerCase('en-US') - ); -} diff --git a/packages/catalog-model/src/entity/util.test.ts b/packages/catalog-model/src/entity/util.test.ts deleted file mode 100644 index 1c39961c0f..0000000000 --- a/packages/catalog-model/src/entity/util.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 lodash from 'lodash'; -import { - generateEntityEtag, - generateEntityUid, - entityHasChanges, - generateUpdatedEntity, -} from './util'; -import { Entity } from './Entity'; - -describe('util', () => { - describe('generateEntityUid', () => { - it('generates randomness', () => { - expect(generateEntityUid()).not.toEqual(''); - expect(generateEntityUid()).not.toEqual(generateEntityUid()); - }); - }); - - describe('generateEntityEtag', () => { - it('generates randomness', () => { - expect(generateEntityEtag()).not.toEqual(''); - expect(generateEntityEtag()).not.toEqual(generateEntityEtag()); - }); - }); - - describe('entityHasChanges', () => { - let a: Entity; - beforeEach(() => { - a = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'name', - custom: 'custom', - labels: { - labelKey: 'labelValue', - }, - annotations: { - annotationKey: 'annotationValue', - }, - }, - spec: { - a: 'a', - }, - }; - }); - - it('happy path: clone has no changes', () => { - const b = lodash.cloneDeep(a); - expect(entityHasChanges(a, b)).toBe(false); - }); - - it('detects root field changes', () => { - let b: any = lodash.cloneDeep(a); - b.apiVersion += 'a'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - delete b.apiVersion; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - b.kind += 'a'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - delete b.kind; - expect(entityHasChanges(a, b)).toBe(true); - }); - - it('detects metadata changes', () => { - let b: any = lodash.cloneDeep(a); - b.metadata.name += 'a'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - delete b.metadata.custom; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - delete b.metadata.custom; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - b.metadata.labels.n = 'n'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - b.metadata.labels.labelKey += 'a'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - b.metadata.annotations.annotationKey += 'a'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - delete b.metadata.annotations.annotationKey; - expect(entityHasChanges(a, b)).toBe(true); - }); - - it('detects spec changes', () => { - let b: any = lodash.cloneDeep(a); - b.spec.a += 'a'; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - delete b.spec.a; - expect(entityHasChanges(a, b)).toBe(true); - b = lodash.cloneDeep(a); - b.spec.n = 'n'; - expect(entityHasChanges(a, b)).toBe(true); - }); - }); - - describe('generateUpdatedEntity', () => { - let a: Entity; - let b: any; - beforeEach(() => { - a = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - uid: 'da921f56-f655-4e6e-9b8b-bb19a57818d8', - etag: 'NzY5NDA5NzQtYmEwNC00MDY0LWFiYmItNTYxYzQxM2JhZDcx', - generation: 2, - name: 'name', - custom: 'custom', - labels: { - labelKey: 'labelValue', - }, - annotations: { - annotationKey: 'annotationValue', - }, - }, - spec: { - a: 'a', - }, - }; - b = lodash.cloneDeep(a); - delete b.metadata.uid; - delete b.metadata.etag; - delete b.metadata.generation; - }); - - it('happy path: running on itself leaves it unchanged', () => { - const result = generateUpdatedEntity(a, b); - expect(result).toEqual(a); - }); - - it('bumps etag and generation when spec is changed', () => { - b.spec.a += 'a'; - const result = generateUpdatedEntity(a, b); - expect(result.metadata.uid).toEqual(a.metadata.uid); - expect(result.metadata.etag).not.toEqual(a.metadata.etag); - expect(result.metadata.generation).toEqual(a.metadata.generation! + 1); - expect(result.spec).toEqual({ a: 'aa' }); - }); - - it('bumps only etag when other things than spec are changed', () => { - b.metadata.n = 'n'; - const result = generateUpdatedEntity(a, b); - expect(result.metadata.uid).toEqual(a.metadata.uid); - expect(result.metadata.etag).not.toEqual(a.metadata.etag); - expect(result.metadata.generation).toEqual(a.metadata.generation); - expect(result.metadata.n).toEqual('n'); - }); - - it('retains new annotations', () => { - b.metadata.annotations.annotationKey = 'changedValue'; - b.metadata.annotations.newKey = 'newValue'; - const result = generateUpdatedEntity(a, b); - expect(result.metadata.uid).toEqual(a.metadata.uid); - expect(result.metadata.etag).not.toEqual(a.metadata.etag); - expect(result.metadata.generation).toEqual(a.metadata.generation); - expect(result.metadata.annotations).toEqual({ - annotationKey: 'changedValue', - newKey: 'newValue', - }); - }); - - it('retains old annotations', () => { - b.metadata.annotations.newKey = 'newValue'; - const result = generateUpdatedEntity(a, b); - expect(result.metadata.uid).toEqual(a.metadata.uid); - expect(result.metadata.etag).not.toEqual(a.metadata.etag); - expect(result.metadata.generation).toEqual(a.metadata.generation); - expect(result.metadata.annotations).toEqual({ - annotationKey: 'annotationValue', - newKey: 'newValue', - }); - }); - }); -}); diff --git a/packages/catalog-model/src/entity/util.ts b/packages/catalog-model/src/entity/util.ts deleted file mode 100644 index 78359b45f4..0000000000 --- a/packages/catalog-model/src/entity/util.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 lodash from 'lodash'; -import { v4 as uuidv4 } from 'uuid'; -import { Entity, AlphaEntity } from './Entity'; - -/** - * Generates a new random UID for an entity. - * - * @public - * @returns A string with enough randomness to uniquely identify an entity - * @deprecated use `uuidv4()` instead. - */ -export function generateEntityUid(): string { - return uuidv4(); -} - -/** - * Generates a new random Etag for an entity. - * - * @public - * @returns A string with enough randomness to uniquely identify an entity - * revision - * @deprecated will be removed in a future release. - */ -export function generateEntityEtag(): string { - return Buffer.from(uuidv4(), 'utf8').toString('base64').replace(/[^\w]/g, ''); -} - -/** - * Checks whether there are any significant changes going from the previous to - * the next version of this entity. - * - * @remarks - * - * Significance, in this case, means that we do not compare generated fields - * such as uid, etag and generation. - * - * Note that this comparison does NOT take status, relations or similar into - * account. It only compares the actual input entity data, i.e. metadata and - * spec. - * - * @public - * @param previous - The old state of the entity - * @param next - The new state of the entity - * @deprecated will be removed in a future release. - */ -export function entityHasChanges(previous: Entity, next: Entity): boolean { - const e1 = lodash.cloneDeep(previous); - const e2 = lodash.cloneDeep(next); - - if (!e1.metadata.labels) { - e1.metadata.labels = {}; - } - if (!e2.metadata.labels) { - e2.metadata.labels = {}; - } - if (!e1.metadata.annotations) { - e1.metadata.annotations = {}; - } - if (!e2.metadata.annotations) { - e2.metadata.annotations = {}; - } - if (!e1.metadata.tags) { - e1.metadata.tags = []; - } - if (!e2.metadata.tags) { - e2.metadata.tags = []; - } - - // Remove generated fields - delete e1.metadata.uid; - delete e1.metadata.etag; - delete e1.metadata.generation; - delete e2.metadata.uid; - delete e2.metadata.etag; - delete e2.metadata.generation; - - // Remove things that we explicitly do not compare - delete e1.relations; - delete (e1 as AlphaEntity).status; - delete e2.relations; - delete (e2 as AlphaEntity).status; - - return !lodash.isEqual(e1, e2); -} - -/** - * Takes an old revision of an entity and a new desired state, and merges - * them into a complete new state. - * - * @remarks - * - * The previous revision is expected to be a complete model loaded from the - * catalog, including the uid, etag and generation fields. - * - * @public - * @param previous - The old state of the entity - * @param next - The new state of the entity - * @returns An entity with the merged state of both - * @deprecated will be removed in a future release. - */ -export function generateUpdatedEntity(previous: Entity, next: Entity): Entity { - const { uid, etag, generation } = previous.metadata; - if (!uid || !etag || !generation) { - throw new Error('Previous entity must have uid, etag and generation'); - } - - const result = lodash.cloneDeep(next); - - // Generated fields are copied and updated - const bumpEtag = entityHasChanges(previous, result); - const bumpGeneration = !lodash.isEqual(previous.spec, result.spec); - result.metadata.uid = uid; - result.metadata.etag = bumpEtag ? generateEntityEtag() : etag; - result.metadata.generation = bumpGeneration ? generation + 1 : generation; - - return result; -} diff --git a/packages/catalog-model/src/index.ts b/packages/catalog-model/src/index.ts index ef55337ae2..986341bd33 100644 --- a/packages/catalog-model/src/index.ts +++ b/packages/catalog-model/src/index.ts @@ -24,5 +24,5 @@ export * from './entity'; export { EntityPolicies } from './EntityPolicies'; export * from './kinds'; export * from './location'; -export type { EntityName, EntityRef, JSONSchema } from './types'; +export type { EntityName, EntityRef } from './types'; export * from './validation'; diff --git a/packages/catalog-model/src/location/annotation.ts b/packages/catalog-model/src/location/annotation.ts index 3ff624360e..61dbc0b305 100644 --- a/packages/catalog-model/src/location/annotation.ts +++ b/packages/catalog-model/src/location/annotation.ts @@ -14,30 +14,6 @@ * limitations under the License. */ -/** - * Constant storing location annotation. - * - * @public - * @deprecated use {@link ANNOTATION_LOCATION} instead. - * */ -export const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; -/** - * Constant storing origin location annotation - * - * @public - * @deprecated use {@link ANNOTATION_ORIGIN_LOCATION} instead. - */ -export const ORIGIN_LOCATION_ANNOTATION = - 'backstage.io/managed-by-origin-location'; - -/** - * Contant storing source location annotation - * - * @public - * @deprecated use {@link ANNOTATION_SOURCE_LOCATION} instead. - * */ -export const SOURCE_LOCATION_ANNOTATION = 'backstage.io/source-location'; - /** * Constant storing location annotation. * diff --git a/packages/catalog-model/src/location/helpers.ts b/packages/catalog-model/src/location/helpers.ts index 7c73908157..8f8acc3b1d 100644 --- a/packages/catalog-model/src/location/helpers.ts +++ b/packages/catalog-model/src/location/helpers.ts @@ -18,18 +18,6 @@ import { ANNOTATION_SOURCE_LOCATION } from '.'; import { Entity, stringifyEntityRef } from '../entity'; import { ANNOTATION_LOCATION } from './annotation'; -/** - * Parses a string form location reference. - * - * @public - * @param ref - A string-form location reference, e.g. `'url:https://host'` - * @returns A location reference, e.g. `{ type: 'url', target: 'https://host' }` - * @deprecated use {@link parseLocationRef} instead - */ -export function parseLocationReference(ref: string) { - return parseLocationRef(ref); -} - /** * Parses a string form location reference. * @@ -72,21 +60,6 @@ export function parseLocationRef(ref: string): { return { type, target }; } -/** - * Turns a location reference into its string form. - * - * @public - * @param ref - A location reference, e.g. `{ type: 'url', target: 'https://host' }` - * @returns A string-form location reference, e.g. `'url:https://host'` - * @deprecated use {@link stringifyLocationRef} instead - */ -export function stringifyLocationReference(ref: { - type: string; - target: string; -}): string { - return stringifyLocationRef(ref); -} - /** * Turns a location ref into its string form. * diff --git a/packages/catalog-model/src/location/index.ts b/packages/catalog-model/src/location/index.ts index 8302ec202f..dc709144b2 100644 --- a/packages/catalog-model/src/location/index.ts +++ b/packages/catalog-model/src/location/index.ts @@ -18,15 +18,10 @@ export { ANNOTATION_LOCATION, ANNOTATION_ORIGIN_LOCATION, ANNOTATION_SOURCE_LOCATION, - LOCATION_ANNOTATION, - ORIGIN_LOCATION_ANNOTATION, - SOURCE_LOCATION_ANNOTATION, } from './annotation'; export { getEntitySourceLocation, parseLocationRef, - parseLocationReference, stringifyLocationRef, - stringifyLocationReference, } from './helpers'; -export type { Location, LocationSpec } from './types'; +export type { LocationSpec } from './types'; diff --git a/packages/catalog-model/src/location/types.ts b/packages/catalog-model/src/location/types.ts index 9b87e4919d..9e2c9339e6 100644 --- a/packages/catalog-model/src/location/types.ts +++ b/packages/catalog-model/src/location/types.ts @@ -31,13 +31,3 @@ export type LocationSpec = { target: string; presence?: 'optional' | 'required'; }; - -/** - * Entity location for a specific entity. - * - * @public - * @deprecated import from {@link @backstage/catalog-client#Location} instead. - */ -export type Location = { - id: string; -} & LocationSpec; diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts index 13550968be..5b5f063ca3 100644 --- a/packages/catalog-model/src/types.ts +++ b/packages/catalog-model/src/types.ts @@ -14,17 +14,6 @@ * limitations under the License. */ -import { JsonValue } from '@backstage/types'; -import { JSONSchema7 } from 'json-schema'; - -/** - * JSONSchema extendable by arbitrary JSON attributes - * - * @public - * @deprecated use JSONSchema7 from the json-schema package instead. - */ -export type JSONSchema = JSONSchema7 & { [key in string]?: JsonValue }; - /** * A complete entity name, with the full kind-namespace-name triplet. * From c81f9d6b9e247e5435c37512d20e2b529a57b473 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 2 Mar 2022 09:22:28 +0100 Subject: [PATCH 090/353] update changeset Signed-off-by: Johan Haals Co-authored-by: Patrik Oldsberg --- .changeset/weak-news-reply.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/weak-news-reply.md b/.changeset/weak-news-reply.md index 3becabc2e6..4a7a38cef5 100644 --- a/.changeset/weak-news-reply.md +++ b/.changeset/weak-news-reply.md @@ -2,7 +2,7 @@ '@backstage/catalog-model': minor --- -**Breaking**: The following changes are all breaking changes. +**BREAKING**: The following changes are all breaking changes. Removed `EDIT_URL_ANNOTATION` and `VIEW_URL_ANNOTATION`, `LOCATION_ANNOTATION`, `ORIGIN_LOCATION_ANNOTATION`, `LOCATION_ANNOTATION`, `SOURCE_LOCATION_ANNOTATION`. All of these constants have been prefixed with ANNOTATION to be easier to find meaning `SOURCE_LOCATION_ANNOTATION` is available as `ANNOTATION_SOURCE_LOCATION`. From 078319592a7152b60167f6e2d4495ec85077b301 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 2 Mar 2022 10:06:12 +0100 Subject: [PATCH 091/353] remove roadie resolutions for plugin-catalog & plugin-model Signed-off-by: Johan Haals --- package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/package.json b/package.json index e7cb42c1c6..1882d400b4 100644 --- a/package.json +++ b/package.json @@ -45,9 +45,7 @@ ] }, "resolutions": { - "**/@graphql-codegen/cli/**/ws": "^7.4.6", - "**/@roadiehq/**/@backstage/plugin-catalog": "*", - "**/@roadiehq/**/@backstage/catalog-model": "*" + "**/@graphql-codegen/cli/**/ws": "^7.4.6" }, "version": "0.69.0", "dependencies": { From 06cd854a0583959c746527d5bdb72fbf27aede63 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 2 Mar 2022 10:20:31 +0100 Subject: [PATCH 092/353] update yarn.lock Signed-off-by: Johan Haals --- yarn.lock | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9eef93080e..90dd0207c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1359,7 +1359,9 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.10.0": - version "0.11.0" + version "0.10.1" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.10.1.tgz#dcc3415eb4d4ee3d437355c477e85c7479626b3b" + integrity sha512-c004aQeO9cxtSZZc2iBcE6eoqurQLdj7YUm8mHWs8hEaPTA2UPVHawt+wlt89VywkI89X0wF7BuXV2LKVUfXvw== dependencies: "@backstage/config" "^0.1.15" "@backstage/errors" "^0.2.2" @@ -1371,16 +1373,20 @@ uuid "^8.0.0" "@backstage/catalog-model@^0.9.7": - version "0.11.0" + version "0.9.10" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.9.10.tgz#bd5662e1ad7bd7c9604f3f45d055c99b5b2bb87f" + integrity sha512-KhCjbZKhS5zZhHiGHmBMq6hDGDshMSZOPGXehtdhr6/oW7Ee5fDcOnhMqreCi1Ebm4RIWJhZcRrxO6X1TTi4TQ== dependencies: - "@backstage/config" "^0.1.15" - "@backstage/errors" "^0.2.2" - "@backstage/types" "^0.1.3" + "@backstage/config" "^0.1.13" + "@backstage/errors" "^0.2.0" + "@backstage/types" "^0.1.1" "@types/json-schema" "^7.0.5" + "@types/yup" "^0.29.13" ajv "^7.0.3" json-schema "^0.4.0" lodash "^4.17.21" uuid "^8.0.0" + yup "^0.32.9" "@backstage/core-plugin-api@^0.6.0", "@backstage/core-plugin-api@^0.6.1": version "0.6.1" From 2de1d82bd1b8f4f23e0117a7530cba9da10c18eb Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 2 Mar 2022 10:23:30 +0100 Subject: [PATCH 093/353] chore: added changeset Signed-off-by: blam --- .changeset/strong-suns-hope.md | 5 +++++ plugins/catalog-react/api-report.md | 2 +- .../catalog-react/src/hooks/useEntityOwnership.ts | 13 +++++-------- 3 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 .changeset/strong-suns-hope.md diff --git a/.changeset/strong-suns-hope.md b/.changeset/strong-suns-hope.md new file mode 100644 index 0000000000..ca59e43deb --- /dev/null +++ b/.changeset/strong-suns-hope.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Removing the `EntityName` path for the `useEntityOwnership` as it has never worked correctly. Please pass in an entire `Entity` instead. diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 2d2b56b634..c5d102cc29 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -544,7 +544,7 @@ export function useEntityListProvider< // @public export function useEntityOwnership(): { loading: boolean; - isOwnedEntity: (entity: Entity | EntityName) => boolean; + isOwnedEntity: (entity: Entity) => boolean; }; // @alpha diff --git a/plugins/catalog-react/src/hooks/useEntityOwnership.ts b/plugins/catalog-react/src/hooks/useEntityOwnership.ts index 77f6c4bf24..de3eca9dab 100644 --- a/plugins/catalog-react/src/hooks/useEntityOwnership.ts +++ b/plugins/catalog-react/src/hooks/useEntityOwnership.ts @@ -17,7 +17,6 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Entity, - EntityName, parseEntityRef, RELATION_MEMBER_OF, RELATION_OWNED_BY, @@ -77,7 +76,7 @@ export async function loadCatalogOwnerRefs( */ export function useEntityOwnership(): { loading: boolean; - isOwnedEntity: (entity: Entity | EntityName) => boolean; + isOwnedEntity: (entity: Entity) => boolean; } { const identityApi = useApi(identityApiRef); const catalogApi = useApi(catalogApiRef); @@ -94,12 +93,10 @@ export function useEntityOwnership(): { const isOwnedEntity = useMemo(() => { const myOwnerRefs = new Set(refs ?? []); - return (entity: Entity | EntityName) => { - const entityOwnerRefs = ( - 'metadata' in entity - ? getEntityRelations(entity, RELATION_OWNED_BY) - : [entity] - ).map(stringifyEntityRef); + return (entity: Entity) => { + const entityOwnerRefs = getEntityRelations(entity, RELATION_OWNED_BY).map( + stringifyEntityRef, + ); for (const ref of entityOwnerRefs) { if (myOwnerRefs.has(ref)) { return true; From fd061e47ee35fe54000d7e6b31d5fa47f76ec034 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 2 Mar 2022 10:38:10 +0100 Subject: [PATCH 094/353] chore: code review comments Signed-off-by: blam --- .changeset/tall-pillows-smash.md | 6 ------ plugins/catalog-react/src/hooks/useEntity.tsx | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 .changeset/tall-pillows-smash.md diff --git a/.changeset/tall-pillows-smash.md b/.changeset/tall-pillows-smash.md deleted file mode 100644 index 3c1d18cbe3..0000000000 --- a/.changeset/tall-pillows-smash.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-allure': patch -'@backstage/plugin-todo': patch ---- - -Fixing broken tests for the packages with the new `useEntity` change diff --git a/plugins/catalog-react/src/hooks/useEntity.tsx b/plugins/catalog-react/src/hooks/useEntity.tsx index f70adef708..da3fb8f3a5 100644 --- a/plugins/catalog-react/src/hooks/useEntity.tsx +++ b/plugins/catalog-react/src/hooks/useEntity.tsx @@ -178,7 +178,7 @@ export function useEntity(): { */ export function useAsyncEntity< TEntity extends Entity = Entity, ->(): EntityLoadingStatus { +>(): EntityLoadingStatus { const versionedHolder = useVersionedContext<{ 1: EntityLoadingStatus }>('entity-context'); From ec430528591bf7643c6e417da38ed064331575e1 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Wed, 2 Mar 2022 10:23:16 +0000 Subject: [PATCH 095/353] Remove an unneeded dependency Signed-off-by: Karan Shah --- .../components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx index 6ec820efa6..327da881a3 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx @@ -75,7 +75,7 @@ export const EntityAirbrakeWidget = ({ entity }: { entity: Entity }) => { } throw e; } - }, [componentState, airbrakeApi, projectId]); + }, [airbrakeApi, projectId]); useEffect(() => { if ( From baca071d05bf087a2527b6651bcdc24a5f91fb40 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Wed, 2 Mar 2022 11:26:50 +0100 Subject: [PATCH 096/353] Updating changeset Minor change but no deprecation period as it doesn't work Signed-off-by: Ben Lambert --- .changeset/strong-suns-hope.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/strong-suns-hope.md b/.changeset/strong-suns-hope.md index ca59e43deb..876f5f1618 100644 --- a/.changeset/strong-suns-hope.md +++ b/.changeset/strong-suns-hope.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-catalog-react': patch +'@backstage/plugin-catalog-react': minor --- Removing the `EntityName` path for the `useEntityOwnership` as it has never worked correctly. Please pass in an entire `Entity` instead. From c5a462bff14c41e84f782538d067c490700aca4d Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Wed, 2 Mar 2022 10:30:13 +0000 Subject: [PATCH 097/353] Add a changeset Signed-off-by: Karan Shah --- .changeset/giant-bottles-eat.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/giant-bottles-eat.md diff --git a/.changeset/giant-bottles-eat.md b/.changeset/giant-bottles-eat.md new file mode 100644 index 0000000000..d37343bfe4 --- /dev/null +++ b/.changeset/giant-bottles-eat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-airbrake': patch +--- + +Fix a bug where API calls were being made and errors were being added to the snack bar when no project ID was present. This is a common use case for components that haven't added the Airbrake plugin annotation to their `catalog-info.yaml`. From 6ed51204ff27d95cdbbac9862ad5a959aa2a1207 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 2 Mar 2022 11:32:45 +0100 Subject: [PATCH 098/353] chore: woops - api-report needed update Signed-off-by: blam --- plugins/catalog-react/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 1a94af6052..b660ceadb5 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -526,7 +526,7 @@ export type UnregisterEntityDialogProps = { // @public export function useAsyncEntity< TEntity extends Entity = Entity, ->(): EntityLoadingStatus; +>(): EntityLoadingStatus; // @public export function useEntity(): { From b838717e9235e6d02c47f7b98e660de0c4cf51f8 Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Wed, 2 Mar 2022 12:00:44 +0000 Subject: [PATCH 099/353] exort FetchUrlReader Signed-off-by: Brian Fletcher --- .changeset/olive-glasses-approve.md | 5 +++++ packages/backend-common/src/reading/index.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/olive-glasses-approve.md diff --git a/.changeset/olive-glasses-approve.md b/.changeset/olive-glasses-approve.md new file mode 100644 index 0000000000..e74b7ef485 --- /dev/null +++ b/.changeset/olive-glasses-approve.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Export FetchUrlReader to facilitate more flexible configuration of the backend. diff --git a/packages/backend-common/src/reading/index.ts b/packages/backend-common/src/reading/index.ts index 2c3394e4fc..9c29bc93fe 100644 --- a/packages/backend-common/src/reading/index.ts +++ b/packages/backend-common/src/reading/index.ts @@ -19,6 +19,7 @@ export { BitbucketUrlReader } from './BitbucketUrlReader'; export { GithubUrlReader } from './GithubUrlReader'; export { GitlabUrlReader } from './GitlabUrlReader'; export { AwsS3UrlReader } from './AwsS3UrlReader'; +export { FetchUrlReader } from './FetchUrlReader'; export type { FromReadableArrayOptions, ReaderFactory, From fb01d26fd71802671926d354227f86b305b2fe5a Mon Sep 17 00:00:00 2001 From: Brian Fletcher Date: Wed, 2 Mar 2022 12:27:49 +0000 Subject: [PATCH 100/353] adds api reports Signed-off-by: Brian Fletcher --- packages/backend-common/api-report.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 9b963f3fc9..4ed9bb1514 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -234,6 +234,21 @@ export type ErrorHandlerOptions = { logClientErrors?: boolean; }; +// @public +export class FetchUrlReader implements UrlReader { + static factory: ReaderFactory; + // (undocumented) + read(url: string): Promise; + // (undocumented) + readTree(): Promise; + // (undocumented) + readUrl(url: string, options?: ReadUrlOptions): Promise; + // (undocumented) + search(): Promise; + // (undocumented) + toString(): string; +} + // @public export type FromReadableArrayOptions = Array<{ data: Readable; From 950003f5f3cd3764295ead09bc713a768f4b999a Mon Sep 17 00:00:00 2001 From: Hasan Ozdemir <21654050+nodify-at@users.noreply.github.com> Date: Wed, 2 Mar 2022 13:07:41 +0100 Subject: [PATCH 101/353] Update plugins/jenkins-backend/src/service/jenkinsApi.test.ts Co-authored-by: MT Lewis Signed-off-by: Hasan Oezdemir <21654050+nodify-at@users.noreply.github.com> --- plugins/jenkins-backend/src/service/jenkinsApi.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts index 350ec996ca..45f7424e40 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts @@ -435,7 +435,7 @@ describe('JenkinsApi', () => { ).rejects.toThrow(NotAllowedError); }); - it('buildProject should success if it have required permissions', async () => { + it('buildProject should succeed if it have required permissions', async () => { fakePermissionApi.authorize.mockResolvedValueOnce([ { result: AuthorizeResult.ALLOW, From efa549c4abcc8703184718acfa1c8019504de473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 2 Mar 2022 13:47:03 +0100 Subject: [PATCH 102/353] form refs properly in resolver docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- docs/auth/identity-resolver.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index a33351e938..3b355470d1 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -27,6 +27,8 @@ sign-in resolvers and set them for any of the Authentication providers inside `@backstage/plugin-auth-backend` plugin. ```ts +import { DEFAULT_NAMESPACE, stringifyEntityRef } from '@backstage/catalog-model'; + export default async function createPlugin({ ... }: PluginEnvironment): Promise { @@ -38,22 +40,31 @@ export default async function createPlugin({ resolver: async ({ profile: { email } }, ctx) => { // Call a custom validator function that checks that the email is // valid and on our own company's domain, and throws an Error if it - // isn't + // isn't. + // TODO: Implement this function validateEmail(email); // List of entity references that denote the identity and // membership of the user - const ent = []; + const ent: string[] = []; // Let's use the username in the email ID as the user's default // unique identifier inside Backstage. const [id] = email.split('@'); - ent.push(`User:default/${id}`) + ent.push(stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: id, + })); // Let's call the internal LDAP provider to get a list of groups // that the user belongs to, and add those to the list as well const ldapGroups = await getLdapGroups(email); - ldapGroups.forEach(group => ent.push(`Group:default/${group}`)) + ldapGroups.forEach(group => ent.push(stringifyEntityRef({ + kind: 'Group', + namespace: DEFAULT_NAMESPACE, + name: group, + }))); // Issue the token containing the entity claims const token = await ctx.tokenIssuer.issueToken({ From f3cce3dcf79b2597a5b77086b46ad3529324889b Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 2 Mar 2022 13:50:18 +0100 Subject: [PATCH 103/353] core-app-api: Remove GithubSession and SamlSession types Signed-off-by: Johan Haals --- .changeset/olive-lobsters-rescue.md | 5 ++ packages/core-app-api/api-report.md | 18 ------ .../apis/implementations/auth/github/index.ts | 1 - .../apis/implementations/auth/github/types.ts | 62 ------------------- .../apis/implementations/auth/saml/index.ts | 2 +- .../apis/implementations/auth/saml/types.ts | 12 ---- 6 files changed, 6 insertions(+), 94 deletions(-) create mode 100644 .changeset/olive-lobsters-rescue.md delete mode 100644 packages/core-app-api/src/apis/implementations/auth/github/types.ts diff --git a/.changeset/olive-lobsters-rescue.md b/.changeset/olive-lobsters-rescue.md new file mode 100644 index 0000000000..37a7712782 --- /dev/null +++ b/.changeset/olive-lobsters-rescue.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': minor +--- + +**BREAKING**: Removed export of `GithubSession` and `SamlSession` which are only used internally. diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 17a9592523..c52b953465 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -380,17 +380,6 @@ export class GithubAuth { static normalizeScope(scope?: string): Set; } -// @public @deprecated -export type GithubSession = { - providerInfo: { - accessToken: string; - scopes: Set; - expiresAt?: Date; - }; - profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; -}; - // @public export class GitlabAuth { // (undocumented) @@ -532,13 +521,6 @@ export class SamlAuth signOut(): Promise; } -// @public @deprecated -export type SamlSession = { - userId: string; - profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; -}; - // @public export type SignInPageProps = { onSignInSuccess(identityApi: IdentityApi): void; diff --git a/packages/core-app-api/src/apis/implementations/auth/github/index.ts b/packages/core-app-api/src/apis/implementations/auth/github/index.ts index b5aa1a0a25..5e53bacfc1 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export type { GithubSession } from './types'; export { default as GithubAuth } from './GithubAuth'; diff --git a/packages/core-app-api/src/apis/implementations/auth/github/types.ts b/packages/core-app-api/src/apis/implementations/auth/github/types.ts deleted file mode 100644 index 0ef662905f..0000000000 --- a/packages/core-app-api/src/apis/implementations/auth/github/types.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { - ProfileInfo, - BackstageIdentityResponse, -} from '@backstage/core-plugin-api'; -import { z } from 'zod'; - -// TODO(Rugvip): Make GithubSession internal - -/** - * Session information for GitHub auth. - * - * @public - * @deprecated This type is internal and will be removed - */ -export type GithubSession = { - providerInfo: { - accessToken: string; - scopes: Set; - expiresAt?: Date; - }; - profile: ProfileInfo; - // TODO(Rugvip): This should be made optional once the type is no longer public - backstageIdentity: BackstageIdentityResponse; -}; - -export const githubSessionSchema: z.ZodSchema = z.object({ - providerInfo: z.object({ - accessToken: z.string(), - scopes: z.set(z.string()), - expiresAt: z.date().optional(), - }), - profile: z.object({ - email: z.string().optional(), - displayName: z.string().optional(), - picture: z.string().optional(), - }), - backstageIdentity: z.object({ - id: z.string(), - token: z.string(), - identity: z.object({ - type: z.literal('user'), - userEntityRef: z.string(), - ownershipEntityRefs: z.array(z.string()), - }), - }), -}); diff --git a/packages/core-app-api/src/apis/implementations/auth/saml/index.ts b/packages/core-app-api/src/apis/implementations/auth/saml/index.ts index 2e749a0648..1f5f8fbbf8 100644 --- a/packages/core-app-api/src/apis/implementations/auth/saml/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/saml/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ export { default as SamlAuth } from './SamlAuth'; -export type { ExportedSamlSession as SamlSession } from './types'; +// export type { ExportedSamlSession as SamlSession } from './types'; diff --git a/packages/core-app-api/src/apis/implementations/auth/saml/types.ts b/packages/core-app-api/src/apis/implementations/auth/saml/types.ts index f1345fd154..55b278b429 100644 --- a/packages/core-app-api/src/apis/implementations/auth/saml/types.ts +++ b/packages/core-app-api/src/apis/implementations/auth/saml/types.ts @@ -20,18 +20,6 @@ import { } from '@backstage/core-plugin-api'; import { z } from 'zod'; -/** - * Session information for SAML auth. - * - * @public - * @deprecated This type is internal and will be removed - */ -export type ExportedSamlSession = { - userId: string; - profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; -}; - /** @internal */ export type SamlSession = { profile: ProfileInfo; From 70fdad285ed589ea2a2d881608d0518e92315b41 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 2 Mar 2022 13:50:56 +0100 Subject: [PATCH 104/353] chore: remove comment Signed-off-by: Johan Haals --- .../core-app-api/src/apis/implementations/auth/saml/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core-app-api/src/apis/implementations/auth/saml/index.ts b/packages/core-app-api/src/apis/implementations/auth/saml/index.ts index 1f5f8fbbf8..930e6cb115 100644 --- a/packages/core-app-api/src/apis/implementations/auth/saml/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/saml/index.ts @@ -14,4 +14,3 @@ * limitations under the License. */ export { default as SamlAuth } from './SamlAuth'; -// export type { ExportedSamlSession as SamlSession } from './types'; From 3c1d3cb07e360caa1d697f3d6db5d3d33589d631 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Wed, 2 Mar 2022 13:41:02 +0000 Subject: [PATCH 105/353] Add a changeset Signed-off-by: Karan Shah --- .changeset/red-chefs-beam.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/red-chefs-beam.md diff --git a/.changeset/red-chefs-beam.md b/.changeset/red-chefs-beam.md new file mode 100644 index 0000000000..5cadccb0aa --- /dev/null +++ b/.changeset/red-chefs-beam.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-airbrake': patch +'@backstage/plugin-airbrake-backend': patch +--- + +The Airbrake plugin installation instructions have been updated to work better and conform to how the frontend and backend plugins are supposed to be integrated into a Backstage instance. From dbf84eee55e10cab80f658dca5f68f2ebf7bb54e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 2 Mar 2022 12:26:49 +0100 Subject: [PATCH 106/353] core-app-api: removed deprecated GithubAuth.normalizeScopes Signed-off-by: Patrik Oldsberg --- .changeset/tame-lions-know.md | 5 +++++ packages/core-app-api/api-report.md | 2 -- .../implementations/auth/github/GithubAuth.ts | 15 --------------- 3 files changed, 5 insertions(+), 17 deletions(-) create mode 100644 .changeset/tame-lions-know.md diff --git a/.changeset/tame-lions-know.md b/.changeset/tame-lions-know.md new file mode 100644 index 0000000000..7f304b22ef --- /dev/null +++ b/.changeset/tame-lions-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': minor +--- + +**BREAKING**: Removed the deprecated `GithubAuth.normalizeScopes` method. diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index c52b953465..116b33c598 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -376,8 +376,6 @@ export type FlatRoutesProps = { export class GithubAuth { // (undocumented) static create(options: OAuthApiCreateOptions): typeof githubAuthApiRef.T; - // @deprecated (undocumented) - static normalizeScope(scope?: string): Set; } // @public diff --git a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts index b0af4c7ade..7efe4e95c6 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -47,19 +47,4 @@ export default class GithubAuth { defaultScopes, }); } - - /** - * @deprecated This method is deprecated and will be removed in a future release. - */ - static normalizeScope(scope?: string): Set { - if (!scope) { - return new Set(); - } - - const scopeList = Array.isArray(scope) - ? scope - : scope.split(/[\s|,]/).filter(Boolean); - - return new Set(scopeList); - } } From 5fba4c0304b1a4f27aa296820d2d38343a8af38e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 2 Mar 2022 12:27:15 +0100 Subject: [PATCH 107/353] config-loader: tweak a comment so that it does not show up in deprecation list Signed-off-by: Patrik Oldsberg --- packages/config-loader/src/lib/schema/collect.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/config-loader/src/lib/schema/collect.ts b/packages/config-loader/src/lib/schema/collect.ts index a05796139b..3ff161cec4 100644 --- a/packages/config-loader/src/lib/schema/collect.ts +++ b/packages/config-loader/src/lib/schema/collect.ts @@ -182,7 +182,7 @@ function compileTsSchemas(paths: string[]) { program, // All schemas should export a `Config` symbol 'Config', - // This enables usage of @visibility and @deprecated in doc comments + // This enables the use of these tags in TSDoc comments { required: true, validationKeywords: ['visibility', 'deprecated'], From 8c3f30cb28196312f4e2bd79f690e7243e392c13 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 2 Mar 2022 12:29:24 +0100 Subject: [PATCH 108/353] cli: remove deprecated index.html templating variables Signed-off-by: Patrik Oldsberg --- .changeset/orange-crews-explain.md | 5 ++++ packages/cli/src/lib/bundler/config.ts | 33 -------------------------- 2 files changed, 5 insertions(+), 33 deletions(-) create mode 100644 .changeset/orange-crews-explain.md diff --git a/.changeset/orange-crews-explain.md b/.changeset/orange-crews-explain.md new file mode 100644 index 0000000000..4b831392eb --- /dev/null +++ b/.changeset/orange-crews-explain.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': minor +--- + +**BREAKING**: Removed the deprecated `app.` template variables from the `index.html` templating. These should be replaced by using `config.getString("app.")` instead. diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index fe64f23209..bfd05807ad 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -15,7 +15,6 @@ */ import fs from 'fs-extra'; -import chalk from 'chalk'; import { resolve as resolvePath } from 'path'; import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; @@ -118,43 +117,11 @@ export async function createConfig( }), ); - const appParamDeprecationMsg = chalk.red( - 'DEPRECATION WARNING: using `app.` in the index.html template is deprecated, use `config.getString("app.")` instead.', - ); plugins.push( new HtmlWebpackPlugin({ template: paths.targetHtml, templateParameters: { publicPath: validBaseUrl.pathname.replace(/\/$/, ''), - app: { - get title() { - console.warn(appParamDeprecationMsg); - return frontendConfig.getString('app.title'); - }, - get baseUrl() { - console.warn(appParamDeprecationMsg); - return validBaseUrl.href; - }, - get googleAnalyticsTrackingId() { - console.warn(appParamDeprecationMsg); - return frontendConfig.getOptionalString( - 'app.googleAnalyticsTrackingId', - ); - }, - get datadogRum() { - console.warn(appParamDeprecationMsg); - return { - env: frontendConfig.getOptionalString('app.datadogRum.env'), - clientToken: frontendConfig.getOptionalString( - 'app.datadogRum.clientToken', - ), - applicationId: frontendConfig.getOptionalString( - 'app.datadogRum.applicationId', - ), - site: frontendConfig.getOptionalString('app.datadogRum.site'), - }; - }, - }, config: frontendConfig, }, }), From f590d1681b26ba77ca8f50c267107e0f4e1490df Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 2 Mar 2022 15:48:17 +0100 Subject: [PATCH 109/353] core-plugin-api: Deprecate `favoriteEntityTooltip` and `favoriteEntityIcon` Signed-off-by: Johan Haals --- .changeset/metal-months-tie.md | 5 +++++ .changeset/small-brooms-retire.md | 6 ++++++ plugins/catalog-react/api-report.md | 4 ++-- .../FavoriteEntity/FavoriteEntity.tsx | 16 ++++++++++++---- .../components/CatalogTable/CatalogTable.tsx | 15 +++++++++++---- .../src/home/components/Tables/actions.tsx | 17 +++++++++++------ 6 files changed, 47 insertions(+), 16 deletions(-) create mode 100644 .changeset/metal-months-tie.md create mode 100644 .changeset/small-brooms-retire.md diff --git a/.changeset/metal-months-tie.md b/.changeset/metal-months-tie.md new file mode 100644 index 0000000000..890c7e8b5d --- /dev/null +++ b/.changeset/metal-months-tie.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Deprecated `favoriteEntityTooltip` and `favoriteEntityIcon` since the utility value is very low. diff --git a/.changeset/small-brooms-retire.md b/.changeset/small-brooms-retire.md new file mode 100644 index 0000000000..2aa722a666 --- /dev/null +++ b/.changeset/small-brooms-retire.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog': patch +'@backstage/plugin-techdocs': patch +--- + +Removed usage of deprecated favorite utility methods. diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 78ad5d4b97..3f9a2c47e1 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -400,7 +400,7 @@ export type EntityTypeReturn = { // @public export const FavoriteEntity: (props: FavoriteEntityProps) => JSX.Element; -// @public (undocumented) +// @public @deprecated (undocumented) export const favoriteEntityIcon: (isStarred: boolean) => JSX.Element; // @public (undocumented) @@ -408,7 +408,7 @@ export type FavoriteEntityProps = ComponentProps & { entity: Entity; }; -// @public (undocumented) +// @public @deprecated (undocumented) export const favoriteEntityTooltip: ( isStarred: boolean, ) => 'Remove from favorites' | 'Add to favorites'; diff --git a/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx b/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx index 3832a82877..eaa2821448 100644 --- a/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx +++ b/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx @@ -32,11 +32,17 @@ const YellowStar = withStyles({ }, })(Star); -/** @public */ +/** + * @public + * @deprecated due to low utility value. + */ export const favoriteEntityTooltip = (isStarred: boolean) => isStarred ? 'Remove from favorites' : 'Add to favorites'; -/** @public */ +/** + * @public + * @deprecated due to low utility value. + */ export const favoriteEntityIcon = (isStarred: boolean) => isStarred ? : ; @@ -55,8 +61,10 @@ export const FavoriteEntity = (props: FavoriteEntityProps) => { {...props} onClick={() => toggleStarredEntity()} > - - {favoriteEntityIcon(isStarredEntity)} + + {isStarredEntity ? : } ); diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index a9a6f26228..06a2395ab6 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -20,8 +20,6 @@ import { RELATION_PART_OF, } from '@backstage/catalog-model'; import { - favoriteEntityIcon, - favoriteEntityTooltip, formatEntityRefTitle, getEntityRelations, useEntityList, @@ -40,6 +38,9 @@ import { TableProps, WarningPanel, } from '@backstage/core-components'; +import StarBorder from '@material-ui/icons/StarBorder'; +import { withStyles } from '@material-ui/core/styles'; +import Star from '@material-ui/icons/Star'; /** * Props for {@link CatalogTable}. @@ -51,6 +52,12 @@ export interface CatalogTableProps { actions?: TableProps['actions']; } +const YellowStar = withStyles({ + root: { + color: '#f3ba37', + }, +})(Star); + /** @public */ export const CatalogTable = (props: CatalogTableProps) => { const { columns, actions } = props; @@ -116,8 +123,8 @@ export const CatalogTable = (props: CatalogTableProps) => { const isStarred = isStarredEntity(entity); return { cellStyle: { paddingLeft: '1em' }, - icon: () => favoriteEntityIcon(isStarred), - tooltip: favoriteEntityTooltip(isStarred), + icon: () => (isStarred ? : ), + tooltip: isStarred ? 'Remove from favorites' : 'Add to favorites', onClick: () => toggleStarredEntity(entity), }; }, diff --git a/plugins/techdocs/src/home/components/Tables/actions.tsx b/plugins/techdocs/src/home/components/Tables/actions.tsx index d0dbb8bf47..3fd8881718 100644 --- a/plugins/techdocs/src/home/components/Tables/actions.tsx +++ b/plugins/techdocs/src/home/components/Tables/actions.tsx @@ -16,11 +16,16 @@ import React from 'react'; import ShareIcon from '@material-ui/icons/Share'; -import { - favoriteEntityIcon, - favoriteEntityTooltip, -} from '@backstage/plugin-catalog-react'; import { DocsTableRow } from './types'; +import { withStyles } from '@material-ui/styles'; +import Star from '@material-ui/icons/Star'; +import StarBorder from '@material-ui/icons/StarBorder'; + +const YellowStar = withStyles({ + root: { + color: '#f3ba37', + }, +})(Star); /** * Not directly exported, but through DocsTable.actions and EntityListDocsTable.actions @@ -46,8 +51,8 @@ export const actionFactories = { const isStarred = isStarredEntity(entity); return { cellStyle: { paddingLeft: '1em' }, - icon: () => favoriteEntityIcon(isStarred), - tooltip: favoriteEntityTooltip(isStarred), + icon: () => (isStarred ? : ), + tooltip: isStarred ? 'Remove from favorites' : 'Add to favorites', onClick: () => toggleStarredEntity(entity), }; }; From 9562b13913f36ce7e845e94bd69aa910bf39a549 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 2 Mar 2022 17:01:36 +0100 Subject: [PATCH 110/353] chore: removing FavouriteTemplate in favor of FavoriteEntity Signed-off-by: blam --- .../FavouriteTemplate/FavouriteTemplate.tsx | 74 ------------------- .../src/components/FavouriteTemplate/index.ts | 17 ----- .../components/TemplateCard/TemplateCard.tsx | 10 ++- plugins/scaffolder/src/components/index.ts | 1 - 4 files changed, 8 insertions(+), 94 deletions(-) delete mode 100644 plugins/scaffolder/src/components/FavouriteTemplate/FavouriteTemplate.tsx delete mode 100644 plugins/scaffolder/src/components/FavouriteTemplate/index.ts diff --git a/plugins/scaffolder/src/components/FavouriteTemplate/FavouriteTemplate.tsx b/plugins/scaffolder/src/components/FavouriteTemplate/FavouriteTemplate.tsx deleted file mode 100644 index 6e8fe45b2f..0000000000 --- a/plugins/scaffolder/src/components/FavouriteTemplate/FavouriteTemplate.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { useStarredEntity } from '@backstage/plugin-catalog-react'; -import { IconButton, makeStyles, Tooltip, withStyles } from '@material-ui/core'; -import Star from '@material-ui/icons/Star'; -import StarBorder from '@material-ui/icons/StarBorder'; -import React, { ComponentProps } from 'react'; - -type Props = ComponentProps & { entity: Entity }; - -const YellowStar = withStyles({ - root: { - color: '#f3ba37', - }, -})(Star); - -const WhiteBorderStar = withStyles({ - root: { - color: '#ffffff', - }, -})(StarBorder); - -const useStyles = makeStyles(theme => ({ - starButton: { - position: 'absolute', - top: theme.spacing(0.5), - right: theme.spacing(0.5), - padding: '0.25rem', - }, -})); - -export const favouriteTemplateTooltip = (isStarred: boolean) => - isStarred ? 'Remove from favorites' : 'Add to favorites'; - -export const favouriteTemplateIcon = (isStarred: boolean) => - isStarred ? : ; - -/** - * IconButton for showing if a current entity is starred and adding/removing it from the favourite entities - * @param props - MaterialUI IconButton props extended by required `entity` prop - */ -export const FavouriteTemplate = (props: Props) => { - const classes = useStyles(); - const { toggleStarredEntity, isStarredEntity } = useStarredEntity( - props.entity, - ); - return ( - toggleStarredEntity()} - > - - {favouriteTemplateIcon(isStarredEntity)} - - - ); -}; diff --git a/plugins/scaffolder/src/components/FavouriteTemplate/index.ts b/plugins/scaffolder/src/components/FavouriteTemplate/index.ts deleted file mode 100644 index 5955e88c01..0000000000 --- a/plugins/scaffolder/src/components/FavouriteTemplate/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 './FavouriteTemplate'; diff --git a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx index f4710427c2..3a7691dd5a 100644 --- a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx @@ -21,6 +21,7 @@ import { } from '@backstage/integration-react'; import { EntityRefLinks, + FavoriteEntity, getEntityRelations, getEntitySourceLocation, } from '@backstage/plugin-catalog-react'; @@ -42,7 +43,6 @@ import { import WarningIcon from '@material-ui/icons/Warning'; import React from 'react'; import { selectedTemplateRouteRef } from '../../routes'; -import { FavouriteTemplate } from '../FavouriteTemplate/FavouriteTemplate'; import { Button, ItemCardHeader } from '@backstage/core-components'; import { useApi, useRouteRef } from '@backstage/core-plugin-api'; @@ -74,6 +74,12 @@ const useStyles = makeStyles(theme => ({ leftButton: { marginRight: 'auto', }, + starButton: { + position: 'absolute', + top: theme.spacing(0.5), + right: theme.spacing(0.5), + padding: '0.25rem', + }, })); const useDeprecationStyles = makeStyles(theme => ({ @@ -159,7 +165,7 @@ export const TemplateCard = ({ template, deprecated }: TemplateCardProps) => { return ( - + {deprecated && } Date: Wed, 2 Mar 2022 17:03:25 +0100 Subject: [PATCH 111/353] chore: update api-report Signed-off-by: blam --- plugins/scaffolder/api-report.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index a385e13534..b51f18fcc9 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -8,7 +8,6 @@ import { ApiHolder } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; -import { ComponentProps } from 'react'; import { ComponentType } from 'react'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; @@ -17,7 +16,6 @@ import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; import { FieldProps } from '@rjsf/core'; import { FieldValidation } from '@rjsf/core'; -import { IconButton } from '@material-ui/core'; import { JsonObject } from '@backstage/types'; import { JSONSchema7 } from 'json-schema'; import { JsonValue } from '@backstage/types'; @@ -87,12 +85,6 @@ export interface EntityTagsPickerUiOptions { kinds?: string[]; } -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "FavouriteTemplate" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export const FavouriteTemplate: (props: Props) => JSX.Element; - // @public export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null; From 1c2755991dc56abf8a52b1153e3e2c7d6c067006 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 2 Mar 2022 17:05:26 +0100 Subject: [PATCH 112/353] chore: added changeset Signed-off-by: blam --- .changeset/large-dancers-learn.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/large-dancers-learn.md diff --git a/.changeset/large-dancers-learn.md b/.changeset/large-dancers-learn.md new file mode 100644 index 0000000000..43f9d27b11 --- /dev/null +++ b/.changeset/large-dancers-learn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +- **BREAKING**: Removed the `FavouriteEntity` export in favor of the `FavoriteEntity` from `@backstage/plguin-catalog-react`. Please migrate any usages to that component instead if you are creating your own `TemplateCard` page. From c8222292bfec538787f8c6c488103d0a414bce75 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 2 Mar 2022 17:08:25 +0100 Subject: [PATCH 113/353] chore: renamed formatEntityRefTitle to humanizeEnityRef Signed-off-by: blam --- .../CatalogGraphCard/CatalogGraphCard.tsx | 4 ++-- .../CatalogGraphPage/CatalogGraphPage.tsx | 8 ++++---- .../components/EntityRelationsGraph/CustomNode.tsx | 4 ++-- .../EntityListComponent/EntityListComponent.tsx | 8 ++++---- .../StepPrepareCreatePullRequest.tsx | 4 ++-- .../EntityOwnerPicker/EntityOwnerPicker.tsx | 4 ++-- .../src/components/EntityRefLink/EntityRefLink.tsx | 4 ++-- .../{format.test.ts => humanize.test.ts} | 14 +++++++------- .../EntityRefLink/{format.ts => humanize.ts} | 2 +- .../src/components/EntityRefLink/index.ts | 2 +- .../src/components/EntityTable/columns.tsx | 6 +++--- .../components/AncestryPage.tsx | 4 ++-- plugins/catalog-react/src/filters.ts | 4 ++-- .../src/components/CatalogTable/CatalogTable.tsx | 8 ++++---- .../src/components/CatalogTable/columns.tsx | 4 ++-- .../GroupsExplorerContent/GroupsDiagram.tsx | 4 ++-- .../fossa/src/components/FossaPage/FossaPage.tsx | 6 +++--- .../Cards/OwnershipCard/OwnershipCard.tsx | 4 ++-- .../fields/EntityPicker/EntityPicker.tsx | 4 ++-- .../fields/OwnedEntityPicker/OwnedEntityPicker.tsx | 4 ++-- .../src/home/components/Tables/DocsTable.tsx | 4 ++-- 21 files changed, 53 insertions(+), 53 deletions(-) rename plugins/catalog-react/src/components/EntityRefLink/{format.test.ts => humanize.test.ts} (86%) rename plugins/catalog-react/src/components/EntityRefLink/{format.ts => humanize.ts} (97%) diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx index b2dfd9f9b2..e6811aad62 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx @@ -21,7 +21,7 @@ import { import { InfoCard, InfoCardVariants } from '@backstage/core-components'; import { useAnalytics, useRouteRef } from '@backstage/core-plugin-api'; import { - formatEntityRefTitle, + humanizeEntityRef, useEntity, entityRouteRef, } from '@backstage/plugin-catalog-react'; @@ -94,7 +94,7 @@ export const CatalogGraphCard = ({ }); analytics.captureEvent( 'click', - node.title ?? formatEntityRefTitle(nodeEntityName), + node.title ?? humanizeEntityRef(nodeEntityName), { attributes: { to: path } }, ); navigate(path); diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx index 2ae7a99dd8..15d0cd139b 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx @@ -24,7 +24,7 @@ import { import { useAnalytics, useRouteRef } from '@backstage/core-plugin-api'; import { entityRouteRef, - formatEntityRefTitle, + humanizeEntityRef, } from '@backstage/plugin-catalog-react'; import { Grid, makeStyles, Paper, Typography } from '@material-ui/core'; import FilterListIcon from '@material-ui/icons/FilterList'; @@ -149,14 +149,14 @@ export const CatalogGraphPage = ({ analytics.captureEvent( 'click', - node.title ?? formatEntityRefTitle(nodeEntityName), + node.title ?? humanizeEntityRef(nodeEntityName), { attributes: { to: path } }, ); navigate(path); } else { analytics.captureEvent( 'click', - node.title ?? formatEntityRefTitle(nodeEntityName), + node.title ?? humanizeEntityRef(nodeEntityName), ); setRootEntityNames([nodeEntityName]); } @@ -168,7 +168,7 @@ export const CatalogGraphPage = ({

formatEntityRefTitle(e)).join(', ')} + subtitle={rootEntityNames.map(e => humanizeEntityRef(e)).join(', ')} /> ({ function sortEntities(entities: Array) { return entities.sort((a, b) => - formatEntityRefTitle(a).localeCompare(formatEntityRefTitle(b)), + humanizeEntityRef(a).localeCompare(humanizeEntityRef(b)), ); } @@ -130,7 +130,7 @@ export const EntityListComponent = (props: EntityListComponentProps) => { ) ?? WorkIcon; return ( { - + ); })} diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx index 36e8c33a5c..1cbdfce355 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx @@ -19,7 +19,7 @@ import { errorApiRef, useApi } from '@backstage/core-plugin-api'; import { assertError } from '@backstage/errors'; import { catalogApiRef, - formatEntityRefTitle, + humanizeEntityRef, } from '@backstage/plugin-catalog-react'; import { Box, FormHelperText, Grid, Typography } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; @@ -139,7 +139,7 @@ export const StepPrepareCreatePullRequest = ( }); return groupEntities.items - .map(e => formatEntityRefTitle(e, { defaultKind: 'group' })) + .map(e => humanizeEntityRef(e, { defaultKind: 'group' })) .sort(); }); diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index d025d08012..ec2c8126e5 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -31,7 +31,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useEntityList } from '../../hooks/useEntityListProvider'; import { EntityOwnerFilter } from '../../filters'; import { getEntityRelations } from '../../utils'; -import { formatEntityRefTitle } from '../EntityRefLink'; +import { humanizeEntityRef } from '../EntityRefLink'; /** @public */ export type CatalogReactEntityOwnerPickerClassKey = 'input'; @@ -86,7 +86,7 @@ export const EntityOwnerPicker = () => { backendEntities .flatMap((e: Entity) => getEntityRelations(e, RELATION_OWNED_BY).map(o => - formatEntityRefTitle(o, { defaultKind: 'group' }), + humanizeEntityRef(o, { defaultKind: 'group' }), ), ) .filter(Boolean) as string[], diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index 1e01402ddf..da2db24341 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -22,7 +22,7 @@ import { } from '@backstage/catalog-model'; import React, { forwardRef } from 'react'; import { entityRouteRef } from '../../routes'; -import { formatEntityRefTitle } from './format'; +import { humanizeEntityRef } from './format'; import { Link, LinkProps } from '@backstage/core-components'; import { useRouteRef } from '@backstage/core-plugin-api'; import { Tooltip } from '@material-ui/core'; @@ -72,7 +72,7 @@ export const EntityRefLink = forwardRef( namespace = namespace?.toLocaleLowerCase('en-US') ?? DEFAULT_NAMESPACE; const routeParams = { kind, namespace, name }; - const formattedEntityRefTitle = formatEntityRefTitle( + const formattedEntityRefTitle = humanizeEntityRef( { kind, namespace, name }, { defaultKind }, ); diff --git a/plugins/catalog-react/src/components/EntityRefLink/format.test.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts similarity index 86% rename from plugins/catalog-react/src/components/EntityRefLink/format.test.ts rename to plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts index b489f088fd..b541946ece 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/format.test.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { formatEntityRefTitle } from './format'; +import { humanizeEntityRef } from './humanize'; describe('formatEntityRefTitle', () => { it('formats entity in default namespace', () => { @@ -30,7 +30,7 @@ describe('formatEntityRefTitle', () => { lifecycle: 'production', }, }; - const title = formatEntityRefTitle(entity); + const title = humanizeEntityRef(entity); expect(title).toEqual('component:software'); }); @@ -48,7 +48,7 @@ describe('formatEntityRefTitle', () => { lifecycle: 'production', }, }; - const title = formatEntityRefTitle(entity); + const title = humanizeEntityRef(entity); expect(title).toEqual('component:test/software'); }); @@ -66,7 +66,7 @@ describe('formatEntityRefTitle', () => { lifecycle: 'production', }, }; - const title = formatEntityRefTitle(entity, { defaultKind: 'Component' }); + const title = humanizeEntityRef(entity, { defaultKind: 'Component' }); expect(title).toEqual('test/software'); }); @@ -76,7 +76,7 @@ describe('formatEntityRefTitle', () => { namespace: 'default', name: 'software', }; - const title = formatEntityRefTitle(entityName); + const title = humanizeEntityRef(entityName); expect(title).toEqual('component:software'); }); @@ -87,7 +87,7 @@ describe('formatEntityRefTitle', () => { name: 'software', }; - const title = formatEntityRefTitle(entityName); + const title = humanizeEntityRef(entityName); expect(title).toEqual('component:test/software'); }); @@ -98,7 +98,7 @@ describe('formatEntityRefTitle', () => { name: 'software', }; - const title = formatEntityRefTitle(entityName, { + const title = humanizeEntityRef(entityName, { defaultKind: 'component', }); expect(title).toEqual('test/software'); diff --git a/plugins/catalog-react/src/components/EntityRefLink/format.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts similarity index 97% rename from plugins/catalog-react/src/components/EntityRefLink/format.ts rename to plugins/catalog-react/src/components/EntityRefLink/humanize.ts index 90100245a1..35782c21b0 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/format.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts @@ -21,7 +21,7 @@ import { } from '@backstage/catalog-model'; /** @public */ -export function formatEntityRefTitle( +export function humanizeEntityRef( entityRef: Entity | EntityName, opts?: { defaultKind?: string }, ) { diff --git a/plugins/catalog-react/src/components/EntityRefLink/index.ts b/plugins/catalog-react/src/components/EntityRefLink/index.ts index d49993feb6..19e185459f 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/index.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/index.ts @@ -18,4 +18,4 @@ export { EntityRefLink } from './EntityRefLink'; export type { EntityRefLinkProps } from './EntityRefLink'; export { EntityRefLinks } from './EntityRefLinks'; export type { EntityRefLinksProps } from './EntityRefLinks'; -export { formatEntityRefTitle } from './format'; +export { humanizeEntityRef } from './format'; diff --git a/plugins/catalog-react/src/components/EntityTable/columns.tsx b/plugins/catalog-react/src/components/EntityTable/columns.tsx index d6c3582af6..d5ef791831 100644 --- a/plugins/catalog-react/src/components/EntityTable/columns.tsx +++ b/plugins/catalog-react/src/components/EntityTable/columns.tsx @@ -26,7 +26,7 @@ import { getEntityRelations } from '../../utils'; import { EntityRefLink, EntityRefLinks, - formatEntityRefTitle, + humanizeEntityRef, } from '../EntityRefLink'; /** @public */ @@ -38,7 +38,7 @@ export const columnFactories = Object.freeze({ function formatContent(entity: T): string { return ( entity.metadata?.title || - formatEntityRefTitle(entity, { + humanizeEntityRef(entity, { defaultKind, }) ); @@ -87,7 +87,7 @@ export const columnFactories = Object.freeze({ function formatContent(entity: T): string { return getRelations(entity) - .map(r => formatEntityRefTitle(r, { defaultKind })) + .map(r => humanizeEntityRef(r, { defaultKind })) .join(', '); } diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx index bf6c8971b0..1f37ee41b1 100644 --- a/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx @@ -33,7 +33,7 @@ import React, { useLayoutEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router'; import useAsync from 'react-use/lib/useAsync'; import { catalogApiRef } from '../../../api'; -import { formatEntityRefTitle } from '../../../components/EntityRefLink/format'; +import { humanizeEntityRef } from '../../../components/EntityRefLink/format'; import { entityRouteRef } from '../../../routes'; import { EntityKindIcon } from './EntityKindIcon'; @@ -132,7 +132,7 @@ function CustomNode({ node }: DependencyGraphTypes.RenderNodeProps) { const displayTitle = node.metadata.title || (node.kind && node.metadata.name && node.metadata.namespace - ? formatEntityRefTitle({ + ? humanizeEntityRef({ kind: node.kind, name: node.metadata.name, namespace: node.metadata.namespace || '', diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index 5d2c2e531b..9fb057a01c 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -15,7 +15,7 @@ */ import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { formatEntityRefTitle } from './components/EntityRefLink'; +import { humanizeEntityRef } from './components/EntityRefLink'; import { EntityFilter, UserListFilterKind } from './types'; import { getEntityRelations } from './utils'; @@ -107,7 +107,7 @@ export class EntityOwnerFilter implements EntityFilter { filterEntity(entity: Entity): boolean { return this.values.some(v => getEntityRelations(entity, RELATION_OWNED_BY).some( - o => formatEntityRefTitle(o, { defaultKind: 'group' }) === v, + o => humanizeEntityRef(o, { defaultKind: 'group' }) === v, ), ); } diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index a9a6f26228..4ef51d9945 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -22,7 +22,7 @@ import { import { favoriteEntityIcon, favoriteEntityTooltip, - formatEntityRefTitle, + humanizeEntityRef, getEntityRelations, useEntityList, useStarredEntities, @@ -132,16 +132,16 @@ export const CatalogTable = (props: CatalogTableProps) => { return { entity, resolved: { - name: formatEntityRefTitle(entity, { + name: humanizeEntityRef(entity, { defaultKind: 'Component', }), ownedByRelationsTitle: ownedByRelations - .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) + .map(r => humanizeEntityRef(r, { defaultKind: 'group' })) .join(', '), ownedByRelations, partOfSystemRelationTitle: partOfSystemRelations .map(r => - formatEntityRefTitle(r, { + humanizeEntityRef(r, { defaultKind: 'system', }), ) diff --git a/plugins/catalog/src/components/CatalogTable/columns.tsx b/plugins/catalog/src/components/CatalogTable/columns.tsx index d243be7c6f..0fb91d4f3f 100644 --- a/plugins/catalog/src/components/CatalogTable/columns.tsx +++ b/plugins/catalog/src/components/CatalogTable/columns.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; import { - formatEntityRefTitle, + humanizeEntityRef, EntityRefLink, EntityRefLinks, } from '@backstage/plugin-catalog-react'; @@ -34,7 +34,7 @@ export const columnFactories = Object.freeze({ function formatContent(entity: Entity): string { return ( entity.metadata?.title || - formatEntityRefTitle(entity, { + humanizeEntityRef(entity, { defaultKind: options?.defaultKind, }) ); diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx index 36788ed1b6..23553b8b76 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx @@ -31,7 +31,7 @@ import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; import { catalogApiRef, entityRouteRef, - formatEntityRefTitle, + humanizeEntityRef, getEntityRelations, } from '@backstage/plugin-catalog-react'; import { BackstageTheme } from '@backstage/theme'; @@ -193,7 +193,7 @@ export function GroupsDiagram() { kind: catalogItem.kind, name: (catalogItem as GroupEntity).spec?.profile?.displayName || - formatEntityRefTitle(catalogItem, { defaultKind: 'Group' }), + humanizeEntityRef(catalogItem, { defaultKind: 'Group' }), }); // Edge to parent diff --git a/plugins/fossa/src/components/FossaPage/FossaPage.tsx b/plugins/fossa/src/components/FossaPage/FossaPage.tsx index 1b53546ccd..90a36a6177 100644 --- a/plugins/fossa/src/components/FossaPage/FossaPage.tsx +++ b/plugins/fossa/src/components/FossaPage/FossaPage.tsx @@ -23,7 +23,7 @@ import { catalogApiRef, EntityRefLink, EntityRefLinks, - formatEntityRefTitle, + humanizeEntityRef, getEntityRelations, } from '@backstage/plugin-catalog-react'; import { Tooltip } from '@material-ui/core'; @@ -222,10 +222,10 @@ export const FossaPage = ({ return { entity, resolved: { - name: formatEntityRefTitle(entity), + name: humanizeEntityRef(entity), ownedByRelations, ownedByRelationsTitle: ownedByRelations - .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) + .map(r => humanizeEntityRef(r, { defaultKind: 'group' })) .join(', '), loading: summariesLoading, details: summary, diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx index 2b292cbe8b..cbf517bdca 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx @@ -25,7 +25,7 @@ import { import { useApi, useRouteRef } from '@backstage/core-plugin-api'; import { catalogApiRef, - formatEntityRefTitle, + humanizeEntityRef, isOwnerOf, useEntity, } from '@backstage/plugin-catalog-react'; @@ -108,7 +108,7 @@ const getQueryParams = ( owner: Entity, selectedEntity: EntityTypeProps, ): string => { - const ownerName = formatEntityRefTitle(owner, { defaultKind: 'group' }); + const ownerName = humanizeEntityRef(owner, { defaultKind: 'group' }); const { kind, type } = selectedEntity; const filters = { kind, diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx index 31c44bfe97..4a2f212fee 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx @@ -16,7 +16,7 @@ import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef, - formatEntityRefTitle, + humanizeEntityRef, } from '@backstage/plugin-catalog-react'; import { TextField } from '@material-ui/core'; import FormControl from '@material-ui/core/FormControl'; @@ -58,7 +58,7 @@ export const EntityPicker = ( ); const entityRefs = entities?.items.map(e => - formatEntityRefTitle(e, { defaultKind }), + humanizeEntityRef(e, { defaultKind }), ); const onSelect = useCallback( diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx index 80f63efaf9..aded9acffa 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import { - formatEntityRefTitle, + humanizeEntityRef, useOwnedEntities, } from '@backstage/plugin-catalog-react'; import { TextField } from '@material-ui/core'; @@ -50,7 +50,7 @@ export const OwnedEntityPicker = ( const { ownedEntities, loading } = useOwnedEntities(allowedKinds); const entityRefs = ownedEntities?.items - .map(e => formatEntityRefTitle(e, { defaultKind })) + .map(e => humanizeEntityRef(e, { defaultKind })) .filter(n => n); const onSelect = (_: any, value: string | null) => { diff --git a/plugins/techdocs/src/home/components/Tables/DocsTable.tsx b/plugins/techdocs/src/home/components/Tables/DocsTable.tsx index b875ba4e98..883c496191 100644 --- a/plugins/techdocs/src/home/components/Tables/DocsTable.tsx +++ b/plugins/techdocs/src/home/components/Tables/DocsTable.tsx @@ -20,7 +20,7 @@ import useCopyToClipboard from 'react-use/lib/useCopyToClipboard'; import { useRouteRef, useApi, configApiRef } from '@backstage/core-plugin-api'; import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; import { - formatEntityRefTitle, + humanizeEntityRef, getEntityRelations, } from '@backstage/plugin-catalog-react'; import { rootDocsRouteRef } from '../../../routes'; @@ -76,7 +76,7 @@ export const DocsTable = (props: DocsTableProps) => { }), ownedByRelations, ownedByRelationsTitle: ownedByRelations - .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) + .map(r => humanizeEntityRef(r, { defaultKind: 'group' })) .join(', '), }, }; From f5a279aa8904a1846d4c692256b777a8e7c86048 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 2 Mar 2022 17:13:17 +0100 Subject: [PATCH 114/353] chore: added backwards compatible method too Signed-off-by: blam --- .../src/components/EntityRefLink/EntityRefLink.tsx | 2 +- plugins/catalog-react/src/components/EntityRefLink/humanize.ts | 3 +++ plugins/catalog-react/src/components/EntityRefLink/index.ts | 2 +- .../components/InspectEntityDialog/components/AncestryPage.tsx | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index da2db24341..4db48351a9 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -22,7 +22,7 @@ import { } from '@backstage/catalog-model'; import React, { forwardRef } from 'react'; import { entityRouteRef } from '../../routes'; -import { humanizeEntityRef } from './format'; +import { humanizeEntityRef } from './humanize'; import { Link, LinkProps } from '@backstage/core-components'; import { useRouteRef } from '@backstage/core-plugin-api'; import { Tooltip } from '@material-ui/core'; diff --git a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts index 35782c21b0..af70ae71f4 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts @@ -20,6 +20,9 @@ import { DEFAULT_NAMESPACE, } from '@backstage/catalog-model'; +/** @deprecated please use {@link humanizeEntityRef} instead */ +export const formatEntityRefTitle = humanizeEntityRef; + /** @public */ export function humanizeEntityRef( entityRef: Entity | EntityName, diff --git a/plugins/catalog-react/src/components/EntityRefLink/index.ts b/plugins/catalog-react/src/components/EntityRefLink/index.ts index 19e185459f..50394547a0 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/index.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/index.ts @@ -18,4 +18,4 @@ export { EntityRefLink } from './EntityRefLink'; export type { EntityRefLinkProps } from './EntityRefLink'; export { EntityRefLinks } from './EntityRefLinks'; export type { EntityRefLinksProps } from './EntityRefLinks'; -export { humanizeEntityRef } from './format'; +export { humanizeEntityRef, formatEntityRefTitle } from './humanize'; diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx index 1f37ee41b1..0423c0da44 100644 --- a/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx @@ -33,7 +33,7 @@ import React, { useLayoutEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router'; import useAsync from 'react-use/lib/useAsync'; import { catalogApiRef } from '../../../api'; -import { humanizeEntityRef } from '../../../components/EntityRefLink/format'; +import { humanizeEntityRef } from '../../EntityRefLink'; import { entityRouteRef } from '../../../routes'; import { EntityKindIcon } from './EntityKindIcon'; From f41a293231ceac36003bbafbd0062ba477476469 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 2 Mar 2022 17:15:06 +0100 Subject: [PATCH 115/353] chore: added a changeset for the new entitRef method to make humanz Signed-off-by: blam --- .changeset/gorgeous-boats-hide.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .changeset/gorgeous-boats-hide.md diff --git a/.changeset/gorgeous-boats-hide.md b/.changeset/gorgeous-boats-hide.md new file mode 100644 index 0000000000..5c60240433 --- /dev/null +++ b/.changeset/gorgeous-boats-hide.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-graph': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-explore': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-org': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-techdocs': patch +--- + +- **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead. From 804c7906cfd345b77a16b00eda2a9f66c452ffdc Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 26 Feb 2022 18:21:52 +0100 Subject: [PATCH 116/353] Update core search types to be stream-based Signed-off-by: Eric Peterson --- packages/search-common/api-report.md | 21 ++++++------ packages/search-common/src/types.ts | 32 +++++++++++++------ plugins/search-backend-node/src/types.ts | 13 +++++--- .../search-backend/src/service/router.test.ts | 2 +- 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/packages/search-common/api-report.md b/packages/search-common/api-report.md index 65f9f25180..e47140d274 100644 --- a/packages/search-common/api-report.md +++ b/packages/search-common/api-report.md @@ -3,25 +3,28 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + import { JsonObject } from '@backstage/types'; import { Permission } from '@backstage/plugin-permission-common'; +import { Readable } from 'stream'; +import { Transform } from 'stream'; +import { Writable } from 'stream'; -// Warning: (ae-missing-release-tag) "DocumentCollator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "DocumentCollatorFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export interface DocumentCollator { - // (undocumented) - execute(): Promise; +export interface DocumentCollatorFactory { + getCollator(): Promise; readonly type: string; readonly visibilityPermission?: Permission; } -// Warning: (ae-missing-release-tag) "DocumentDecorator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "DocumentDecoratorFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export interface DocumentDecorator { - // (undocumented) - execute(documents: IndexableDocument[]): Promise; +export interface DocumentDecoratorFactory { + getDecorator(): Promise; readonly types?: string[]; } @@ -60,7 +63,7 @@ export type QueryTranslator = (query: SearchQuery) => unknown; // // @public export interface SearchEngine { - index(type: string, documents: IndexableDocument[]): Promise; + getIndexer(type: string): Promise; query( query: SearchQuery, options?: QueryRequestOptions, diff --git a/packages/search-common/src/types.ts b/packages/search-common/src/types.ts index 4e61767ea0..6e106d3899 100644 --- a/packages/search-common/src/types.ts +++ b/packages/search-common/src/types.ts @@ -16,6 +16,7 @@ import { Permission } from '@backstage/plugin-permission-common'; import { JsonObject } from '@backstage/types'; +import { Readable, Transform, Writable } from 'stream'; export interface SearchQuery { term: string; @@ -82,10 +83,9 @@ export type DocumentTypeInfo = { }; /** - * Interface that must be implemented in order to expose new documents to - * search. + * Factory class for instantiating collators. */ -export interface DocumentCollator { +export interface DocumentCollatorFactory { /** * The type or name of the document set returned by this collator. Used as an * index name by Search Engines. @@ -98,21 +98,27 @@ export interface DocumentCollator { */ readonly visibilityPermission?: Permission; - execute(): Promise; + /** + * Instantiates and resolves a document collator. + */ + getCollator(): Promise; } /** - * Interface that must be implemented in order to decorate existing documents with - * additional metadata. + * Factory class for instantiating decorators. */ -export interface DocumentDecorator { +export interface DocumentDecoratorFactory { /** * An optional array of document/index types on which this decorator should * be applied. If no types are provided, this decorator will be applied to * all document/index types. */ readonly types?: string[]; - execute(documents: IndexableDocument[]): Promise; + + /** + * Instantiates and resolves a document decorator. + */ + getDecorator(): Promise; } /** @@ -137,9 +143,15 @@ export interface SearchEngine { setTranslator(translator: QueryTranslator): void; /** - * Add the given documents to the SearchEngine index of the given type. + * Factory method for getting a search engine indexer for a given document + * type. + * + * @param type - The type or name of the document set for which an indexer + * should be retrieved. This corresponds to the `type` property on the + * document collator/decorator factories and will most often be used to + * identify an index or group to which documents should be written. */ - index(type: string, documents: IndexableDocument[]): Promise; + getIndexer(type: string): Promise; /** * Perform a search query against the SearchEngine. diff --git a/plugins/search-backend-node/src/types.ts b/plugins/search-backend-node/src/types.ts index df83357f6c..4bcc8ec114 100644 --- a/plugins/search-backend-node/src/types.ts +++ b/plugins/search-backend-node/src/types.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { DocumentCollator, DocumentDecorator } from '@backstage/search-common'; +import { + DocumentCollatorFactory, + DocumentDecoratorFactory, +} from '@backstage/search-common'; /** * Parameters required to register a collator. @@ -26,9 +29,9 @@ export interface RegisterCollatorParameters { defaultRefreshIntervalSeconds: number; /** - * The collator class responsible for returning all documents of the given type. + * The class responsible for returning the document collator of the given type. */ - collator: DocumentCollator; + factory: DocumentCollatorFactory; } /** @@ -36,7 +39,7 @@ export interface RegisterCollatorParameters { */ export interface RegisterDecoratorParameters { /** - * The decorator class responsible for appending or modifying documents of the given type(s). + * The class responsible for returning the decorator which appends, modifies, or filters documents. */ - decorator: DocumentDecorator; + factory: DocumentDecoratorFactory; } diff --git a/plugins/search-backend/src/service/router.test.ts b/plugins/search-backend/src/service/router.test.ts index 0a94250aeb..bdf46a240b 100644 --- a/plugins/search-backend/src/service/router.test.ts +++ b/plugins/search-backend/src/service/router.test.ts @@ -105,7 +105,7 @@ describe('createRouter', () => { beforeAll(async () => { const logger = getVoidLogger(); mockSearchEngine = { - index: jest.fn(), + getIndexer: jest.fn(), setTranslator: jest.fn(), query: jest.fn(), }; From 2eae26293e3ecc1adc30a8fa870b5c260284a3b1 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 26 Feb 2022 18:23:13 +0100 Subject: [PATCH 117/353] Update IndexBuilder to be a stream pipeline Signed-off-by: Eric Peterson --- .../src/IndexBuilder.test.ts | 82 +++++++-------- .../search-backend-node/src/IndexBuilder.ts | 99 +++++++++---------- 2 files changed, 83 insertions(+), 98 deletions(-) diff --git a/plugins/search-backend-node/src/IndexBuilder.test.ts b/plugins/search-backend-node/src/IndexBuilder.test.ts index 0465f2701c..95a2e1d439 100644 --- a/plugins/search-backend-node/src/IndexBuilder.test.ts +++ b/plugins/search-backend-node/src/IndexBuilder.test.ts @@ -16,35 +16,37 @@ import { getVoidLogger } from '@backstage/backend-common'; import { - DocumentCollator, - DocumentDecorator, - IndexableDocument, + DocumentCollatorFactory, + DocumentDecoratorFactory, } from '@backstage/search-common'; +import { Readable, Transform } from 'stream'; import { IndexBuilder } from './IndexBuilder'; import { LunrSearchEngine, SearchEngine } from './index'; -class TestDocumentCollator implements DocumentCollator { +class TestDocumentCollatorFactory implements DocumentCollatorFactory { readonly type: string = 'anything'; - async execute(): Promise { - return []; + async getCollator(): Promise { + const collator = new Readable({ objectMode: true }); + collator._read = () => {}; + return collator; } } -class TypedDocumentCollator extends TestDocumentCollator { +class TypedDocumentCollatorFactory extends TestDocumentCollatorFactory { readonly type = 'an-expected-type'; } -class TestDocumentDecorator implements DocumentDecorator { - async execute(documents: IndexableDocument[]) { - return documents; +class TestDocumentDecoratorFactory implements DocumentDecoratorFactory { + async getDecorator(): Promise { + return new Transform(); } } -class TypedDocumentDecorator extends TestDocumentDecorator { +class TypedDocumentDecoratorFactory extends TestDocumentDecoratorFactory { readonly types = ['an-expected-type']; } -class DifferentlyTypedDocumentDecorator extends TestDocumentDecorator { +class DifferentlyTypedDocumentDecoratorFactory extends TestDocumentDecoratorFactory { readonly types = ['not-the-expected-type']; } @@ -64,13 +66,13 @@ describe('IndexBuilder', () => { describe('addCollator', () => { it('adds a collator', async () => { jest.useFakeTimers(); - const testCollator = new TestDocumentCollator(); - const collatorSpy = jest.spyOn(testCollator, 'execute'); + const testCollatorFactory = new TestDocumentCollatorFactory(); + const collatorSpy = jest.spyOn(testCollatorFactory, 'getCollator'); // Add a collator. testIndexBuilder.addCollator({ defaultRefreshIntervalSeconds: 6, - collator: testCollator, + factory: testCollatorFactory, }); // Build the index and ensure the collator was invoked. @@ -84,19 +86,19 @@ describe('IndexBuilder', () => { describe('addDecorator', () => { it('adds a decorator', async () => { jest.useFakeTimers(); - const testCollator = new TestDocumentCollator(); - const testDecorator = new TestDocumentDecorator(); - const decoratorSpy = jest.spyOn(testDecorator, 'execute'); + const testCollatorFactory = new TestDocumentCollatorFactory(); + const testDecoratorFactory = new TestDocumentDecoratorFactory(); + const decoratorSpy = jest.spyOn(testDecoratorFactory, 'getDecorator'); // Add a collator. testIndexBuilder.addCollator({ defaultRefreshIntervalSeconds: 6, - collator: testCollator, + factory: testCollatorFactory, }); // Add a decorator. testIndexBuilder.addDecorator({ - decorator: testDecorator, + factory: testDecoratorFactory, }); // Build the index and ensure the decorator was invoked. @@ -110,27 +112,20 @@ describe('IndexBuilder', () => { it('adds a type-specific decorator', async () => { jest.useFakeTimers(); - const testCollator = new TypedDocumentCollator(); - const testDecorator = new TypedDocumentDecorator(); - const docFixture = { - title: 'Test', - text: 'Test text.', - location: '/test/location', - }; - jest - .spyOn(testCollator, 'execute') - .mockImplementation(async () => [docFixture]); - const decoratorSpy = jest.spyOn(testDecorator, 'execute'); + const testCollatorFactory = new TypedDocumentCollatorFactory(); + const testDecoratorFactory = new TypedDocumentDecoratorFactory(); + jest.spyOn(testCollatorFactory, 'getCollator'); + const decoratorSpy = jest.spyOn(testDecoratorFactory, 'getDecorator'); // Add a collator. testIndexBuilder.addCollator({ defaultRefreshIntervalSeconds: 6, - collator: testCollator, + factory: testCollatorFactory, }); // Add a decorator for the same type. testIndexBuilder.addDecorator({ - decorator: testDecorator, + factory: testDecoratorFactory, }); // Build the index and ensure the decorator was invoked. @@ -140,31 +135,24 @@ describe('IndexBuilder', () => { // wait for async decorator execution await Promise.resolve(); expect(decoratorSpy).toHaveBeenCalled(); - expect(decoratorSpy).toHaveBeenCalledWith([docFixture]); }); it('adds a type-specific decorator that should not be called', async () => { - const docFixture = { - title: 'Test', - text: 'Test text.', - location: '/test/location', - }; - const testCollator = new TestDocumentCollator(); - const testDecorator = new DifferentlyTypedDocumentDecorator(); - const collatorSpy = jest - .spyOn(testCollator, 'execute') - .mockImplementation(async () => [docFixture]); - const decoratorSpy = jest.spyOn(testDecorator, 'execute'); + const testCollatorFactory = new TestDocumentCollatorFactory(); + const testDecoratorFactory = + new DifferentlyTypedDocumentDecoratorFactory(); + const collatorSpy = jest.spyOn(testCollatorFactory, 'getCollator'); + const decoratorSpy = jest.spyOn(testDecoratorFactory, 'getDecorator'); // Add a collator. testIndexBuilder.addCollator({ defaultRefreshIntervalSeconds: 6, - collator: testCollator, + factory: testCollatorFactory, }); // Add a decorator for a different type. testIndexBuilder.addDecorator({ - decorator: testDecorator, + factory: testDecoratorFactory, }); // Build the index and ensure the decorator was not invoked. diff --git a/plugins/search-backend-node/src/IndexBuilder.ts b/plugins/search-backend-node/src/IndexBuilder.ts index 92adb03d7d..5f39c2fa33 100644 --- a/plugins/search-backend-node/src/IndexBuilder.ts +++ b/plugins/search-backend-node/src/IndexBuilder.ts @@ -15,12 +15,12 @@ */ import { - DocumentCollator, - DocumentDecorator, + DocumentCollatorFactory, + DocumentDecoratorFactory, DocumentTypeInfo, - IndexableDocument, SearchEngine, } from '@backstage/search-common'; +import { Transform, pipeline } from 'stream'; import { Logger } from 'winston'; import { Scheduler } from './index'; import { @@ -29,7 +29,7 @@ import { } from './types'; interface CollatorEnvelope { - collate: DocumentCollator; + factory: DocumentCollatorFactory; refreshInterval: number; } @@ -40,7 +40,7 @@ type IndexBuilderOptions = { export class IndexBuilder { private collators: Record; - private decorators: Record; + private decorators: Record; private documentTypes: Record; private searchEngine: SearchEngine; private logger: Logger; @@ -66,18 +66,18 @@ export class IndexBuilder { * given refresh interval. */ addCollator({ - collator, + factory, defaultRefreshIntervalSeconds, }: RegisterCollatorParameters): void { this.logger.info( - `Added ${collator.constructor.name} collator for type ${collator.type}`, + `Added ${factory.constructor.name} collator factory for type ${factory.type}`, ); - this.collators[collator.type] = { + this.collators[factory.type] = { refreshInterval: defaultRefreshIntervalSeconds, - collate: collator, + factory, }; - this.documentTypes[collator.type] = { - visibilityPermission: collator.visibilityPermission, + this.documentTypes[factory.type] = { + visibilityPermission: factory.visibilityPermission, }; } @@ -86,18 +86,18 @@ export class IndexBuilder { * the decorator, it will be applied to documents from all known collators, * otherwise it will only be applied to documents of the given types. */ - addDecorator({ decorator }: RegisterDecoratorParameters): void { - const types = decorator.types || ['*']; + addDecorator({ factory }: RegisterDecoratorParameters): void { + const types = factory.types || ['*']; this.logger.info( - `Added decorator ${decorator.constructor.name} to types ${types.join( + `Added decorator ${factory.constructor.name} to types ${types.join( ', ', )}`, ); types.forEach(type => { if (this.decorators.hasOwnProperty(type)) { - this.decorators[type].push(decorator); + this.decorators[type].push(factory); } else { - this.decorators[type] = [decorator]; + this.decorators[type] = [factory]; } }); } @@ -111,46 +111,43 @@ export class IndexBuilder { Object.keys(this.collators).forEach(type => { scheduler.addToSchedule(async () => { - // Collate, Decorate, Index. - const decorators: DocumentDecorator[] = ( - this.decorators['*'] || [] - ).concat(this.decorators[type] || []); - - this.logger.debug( - `Collating documents for ${type} via ${this.collators[type].collate.constructor.name}`, + // Instantiate the collator. + const collator = await this.collators[type].factory.getCollator(); + this.logger.info( + `Collating documents for ${type} via ${this.collators[type].factory.constructor.name}`, ); - let documents: IndexableDocument[]; - try { - documents = await this.collators[type].collate.execute(); - } catch (e) { - this.logger.error( - `Collating documents for ${type} via ${this.collators[type].collate.constructor.name} failed: ${e}`, - ); - return; - } + // Instantiate all relevant decorators. + const decorators: Transform[] = await Promise.all( + (this.decorators['*'] || []) + .concat(this.decorators[type] || []) + .map(async factory => { + const decorator = await factory.getDecorator(); + this.logger.info( + `Attached decorator via ${factory.constructor.name} to ${type} index pipeline.`, + ); + return decorator; + }), + ); - for (let i = 0; i < decorators.length; i++) { - this.logger.debug( - `Decorating ${type} documents via ${decorators[i].constructor.name}`, - ); - try { - documents = await decorators[i].execute(documents); - } catch (e) { - this.logger.error( - `Decorating ${type} documents via ${decorators[i].constructor.name} failed: ${e}`, - ); - return; - } - } + // Instantiate the indexer. + const indexer = await this.searchEngine.getIndexer(type); - if (!documents || documents.length === 0) { - this.logger.debug(`No documents for type "${type}" to index`); - return; - } + // Compose collator/decorators/indexer into a pipeline + return new Promise(done => { + pipeline([collator, ...decorators, indexer], error => { + if (error) { + this.logger.error( + `Collating documents for ${type} failed: ${error}`, + ); + } else { + this.logger.info(`Collating documents for ${type} succeeded`); + } - // pushing documents to index to a configured search engine. - await this.searchEngine.index(type, documents); + // Signal index pipeline completion! + done(); + }); + }); }, this.collators[type].refreshInterval * 1000); }); From ce3f566e9c37bace23d002d5962b4a94ac544979 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 26 Feb 2022 18:23:49 +0100 Subject: [PATCH 118/353] Introduce test utilities for stream-based search implementations Signed-off-by: Eric Peterson --- .../src/test-utils/TestPipeline.ts | 138 ++++++++++++++++++ .../src/test-utils/index.ts | 18 +++ 2 files changed, 156 insertions(+) create mode 100644 plugins/search-backend-node/src/test-utils/TestPipeline.ts create mode 100644 plugins/search-backend-node/src/test-utils/index.ts diff --git a/plugins/search-backend-node/src/test-utils/TestPipeline.ts b/plugins/search-backend-node/src/test-utils/TestPipeline.ts new file mode 100644 index 0000000000..2dbdeb85ad --- /dev/null +++ b/plugins/search-backend-node/src/test-utils/TestPipeline.ts @@ -0,0 +1,138 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { IndexableDocument } from '@backstage/search-common'; +import { pipeline, Readable, Transform, Writable } from 'stream'; + +/** + * Object resolved after a test pipeline is executed. + */ +export type TestPipelineResult = { + /** + * If an error was emitted by the pipeline, it will be set here. + */ + error: unknown; + + /** + * A list of documents collected at the end of the pipeline. If the subject + * under test is an indexer, this will be an empty array (because your + * indexer should have received the documents instead). + */ + documents: IndexableDocument[]; +}; + +/** + * Test utility for Backstage Search collators, decorators, and indexers. + */ +export class TestPipeline { + private collator?: Readable; + private decorator?: Transform; + private indexer?: Writable; + + private constructor({ + collator, + decorator, + indexer, + }: { + collator?: Readable; + decorator?: Transform; + indexer?: Writable; + }) { + this.collator = collator; + this.decorator = decorator; + this.indexer = indexer; + } + + /** + * Provide the collator, decorator, or indexer to be tested. + */ + static withSubject(subject: Readable | Transform | Writable) { + if (subject instanceof Transform) { + return new TestPipeline({ decorator: subject }); + } + + if (subject instanceof Readable) { + return new TestPipeline({ collator: subject }); + } + + if (subject instanceof Writable) { + return new TestPipeline({ indexer: subject }); + } + + throw new Error( + 'Unknown test subject: are you passing a readable, writable, or transform stream?', + ); + } + + /** + * Provide documents for testing decorators and indexers. + */ + withDocuments(documents: IndexableDocument[]): TestPipeline { + if (this.collator) { + throw new Error('Cannot provide documents when testing a collator.'); + } + + // Set a naive readable stream that just pushes all given documents. + this.collator = new Readable({ objectMode: true }); + this.collator._read = () => {}; + process.nextTick(() => { + documents.forEach(document => { + this.collator!.push(document); + }); + this.collator!.push(null); + }); + + return this; + } + + /** + * Execute the test pipeline so that you can make assertions about the result + * or behavior of the given test subject. + */ + async execute(): Promise { + const documents: IndexableDocument[] = []; + if (!this.collator) { + throw new Error( + 'Cannot execute pipeline without a collator or documents', + ); + } + + // If we are here and there is no indexer, we are testing a collator or a + // decorator. Set up a naive writable that captures documents in memory. + if (!this.indexer) { + this.indexer = new Writable({ objectMode: true }); + this.indexer._write = (document: IndexableDocument, _, done) => { + documents.push(document); + done(); + }; + } + + return new Promise(done => { + const pipes: (Readable | Transform | Writable)[] = [this.collator!]; + if (this.decorator) { + pipes.push(this.decorator); + } + pipes.push(this.indexer!); + + pipeline(pipes, error => { + done({ + error, + documents, + }); + }); + }); + } +} diff --git a/plugins/search-backend-node/src/test-utils/index.ts b/plugins/search-backend-node/src/test-utils/index.ts new file mode 100644 index 0000000000..185d25433b --- /dev/null +++ b/plugins/search-backend-node/src/test-utils/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { TestPipeline } from './TestPipeline'; +export type { TestPipelineResult } from './TestPipeline'; From cef19ee9662d0746ccac7b047a6ce9bf94af2781 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 26 Feb 2022 18:24:38 +0100 Subject: [PATCH 119/353] Introduce a base classes to simplify stream-based implementations Signed-off-by: Eric Peterson --- .../indexing/BatchSearchEngineIndexer.test.ts | 156 +++++++++++++++++ .../src/indexing/BatchSearchEngineIndexer.ts | 121 ++++++++++++++ .../src/indexing/DecoratorBase.test.ts | 157 ++++++++++++++++++ .../src/indexing/DecoratorBase.ts | 126 ++++++++++++++ .../search-backend-node/src/indexing/index.ts | 19 +++ 5 files changed, 579 insertions(+) create mode 100644 plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.test.ts create mode 100644 plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts create mode 100644 plugins/search-backend-node/src/indexing/DecoratorBase.test.ts create mode 100644 plugins/search-backend-node/src/indexing/DecoratorBase.ts create mode 100644 plugins/search-backend-node/src/indexing/index.ts diff --git a/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.test.ts b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.test.ts new file mode 100644 index 0000000000..b692ce3aff --- /dev/null +++ b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.test.ts @@ -0,0 +1,156 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { IndexableDocument } from '@backstage/search-common'; +import { BatchSearchEngineIndexer } from './BatchSearchEngineIndexer'; +import { TestPipeline } from '../test-utils'; + +const indexSpy = jest.fn().mockResolvedValue(undefined); +const initializeSpy = jest.fn().mockResolvedValue(undefined); +const finalizeSpy = jest.fn().mockResolvedValue(undefined); + +class ConcreteBatchIndexer extends BatchSearchEngineIndexer { + async index(documents: IndexableDocument[]): Promise { + return indexSpy(documents); + } + async initialize(): Promise { + return initializeSpy(); + } + async finalize(): Promise { + return finalizeSpy(); + } +} + +describe('BatchSearchEngineIndexer', () => { + const document = { + title: 'Some Document', + text: 'Some document text.', + location: '/some/location', + }; + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should work end-to-end', async () => { + const indexer = new ConcreteBatchIndexer({ batchSize: 1 }); + await TestPipeline.withSubject(indexer) + .withDocuments([document, document, document]) + .execute(); + expect(indexSpy).toHaveBeenCalledTimes(3); + }); + + it('should call initialize at construction', () => { + // @ts-expect-error + const _indexer = new ConcreteBatchIndexer({ batchSize: 1 }); + + return new Promise(done => { + // Allow initialization to complete. + setImmediate(() => { + expect(initializeSpy).toHaveBeenCalled(); + done(); + }); + }); + }); + + it('should emit error if initialization throws', () => { + // Cause the initializer to throw. + const expectedError = new Error('some error'); + initializeSpy.mockRejectedValue(expectedError); + const indexer = new ConcreteBatchIndexer({ batchSize: 1 }); + + return new Promise(done => { + // Listen for the error and assert it's what was thrown. + indexer.on('error', error => { + expect(error).toStrictEqual(expectedError); + done(); + }); + + // Write a document to force the error state to become known. + indexer.write(document); + }); + }); + + it('should call index according to batchSize', () => { + const indexer = new ConcreteBatchIndexer({ batchSize: 2 }); + + return new Promise(done => { + // Listen for it to finish and assert the batches. + indexer.on('finish', () => { + expect(indexSpy).toHaveBeenCalledTimes(2); + expect(indexSpy).toHaveBeenNthCalledWith(1, [document, document]); + expect(indexSpy).toHaveBeenNthCalledWith(2, [document]); + done(); + }); + + // Write batchSize + 1 documents and end the stream. + indexer.write(document); + indexer.write(document); + indexer.write(document); + indexer.end(); + }); + }); + + it('should call index without exceeding batchSize', () => { + const indexer = new ConcreteBatchIndexer({ batchSize: 2 }); + + return new Promise(done => { + // Listen for it to finish and assert that it still wrote. + indexer.on('finish', () => { + expect(indexSpy).toHaveBeenCalledTimes(1); + expect(indexSpy).toHaveBeenNthCalledWith(1, [document]); + done(); + }); + + // Write batchSize - 1 documents and end the stream. + indexer.write(document); + indexer.end(); + }); + }); + + it('should emit error if index throws', () => { + // Cause the indexer to throw. + const expectedError = new Error('index error'); + indexSpy.mockRejectedValue(expectedError); + const indexer = new ConcreteBatchIndexer({ batchSize: 1 }); + + return new Promise(done => { + // Listen for the error and assert it's what was thrown. + indexer.on('error', error => { + expect(error).toStrictEqual(expectedError); + done(); + }); + + indexer.write(document); + }); + }); + + it('should emit error if finalize throws', () => { + // Cause the indexer to throw. + const expectedError = new Error('finalize error'); + finalizeSpy.mockRejectedValue(expectedError); + const indexer = new ConcreteBatchIndexer({ batchSize: 1 }); + + return new Promise(done => { + // Listen for the error and assert it's what was thrown. + indexer.on('error', error => { + expect(error).toStrictEqual(expectedError); + done(); + }); + + indexer.end(); + }); + }); +}); diff --git a/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts new file mode 100644 index 0000000000..4c29d0b573 --- /dev/null +++ b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts @@ -0,0 +1,121 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { assertError } from '@backstage/errors'; +import { IndexableDocument } from '@backstage/search-common'; +import { Writable } from 'stream'; + +export type BatchSearchEngineOptions = { + batchSize: number; +}; + +/** + * Base class encapsulating batch-based stream processing. Useful as a base + * class for search engine indexers. + */ +export abstract class BatchSearchEngineIndexer extends Writable { + private batchSize: number; + private currentBatch: IndexableDocument[] = []; + private initialized: Promise; + + constructor(options: BatchSearchEngineOptions) { + super({ objectMode: true }); + this.batchSize = options.batchSize; + + // @todo Once node v15 is minimum, convert to _construct implementation. + this.initialized = new Promise(done => { + // Necessary to allow concrete implementation classes to construct + // themselves before calling their initialize() methods. + setImmediate(async () => { + try { + await this.initialize(); + done(undefined); + } catch (e) { + assertError(e); + done(e); + } + }); + }); + } + + /** + * Receives an array of indexable documents (of size this.batchSize) which + * should be written to the search engine. This method won't be called again + * at least until it resolves. + */ + public abstract index(documents: IndexableDocument[]): Promise; + + /** + * Any asynchronous setup tasks can be performed here. + */ + public abstract initialize(): Promise; + + /** + * Any asynchronous teardown tasks can be performed here. + */ + public abstract finalize(): Promise; + + /** + * Encapsulates batch stream write logic. + * @internal + */ + async _write( + doc: IndexableDocument, + _e: any, + done: (error?: Error | null) => void, + ) { + // Wait for init before proceeding. Throw error if initialization failed. + const maybeError = await this.initialized; + if (maybeError) { + done(maybeError); + return; + } + + this.currentBatch.push(doc); + if (this.currentBatch.length < this.batchSize) { + done(); + return; + } + + try { + await this.index(this.currentBatch); + this.currentBatch = []; + done(); + } catch (e) { + assertError(e); + done(e); + } + } + + /** + * Encapsulates finalization and final error handling logic. + * @internal + */ + async _final(done: (error?: Error | null) => void) { + try { + // Index any remaining documents. + if (this.currentBatch.length) { + await this.index(this.currentBatch); + this.currentBatch = []; + } + await this.finalize(); + done(); + } catch (e) { + assertError(e); + done(e); + } + } +} diff --git a/plugins/search-backend-node/src/indexing/DecoratorBase.test.ts b/plugins/search-backend-node/src/indexing/DecoratorBase.test.ts new file mode 100644 index 0000000000..3b045dcddf --- /dev/null +++ b/plugins/search-backend-node/src/indexing/DecoratorBase.test.ts @@ -0,0 +1,157 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { IndexableDocument } from '@backstage/search-common'; +import { DecoratorBase } from './DecoratorBase'; +import { TestPipeline } from '../test-utils'; + +const decorateSpy = jest.fn().mockResolvedValue(undefined); +const initializeSpy = jest.fn().mockResolvedValue(undefined); +const finalizeSpy = jest.fn().mockResolvedValue(undefined); + +class ConcreteDecorator extends DecoratorBase { + public initialize(): Promise { + return initializeSpy(); + } + public decorate( + document: IndexableDocument, + ): Promise { + return decorateSpy(document); + } + public finalize(): Promise { + return finalizeSpy(); + } +} + +describe('DecoratorBase', () => { + const document = { + title: 'Some Document', + text: 'Some document text.', + location: '/some/location', + }; + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should work end-to-end', async () => { + decorateSpy.mockImplementation(doc => ({ + ...doc, + transformed: true, + })); + + const decorator = new ConcreteDecorator(); + const { documents } = await TestPipeline.withSubject(decorator) + .withDocuments([document, document, document]) + .execute(); + + expect(documents.length).toBe(3); + expect((documents[0] as unknown as any).transformed).toBe(true); + expect((documents[1] as unknown as any).transformed).toBe(true); + expect((documents[2] as unknown as any).transformed).toBe(true); + }); + + it('should allow filtering', async () => { + decorateSpy.mockResolvedValue(undefined); + + const decorator = new ConcreteDecorator(); + const { documents } = await TestPipeline.withSubject(decorator) + .withDocuments([document, document, document]) + .execute(); + + expect(decorateSpy).toHaveBeenCalledTimes(3); + expect(documents.length).toBe(0); + }); + + it('should allow fanning', async () => { + decorateSpy.mockImplementation(doc => { + return [doc, doc]; + }); + + const decorator = new ConcreteDecorator(); + const { documents } = await TestPipeline.withSubject(decorator) + .withDocuments([document, document, document]) + .execute(); + + expect(decorateSpy).toHaveBeenCalledTimes(3); + expect(documents.length).toBe(6); + }); + + it('should call initialize at construction', () => { + // @ts-expect-error + const _indexer = new ConcreteDecorator(); + + return new Promise(done => { + // Allow initialization to complete. + setImmediate(() => { + expect(initializeSpy).toHaveBeenCalled(); + done(); + }); + }); + }); + + it('should emit error if initialization throws', () => { + // Cause the initializer to throw. + const expectedError = new Error('some error'); + initializeSpy.mockRejectedValue(expectedError); + const decorator = new ConcreteDecorator(); + + return new Promise(done => { + // Listen for the error and assert it's what was thrown. + decorator.on('error', error => { + expect(error).toStrictEqual(expectedError); + done(); + }); + + // Write a document to force the error state to become known. + decorator.write(document); + }); + }); + + it('should emit error if index throws', () => { + // Cause the indexer to throw. + const expectedError = new Error('decorate error'); + decorateSpy.mockRejectedValue(expectedError); + const decorator = new ConcreteDecorator(); + + return new Promise(done => { + // Listen for the error and assert it's what was thrown. + decorator.on('error', error => { + expect(error).toStrictEqual(expectedError); + done(); + }); + + decorator.write(document); + }); + }); + + it('should emit error if finalize throws', () => { + // Cause the indexer to throw. + const expectedError = new Error('finalize error'); + finalizeSpy.mockRejectedValue(expectedError); + const decorator = new ConcreteDecorator(); + + return new Promise(done => { + // Listen for the error and assert it's what was thrown. + decorator.on('error', error => { + expect(error).toStrictEqual(expectedError); + done(); + }); + + decorator.end(); + }); + }); +}); diff --git a/plugins/search-backend-node/src/indexing/DecoratorBase.ts b/plugins/search-backend-node/src/indexing/DecoratorBase.ts new file mode 100644 index 0000000000..a28d652fd7 --- /dev/null +++ b/plugins/search-backend-node/src/indexing/DecoratorBase.ts @@ -0,0 +1,126 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { assertError } from '@backstage/errors'; +import { IndexableDocument } from '@backstage/search-common'; +import { Transform } from 'stream'; + +/** + * Base class encapsulating simple async transformations. Useful as a base + * class for Backstage search decorators. + */ +export abstract class DecoratorBase extends Transform { + private initialized: Promise; + + constructor() { + super({ objectMode: true }); + + // @todo Once node v15 is minimum, convert to _construct implementation. + this.initialized = new Promise(done => { + // Necessary to allow concrete implementation classes to construct + // themselves before calling their initialize() methods. + setImmediate(async () => { + try { + await this.initialize(); + done(undefined); + } catch (e) { + assertError(e); + done(e); + } + }); + }); + } + + /** + * Any asynchronous setup tasks can be performed here. + */ + public abstract initialize(): Promise; + + /** + * Receives a single indexable document. In your decorate method, you can: + * + * - Resolve `undefined` to indicate the record should be omitted. + * - Resolve a single modified document, which could contain new fields, + * edited fields, or removed fields. + * - Resolve an array of indexable documents, if the purpose if the decorator + * is to convert one document into multiple derivative documents. + */ + public abstract decorate( + document: IndexableDocument, + ): Promise; + + /** + * Any asynchronous teardown tasks can be performed here. + */ + public abstract finalize(): Promise; + + /** + * Encapsulates simple transform stream logic. + * @internal + */ + async _transform( + document: IndexableDocument, + _: any, + done: (error?: Error | null) => void, + ) { + // Wait for init before proceeding. Throw error if initialization failed. + const maybeError = await this.initialized; + if (maybeError) { + done(maybeError); + return; + } + + try { + const decorated = await this.decorate(document); + + // If undefined was returned, omit the record and move on. + if (decorated === undefined) { + done(); + return; + } + + // If an array of documents was given, push them all. + if (Array.isArray(decorated)) { + decorated.forEach(doc => { + this.push(doc); + }); + done(); + return; + } + + // Otherwise, just push the decorated document. + this.push(decorated); + done(); + } catch (e) { + assertError(e); + done(e); + } + } + + /** + * Encapsulates finalization and final error handling logic. + * @internal + */ + async _final(done: (error?: Error | null) => void) { + try { + await this.finalize(); + done(); + } catch (e) { + assertError(e); + done(e); + } + } +} diff --git a/plugins/search-backend-node/src/indexing/index.ts b/plugins/search-backend-node/src/indexing/index.ts new file mode 100644 index 0000000000..e86235df16 --- /dev/null +++ b/plugins/search-backend-node/src/indexing/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { BatchSearchEngineIndexer } from './BatchSearchEngineIndexer'; +export { DecoratorBase } from './DecoratorBase'; +export type { BatchSearchEngineOptions } from './BatchSearchEngineIndexer'; From c87be57d3a6d515b87009a642438bb1eeb0e65cf Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 26 Feb 2022 18:25:05 +0100 Subject: [PATCH 120/353] Update LunrSearchEngine to be stream-based Signed-off-by: Eric Peterson --- .../src/engines/LunrSearchEngine.test.ts | 221 +++++++++++++++--- .../src/engines/LunrSearchEngine.ts | 30 +-- .../engines/LunrSearchEngineIndexer.test.ts | 109 +++++++++ .../src/engines/LunrSearchEngineIndexer.ts | 68 ++++++ .../search-backend-node/src/engines/index.ts | 1 + 5 files changed, 370 insertions(+), 59 deletions(-) create mode 100644 plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.test.ts create mode 100644 plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.ts diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts index cbe48ef5a7..88a3ba9a24 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts @@ -16,28 +16,61 @@ import { getVoidLogger } from '@backstage/backend-common'; import lunr from 'lunr'; -import { SearchEngine } from '@backstage/search-common'; +import { IndexableDocument, SearchEngine } from '@backstage/search-common'; import { ConcreteLunrQuery, LunrSearchEngine, decodePageCursor, encodePageCursor, } from './LunrSearchEngine'; +import { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer'; +import { TestPipeline } from '../test-utils'; /** * Just used to test the default translator shipped with LunrSearchEngine. */ -class LunrSearchEngineForTranslatorTests extends LunrSearchEngine { +class LunrSearchEngineForTests extends LunrSearchEngine { + getDocStore() { + return this.docStore; + } + setDocStore(docStore: Record) { + this.docStore = docStore; + } + getLunrIndices() { + return this.lunrIndices; + } getTranslator() { return this.translator; } } +const indexerMock = { + on: jest.fn(), + buildIndex: jest.fn(), + getDocumentStore: jest.fn(), +}; +jest.mock('./LunrSearchEngineIndexer', () => ({ + LunrSearchEngineIndexer: jest.fn().mockImplementation(() => indexerMock), +})); + +const getActualIndexer = (engine: SearchEngine, index: string) => { + (LunrSearchEngineIndexer as unknown as jest.Mock).mockImplementationOnce( + () => { + const ActualIndexer = jest.requireActual( + './LunrSearchEngineIndexer', + ).LunrSearchEngineIndexer; + return new ActualIndexer(); + }, + ); + return engine.getIndexer(index); +}; + describe('LunrSearchEngine', () => { let testLunrSearchEngine: SearchEngine; beforeEach(() => { testLunrSearchEngine = new LunrSearchEngine({ logger: getVoidLogger() }); + jest.clearAllMocks(); }); describe('translator', () => { @@ -65,7 +98,7 @@ describe('LunrSearchEngine', () => { }); it('should return translated query', async () => { - const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + const inspectableSearchEngine = new LunrSearchEngineForTests({ logger: getVoidLogger(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -107,7 +140,7 @@ describe('LunrSearchEngine', () => { }); it('should have default offset and limit', async () => { - const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + const inspectableSearchEngine = new LunrSearchEngineForTests({ logger: getVoidLogger(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -148,7 +181,7 @@ describe('LunrSearchEngine', () => { }); it('should return translated query with 1 filter', async () => { - const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + const inspectableSearchEngine = new LunrSearchEngineForTests({ logger: getVoidLogger(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -193,7 +226,7 @@ describe('LunrSearchEngine', () => { }); it('should handle single-item array filter as scalar value', async () => { - const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + const inspectableSearchEngine = new LunrSearchEngineForTests({ logger: getVoidLogger(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -224,7 +257,7 @@ describe('LunrSearchEngine', () => { }); it('should return translated query with multiple filters', async () => { - const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + const inspectableSearchEngine = new LunrSearchEngineForTests({ logger: getVoidLogger(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -273,7 +306,7 @@ describe('LunrSearchEngine', () => { }); it('should throw if translated query references missing field', async () => { - const inspectableSearchEngine = new LunrSearchEngineForTranslatorTests({ + const inspectableSearchEngine = new LunrSearchEngineForTests({ logger: getVoidLogger(), }); const translatorUnderTest = inspectableSearchEngine.getTranslator(); @@ -334,7 +367,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -359,7 +398,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -392,7 +437,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -424,7 +475,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -456,7 +513,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -489,7 +552,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -522,7 +591,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 1 document - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -560,7 +635,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 2 documents - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -604,8 +685,21 @@ describe('LunrSearchEngine', () => { ]; // Mock 2 indices with 1 document each - await testLunrSearchEngine.index('test-index', mockDocuments); - await testLunrSearchEngine.index('test-index-2', mockDocuments2); + const indexer1 = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + const indexer2 = await getActualIndexer( + testLunrSearchEngine, + 'test-index-2', + ); + await TestPipeline.withSubject(indexer1) + .withDocuments(mockDocuments) + .execute(); + await TestPipeline.withSubject(indexer2) + .withDocuments(mockDocuments2) + .execute(); + // Perform search query scoped to "test-index-2" with a filter on the field "extraField" const mockedSearchResult = await testLunrSearchEngine.query({ term: 'testTitle', @@ -642,7 +736,13 @@ describe('LunrSearchEngine', () => { ]; // Mock indexing of 2 documents - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); // Perform search query const mockedSearchResult = await testLunrSearchEngine.query({ @@ -695,8 +795,20 @@ describe('LunrSearchEngine', () => { ]; // Mock 2 indices with 2 documents each - await testLunrSearchEngine.index('test-index', mockDocuments); - await testLunrSearchEngine.index('test-index-2', mockDocuments2); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); + const indexer2 = await getActualIndexer( + testLunrSearchEngine, + 'test-index-2', + ); + await TestPipeline.withSubject(indexer2) + .withDocuments(mockDocuments2) + .execute(); // Perform search query scoped to "test-index-2" const mockedSearchResult = await testLunrSearchEngine.query({ @@ -734,7 +846,13 @@ describe('LunrSearchEngine', () => { location: `test/location/${i}`, })); - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer( + testLunrSearchEngine, + 'test-index', + ); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); const mockedSearchResult = await testLunrSearchEngine.query({ term: 'testTitle', @@ -767,7 +885,10 @@ describe('LunrSearchEngine', () => { location: `test/location/${i}`, })); - await testLunrSearchEngine.index('test-index', mockDocuments); + const indexer = await getActualIndexer(testLunrSearchEngine, 'test-index'); + await TestPipeline.withSubject(indexer) + .withDocuments(mockDocuments) + .execute(); const mockedSearchResult = await testLunrSearchEngine.query({ term: 'testTitle', @@ -793,22 +914,46 @@ describe('LunrSearchEngine', () => { }); describe('index', () => { - it('should index document', async () => { - const indexSpy = jest.spyOn(testLunrSearchEngine, 'index'); - const mockDocuments = [ - { - title: 'testTerm', - text: 'testText', - location: 'test/location', - }, - ]; + it('should get indexer', async () => { + const indexer = await testLunrSearchEngine.getIndexer('test-index'); + expect(LunrSearchEngineIndexer).toHaveBeenCalled(); + expect(indexer.on).toHaveBeenCalledWith('close', expect.any(Function)); + }); - // call index func and ensure the index func was invoked. - await testLunrSearchEngine.index('test-index', mockDocuments); - expect(indexSpy).toHaveBeenCalled(); - expect(indexSpy).toHaveBeenCalledWith('test-index', [ - { title: 'testTerm', text: 'testText', location: 'test/location' }, - ]); + it('should manage indices and docs on close', async () => { + const doc = { title: 'A doc', text: 'test', location: 'some-location' }; + + // Set up an inspectable search engine to pre-set some data. + const inspectableSearchEngine = new LunrSearchEngineForTests({ + logger: getVoidLogger(), + }); + inspectableSearchEngine.setDocStore({ 'existing-location': doc }); + + // Mock methds called by close handler. + indexerMock.buildIndex.mockReturnValueOnce('expected-index'); + indexerMock.getDocumentStore.mockReturnValueOnce({ + 'new-location': doc, + }); + + // Get the indexer and invoke its close handler. + await inspectableSearchEngine.getIndexer('test-index'); + const onClose = indexerMock.on.mock.calls[0][1] as Function; + onClose(); + + // Ensure mocked methods were called. + expect(indexerMock.buildIndex).toHaveBeenCalled(); + expect(indexerMock.getDocumentStore).toHaveBeenCalled(); + + // Ensure the lunr index was written to the search engine. + expect(inspectableSearchEngine.getLunrIndices()).toStrictEqual({ + 'test-index': 'expected-index', + }); + + // Ensure documents are merged into the existing store. + expect(inspectableSearchEngine.getDocStore()).toStrictEqual({ + 'existing-location': doc, + 'new-location': doc, + }); }); }); }); diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index ea51ffe8e1..b2e131e56f 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -23,6 +23,7 @@ import { } from '@backstage/search-common'; import lunr from 'lunr'; import { Logger } from 'winston'; +import { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer'; export type ConcreteLunrQuery = { lunrQueryBuilder: lunr.Index.QueryBuilder; @@ -124,30 +125,17 @@ export class LunrSearchEngine implements SearchEngine { this.translator = translator; } - async index(type: string, documents: IndexableDocument[]): Promise { - const lunrBuilder = new lunr.Builder(); + async getIndexer(type: string) { + const indexer = new LunrSearchEngineIndexer(); - lunrBuilder.pipeline.add(lunr.trimmer, lunr.stopWordFilter, lunr.stemmer); - lunrBuilder.searchPipeline.add(lunr.stemmer); - - // Make this lunr index aware of all relevant fields. - Object.keys(documents[0]).forEach(field => { - lunrBuilder.field(field); + indexer.on('close', () => { + // Once the stream is closed, build the index and store the documents in + // memory for later retrieval. + this.lunrIndices[type] = indexer.buildIndex(); + this.docStore = { ...this.docStore, ...indexer.getDocumentStore() }; }); - // Set "location" field as reference field - lunrBuilder.ref('location'); - - documents.forEach((document: IndexableDocument) => { - // Add document to Lunar index - lunrBuilder.add(document); - // Store documents in memory to be able to look up document using the ref during query time - // This is not how you should implement your SearchEngine implementation! Do not copy! - this.docStore[document.location] = document; - }); - - // "Rotate" the index by simply overwriting any existing index of the same name. - this.lunrIndices[type] = lunrBuilder.build(); + return indexer; } async query(query: SearchQuery): Promise { diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.test.ts b/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.test.ts new file mode 100644 index 0000000000..fbb6b153d0 --- /dev/null +++ b/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.test.ts @@ -0,0 +1,109 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 lunr from 'lunr'; +import { range } from 'lodash'; +import { TestPipeline } from '../test-utils'; +import { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer'; + +const lunrBuilderAddSpy = jest.fn(); +const lunrBuilderRefSpy = jest.fn(); +const lunrBuilderFieldSpy = jest.fn(); +const lunrBuilderPipelineAddSpy = jest.fn(); +const lunrBuilderSearchPipelineAddSpy = jest.fn(); + +jest.mock('lunr', () => { + const actualLunr = jest.requireActual('lunr'); + return { + ...actualLunr, + Builder: jest.fn().mockImplementation(() => { + const actualBuilder = new actualLunr.Builder(); + actualBuilder.add = lunrBuilderAddSpy; + actualBuilder.ref = lunrBuilderRefSpy; + actualBuilder.field = lunrBuilderFieldSpy; + actualBuilder.pipeline.add = lunrBuilderPipelineAddSpy; + actualBuilder.searchPipeline.add = lunrBuilderSearchPipelineAddSpy; + return actualBuilder; + }), + }; +}); + +describe('LunrSearchEngineIndexer', () => { + let indexer: LunrSearchEngineIndexer; + + beforeEach(() => { + jest.clearAllMocks(); + indexer = new LunrSearchEngineIndexer(); + }); + + it('should index documents', async () => { + const documents = [ + { + title: 'testTerm', + text: 'testText', + location: 'test/location', + }, + ]; + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + expect(lunrBuilderAddSpy).toHaveBeenCalledWith(documents[0]); + }); + + it('should index documents in bulk', async () => { + const documents = range(350).map(i => ({ + title: `Hello World ${i}`, + text: 'Lorem Ipsum', + location: `location-${i}`, + })); + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + expect(lunrBuilderAddSpy).toHaveBeenCalledTimes(350); + }); + + it('should initialize schema', async () => { + const documents = [ + { + title: 'testTerm', + text: 'testText', + location: 'test/location', + extra: 'field', + }, + ]; + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + // Builder ref should be set to location (and only once). + expect(lunrBuilderRefSpy).toHaveBeenCalledTimes(1); + expect(lunrBuilderRefSpy).toHaveBeenLastCalledWith('location'); + + // Builder fields should be based on document fields. + expect(lunrBuilderFieldSpy).toHaveBeenCalledTimes(4); + expect(lunrBuilderFieldSpy).toHaveBeenCalledWith('title'); + expect(lunrBuilderFieldSpy).toHaveBeenCalledWith('text'); + expect(lunrBuilderFieldSpy).toHaveBeenCalledWith('location'); + expect(lunrBuilderFieldSpy).toHaveBeenCalledWith('extra'); + }); + + it('should configure lunr pipeline', async () => { + expect(lunrBuilderSearchPipelineAddSpy).toHaveBeenLastCalledWith( + lunr.stemmer, + ); + expect(lunrBuilderPipelineAddSpy).toHaveBeenCalledWith( + ...[lunr.trimmer, lunr.stopWordFilter, lunr.stemmer], + ); + }); +}); diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.ts b/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.ts new file mode 100644 index 0000000000..2454889745 --- /dev/null +++ b/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { IndexableDocument } from '@backstage/search-common'; +import lunr from 'lunr'; +import { BatchSearchEngineIndexer } from '../indexing'; + +export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer { + private schemaInitialized = false; + private builder: lunr.Builder; + private docStore: Record = {}; + + constructor() { + super({ batchSize: 100 }); + + this.builder = new lunr.Builder(); + this.builder.pipeline.add(lunr.trimmer, lunr.stopWordFilter, lunr.stemmer); + this.builder.searchPipeline.add(lunr.stemmer); + } + + // No async initialization required. + async initialize(): Promise {} + async finalize(): Promise {} + + async index(documents: IndexableDocument[]): Promise { + if (!this.schemaInitialized) { + // Make this lunr index aware of all relevant fields. + Object.keys(documents[0]).forEach(field => { + this.builder.field(field); + }); + + // Set "location" field as reference field + this.builder.ref('location'); + + this.schemaInitialized = true; + } + + documents.forEach(document => { + // Add document to Lunar index + this.builder.add(document); + + // Store documents in memory to be able to look up document using the ref during query time + // This is not how you should implement your SearchEngine implementation! Do not copy! + this.docStore[document.location] = document; + }); + } + + buildIndex() { + return this.builder.build(); + } + + getDocumentStore() { + return this.docStore; + } +} diff --git a/plugins/search-backend-node/src/engines/index.ts b/plugins/search-backend-node/src/engines/index.ts index 7e6fb86bd4..7b71873c64 100644 --- a/plugins/search-backend-node/src/engines/index.ts +++ b/plugins/search-backend-node/src/engines/index.ts @@ -16,3 +16,4 @@ export { LunrSearchEngine } from './LunrSearchEngine'; export type { ConcreteLunrQuery } from './LunrSearchEngine'; +export type { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer'; From a151cf2a886a9b2e72eca2b719c3bb66827799a9 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 26 Feb 2022 18:25:34 +0100 Subject: [PATCH 121/353] Finalize exports for search-backend-node module Signed-off-by: Eric Peterson --- plugins/search-backend-node/api-report.md | 78 +++++++++++++++++++++-- plugins/search-backend-node/package.json | 6 +- plugins/search-backend-node/src/index.ts | 3 + 3 files changed, 80 insertions(+), 7 deletions(-) diff --git a/plugins/search-backend-node/api-report.md b/plugins/search-backend-node/api-report.md index 1929009760..d4b94a932e 100644 --- a/plugins/search-backend-node/api-report.md +++ b/plugins/search-backend-node/api-report.md @@ -3,16 +3,50 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { DocumentCollator } from '@backstage/search-common'; -import { DocumentDecorator } from '@backstage/search-common'; +/// + +import { DocumentCollatorFactory } from '@backstage/search-common'; +import { DocumentDecoratorFactory } from '@backstage/search-common'; import { DocumentTypeInfo } from '@backstage/search-common'; import { IndexableDocument } from '@backstage/search-common'; import { Logger as Logger_2 } from 'winston'; import { default as lunr_2 } from 'lunr'; import { QueryTranslator } from '@backstage/search-common'; +import { Readable } from 'stream'; import { SearchEngine } from '@backstage/search-common'; import { SearchQuery } from '@backstage/search-common'; import { SearchResultSet } from '@backstage/search-common'; +import { Transform } from 'stream'; +import { Writable } from 'stream'; + +// Warning: (ae-missing-release-tag) "BatchSearchEngineIndexer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export abstract class BatchSearchEngineIndexer extends Writable { + constructor(options: BatchSearchEngineOptions); + abstract finalize(): Promise; + abstract index(documents: IndexableDocument[]): Promise; + abstract initialize(): Promise; +} + +// Warning: (ae-missing-release-tag) "BatchSearchEngineOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type BatchSearchEngineOptions = { + batchSize: number; +}; + +// Warning: (ae-missing-release-tag) "DecoratorBase" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export abstract class DecoratorBase extends Transform { + constructor(); + abstract decorate( + document: IndexableDocument, + ): Promise; + abstract finalize(): Promise; + abstract initialize(): Promise; +} // Warning: (ae-missing-release-tag) "IndexBuilder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -22,11 +56,11 @@ export class IndexBuilder { constructor({ logger, searchEngine }: IndexBuilderOptions); // Warning: (ae-forgotten-export) The symbol "RegisterCollatorParameters" needs to be exported by the entry point index.d.ts addCollator({ - collator, + factory, defaultRefreshIntervalSeconds, }: RegisterCollatorParameters): void; // Warning: (ae-forgotten-export) The symbol "RegisterDecoratorParameters" needs to be exported by the entry point index.d.ts - addDecorator({ decorator }: RegisterDecoratorParameters): void; + addDecorator({ factory }: RegisterDecoratorParameters): void; build(): Promise<{ scheduler: Scheduler; }>; @@ -44,7 +78,7 @@ export class LunrSearchEngine implements SearchEngine { // (undocumented) protected docStore: Record; // (undocumented) - index(type: string, documents: IndexableDocument[]): Promise; + getIndexer(type: string): Promise; // (undocumented) protected logger: Logger_2; // (undocumented) @@ -59,6 +93,23 @@ export class LunrSearchEngine implements SearchEngine { protected translator: QueryTranslator; } +// Warning: (ae-missing-release-tag) "LunrSearchEngineIndexer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer { + constructor(); + // (undocumented) + buildIndex(): lunr_2.Index; + // (undocumented) + finalize(): Promise; + // (undocumented) + getDocumentStore(): Record; + // (undocumented) + index(documents: IndexableDocument[]): Promise; + // (undocumented) + initialize(): Promise; +} + // Warning: (ae-missing-release-tag) "Scheduler" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -70,4 +121,21 @@ export class Scheduler { } export { SearchEngine }; + +// Warning: (ae-missing-release-tag) "TestPipeline" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export class TestPipeline { + execute(): Promise; + withDocuments(documents: IndexableDocument[]): TestPipeline; + static withSubject(subject: Readable | Transform | Writable): TestPipeline; +} + +// Warning: (ae-missing-release-tag) "TestPipelineResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type TestPipelineResult = { + error: unknown; + documents: IndexableDocument[]; +}; ``` diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index dd8ef5599a..2c644e357d 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -23,10 +23,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { + "@backstage/errors": "^0.2.2", "@backstage/search-common": "^0.2.4", - "winston": "^3.2.1", + "@types/lunr": "^2.3.3", + "lodash": "^4.17.21", "lunr": "^2.3.9", - "@types/lunr": "^2.3.3" + "winston": "^3.2.1" }, "devDependencies": { "@backstage/backend-common": "^0.11.0", diff --git a/plugins/search-backend-node/src/index.ts b/plugins/search-backend-node/src/index.ts index d49ef195f2..6ae716553c 100644 --- a/plugins/search-backend-node/src/index.ts +++ b/plugins/search-backend-node/src/index.ts @@ -23,6 +23,9 @@ export { IndexBuilder } from './IndexBuilder'; export { Scheduler } from './Scheduler'; export { LunrSearchEngine } from './engines'; +export type { LunrSearchEngineIndexer } from './engines'; +export * from './indexing'; +export * from './test-utils'; /** * @deprecated Import from @backstage/search-common instead From 0d995e0ddd165f99a29649b98fe86019da597850 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 26 Feb 2022 18:50:50 +0100 Subject: [PATCH 122/353] Update PGSearchEngine to be stream-based Signed-off-by: Eric Peterson --- .../search-backend-module-pg/api-report.md | 29 +++- .../src/PgSearchEngine/PgSearchEngine.test.ts | 58 +++---- .../src/PgSearchEngine/PgSearchEngine.ts | 23 +-- .../PgSearchEngineIndexer.test.ts | 150 ++++++++++++++++++ .../PgSearchEngine/PgSearchEngineIndexer.ts | 74 +++++++++ .../src/PgSearchEngine/index.ts | 4 + .../src/database/DatabaseDocumentStore.ts | 4 + .../src/database/types.ts | 1 + 8 files changed, 288 insertions(+), 55 deletions(-) create mode 100644 plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.test.ts create mode 100644 plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts diff --git a/plugins/search-backend-module-pg/api-report.md b/plugins/search-backend-module-pg/api-report.md index e2edfc291a..ba8fde5f17 100644 --- a/plugins/search-backend-module-pg/api-report.md +++ b/plugins/search-backend-module-pg/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; import { IndexableDocument } from '@backstage/search-common'; import { Knex } from 'knex'; import { PluginDatabaseManager } from '@backstage/backend-common'; @@ -28,6 +29,8 @@ export class DatabaseDocumentStore implements DatabaseStore { // (undocumented) static create(knex: Knex): Promise; // (undocumented) + getTransaction(): Promise; + // (undocumented) insertDocuments( tx: Knex.Transaction, type: string, @@ -55,6 +58,8 @@ export interface DatabaseStore { // (undocumented) completeInsert(tx: Knex.Transaction, type: string): Promise; // (undocumented) + getTransaction(): Promise; + // (undocumented) insertDocuments( tx: Knex.Transaction, type: string, @@ -81,7 +86,7 @@ export class PgSearchEngine implements SearchEngine { database: PluginDatabaseManager; }): Promise; // (undocumented) - index(type: string, documents: IndexableDocument[]): Promise; + getIndexer(type: string): Promise; // (undocumented) query(query: SearchQuery): Promise; // (undocumented) @@ -94,6 +99,28 @@ export class PgSearchEngine implements SearchEngine { translator(query: SearchQuery): ConcretePgSearchQuery; } +// Warning: (ae-missing-release-tag) "PgSearchEngineIndexer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class PgSearchEngineIndexer extends BatchSearchEngineIndexer { + constructor(options: PgSearchEngineIndexerOptions); + // (undocumented) + finalize(): Promise; + // (undocumented) + index(documents: IndexableDocument[]): Promise; + // (undocumented) + initialize(): Promise; +} + +// Warning: (ae-missing-release-tag) "PgSearchEngineIndexerOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PgSearchEngineIndexerOptions = { + batchSize: number; + type: string; + databaseStore: DatabaseStore; +}; + // Warning: (ae-missing-release-tag) "PgSearchQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts index 56a3a6e643..8618bed4c8 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { range } from 'lodash'; import { DatabaseStore } from '../database'; import { ConcretePgSearchQuery, @@ -21,6 +20,13 @@ import { encodePageCursor, PgSearchEngine, } from './PgSearchEngine'; +import { PgSearchEngineIndexer } from './PgSearchEngineIndexer'; + +jest.mock('./PgSearchEngineIndexer', () => ({ + PgSearchEngineIndexer: jest + .fn() + .mockImplementation(async () => 'the-expected-indexer'), +})); describe('PgSearchEngine', () => { const tx: any = {} as any; @@ -30,6 +36,7 @@ describe('PgSearchEngine', () => { beforeEach(() => { database = { transaction: jest.fn(), + getTransaction: jest.fn(), insertDocuments: jest.fn(), query: jest.fn(), completeInsert: jest.fn(), @@ -122,46 +129,21 @@ describe('PgSearchEngine', () => { }); }); - describe('insert', () => { - it('should insert documents', async () => { - database.transaction.mockImplementation(fn => fn(tx)); + describe('index', () => { + it('should instantiate indexer', async () => { + const indexer = await searchEngine.getIndexer('my-type'); - const documents = [ - { title: 'Hello World', text: 'Lorem Ipsum', location: 'location-1' }, - { - location: 'location-2', - text: 'Hello World', - title: 'Dolor sit amet', - }, - ]; - - await searchEngine.index('my-type', documents); - - expect(database.transaction).toHaveBeenCalledTimes(1); - expect(database.prepareInsert).toHaveBeenCalledTimes(1); - expect(database.insertDocuments).toHaveBeenCalledWith( - tx, - 'my-type', - documents, + // Indexer instantiated with expected args. + expect(PgSearchEngineIndexer).toHaveBeenCalledWith( + expect.objectContaining({ + batchSize: 100, + type: 'my-type', + databaseStore: database, + }), ); - expect(database.completeInsert).toHaveBeenCalledWith(tx, 'my-type'); - }); - it('should batch insert documents', async () => { - database.transaction.mockImplementation(fn => fn(tx)); - - const documents = range(350).map(i => ({ - title: `Hello World ${i}`, - text: 'Lorem Ipsum', - location: `location-${i}`, - })); - - await searchEngine.index('my-type', documents); - - expect(database.transaction).toHaveBeenCalledTimes(1); - expect(database.prepareInsert).toHaveBeenCalledTimes(1); - expect(database.insertDocuments).toBeCalledTimes(4); - expect(database.completeInsert).toHaveBeenCalledWith(tx, 'my-type'); + // Indexer is as expected. + expect(indexer).toBe('the-expected-indexer'); }); }); diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts index 6fd6c571a8..ac42007401 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts @@ -15,12 +15,8 @@ */ import { PluginDatabaseManager } from '@backstage/backend-common'; import { SearchEngine } from '@backstage/plugin-search-backend-node'; -import { - IndexableDocument, - SearchQuery, - SearchResultSet, -} from '@backstage/search-common'; -import { chunk } from 'lodash'; +import { SearchQuery, SearchResultSet } from '@backstage/search-common'; +import { PgSearchEngineIndexer } from './PgSearchEngineIndexer'; import { DatabaseDocumentStore, DatabaseStore, @@ -77,16 +73,11 @@ export class PgSearchEngine implements SearchEngine { this.translator = translator; } - async index(type: string, documents: IndexableDocument[]): Promise { - await this.databaseStore.transaction(async tx => { - await this.databaseStore.prepareInsert(tx); - - const batchSize = 100; - for (const documentBatch of chunk(documents, batchSize)) { - await this.databaseStore.insertDocuments(tx, type, documentBatch); - } - - await this.databaseStore.completeInsert(tx, type); + async getIndexer(type: string) { + return new PgSearchEngineIndexer({ + batchSize: 100, + type, + databaseStore: this.databaseStore, }); } diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.test.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.test.ts new file mode 100644 index 0000000000..1fc7e74fc4 --- /dev/null +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.test.ts @@ -0,0 +1,150 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { TestPipeline } from '@backstage/plugin-search-backend-node'; +import { range } from 'lodash'; +import { PgSearchEngineIndexer } from './PgSearchEngineIndexer'; +import { DatabaseStore } from '../database'; + +describe('PgSearchEngineIndexer', () => { + const tx = { + rollback: jest.fn(), + commit: jest.fn(), + } as any; + let database: jest.Mocked; + let indexer: PgSearchEngineIndexer; + + beforeEach(() => { + jest.clearAllMocks(); + database = { + transaction: jest.fn().mockImplementation(fn => fn(tx)), + getTransaction: jest.fn().mockReturnValue(tx), + insertDocuments: jest.fn(), + query: jest.fn(), + completeInsert: jest.fn(), + prepareInsert: jest.fn(), + }; + indexer = new PgSearchEngineIndexer({ + batchSize: 100, + type: 'my-type', + databaseStore: database, + }); + }); + + it('should insert documents', async () => { + const documents = [ + { title: 'Hello World', text: 'Lorem Ipsum', location: 'location-1' }, + { + location: 'location-2', + text: 'Hello World', + title: 'Dolor sit amet', + }, + ]; + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + expect(database.getTransaction).toHaveBeenCalledTimes(1); + expect(database.prepareInsert).toHaveBeenCalledTimes(1); + expect(database.insertDocuments).toHaveBeenCalledWith( + tx, + 'my-type', + documents, + ); + expect(database.completeInsert).toHaveBeenCalledWith(tx, 'my-type'); + expect(tx.commit).toHaveBeenCalled(); + }); + + it('should batch insert documents', async () => { + const documents = range(350).map(i => ({ + title: `Hello World ${i}`, + text: 'Lorem Ipsum', + location: `location-${i}`, + })); + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + expect(database.getTransaction).toHaveBeenCalledTimes(1); + expect(database.prepareInsert).toHaveBeenCalledTimes(1); + expect(database.insertDocuments).toBeCalledTimes(4); + expect(database.completeInsert).toHaveBeenCalledWith(tx, 'my-type'); + }); + + it('should close out stream and bubble up error on prepare', async () => { + const expectedError = new Error('Prepare error'); + const documents = [ + { + title: `Hello World`, + text: 'Lorem Ipsum', + location: `location`, + }, + ]; + + database.prepareInsert.mockRejectedValueOnce(expectedError); + const result = await TestPipeline.withSubject(indexer) + .withDocuments(documents) + .execute(); + + expect(database.getTransaction).toHaveBeenCalledTimes(1); + expect(database.insertDocuments).not.toHaveBeenCalled(); + expect(database.completeInsert).not.toHaveBeenCalled(); + expect(result.error).toBe(expectedError); + expect(tx.rollback).toHaveBeenCalledWith(expectedError); + }); + + it('should close tx and bubble up error on insert', async () => { + const expectedError = new Error('Index error'); + const documents = [ + { + title: `Hello World`, + text: 'Lorem Ipsum', + location: `location`, + }, + ]; + + database.insertDocuments.mockRejectedValueOnce(expectedError); + const result = await TestPipeline.withSubject(indexer) + .withDocuments(documents) + .execute(); + + expect(database.getTransaction).toHaveBeenCalledTimes(1); + expect(database.prepareInsert).toHaveBeenCalledTimes(1); + expect(database.completeInsert).not.toHaveBeenCalled(); + expect(result.error).toBe(expectedError); + expect(tx.rollback).toHaveBeenCalledWith(expectedError); + }); + + it('should close tx and bubble up error on completion', async () => { + const expectedError = new Error('Completion error'); + const documents = [ + { + title: `Hello World`, + text: 'Lorem Ipsum', + location: `location`, + }, + ]; + + database.completeInsert.mockRejectedValueOnce(expectedError); + const result = await TestPipeline.withSubject(indexer) + .withDocuments(documents) + .execute(); + + expect(database.getTransaction).toHaveBeenCalledTimes(1); + expect(database.prepareInsert).toHaveBeenCalledTimes(1); + expect(database.insertDocuments).toHaveBeenCalledTimes(1); + expect(database.completeInsert).toHaveBeenCalledTimes(1); + expect(result.error).toBe(expectedError); + expect(tx.rollback).toHaveBeenCalledWith(expectedError); + }); +}); diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts new file mode 100644 index 0000000000..53d040cb87 --- /dev/null +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; +import { IndexableDocument } from '@backstage/search-common'; +import { Knex } from 'knex'; +import { DatabaseStore } from '../database'; + +export type PgSearchEngineIndexerOptions = { + batchSize: number; + type: string; + databaseStore: DatabaseStore; +}; + +export class PgSearchEngineIndexer extends BatchSearchEngineIndexer { + private store: DatabaseStore; + private type: string; + private tx: Knex.Transaction | undefined; + + constructor(options: PgSearchEngineIndexerOptions) { + super({ batchSize: options.batchSize }); + this.store = options.databaseStore; + this.type = options.type; + } + + async initialize(): Promise { + this.tx = await this.store.getTransaction(); + try { + await this.store.prepareInsert(this.tx); + } catch (e) { + // In case of error, rollback the transaction and re-throw the error so + // that the stream can be closed and destroyed properly. + this.tx.rollback(e); + throw e; + } + } + + async index(documents: IndexableDocument[]): Promise { + try { + await this.store.insertDocuments(this.tx!, this.type, documents); + } catch (e) { + // In case of error, rollback the transaction and re-throw the error so + // that the stream can be closed and destroyed properly. + this.tx!.rollback(e); + throw e; + } + } + + async finalize(): Promise { + // Attempt to complete and commit the transaction. + try { + await this.store.completeInsert(this.tx!, this.type); + this.tx!.commit(); + } catch (e) { + // Otherwise, rollback the transaction and re-throw the error so that the + // stream can be closed and destroyed properly. + this.tx!.rollback!(e); + throw e; + } + } +} diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/index.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/index.ts index 7994998baf..7f8e297648 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/index.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/index.ts @@ -15,3 +15,7 @@ */ export { PgSearchEngine } from './PgSearchEngine'; export type { ConcretePgSearchQuery } from './PgSearchEngine'; +export type { + PgSearchEngineIndexer, + PgSearchEngineIndexerOptions, +} from './PgSearchEngineIndexer'; diff --git a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts index 8c180ea019..0d3ee63ca4 100644 --- a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts +++ b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts @@ -71,6 +71,10 @@ export class DatabaseDocumentStore implements DatabaseStore { return await this.db.transaction(fn); } + async getTransaction(): Promise { + return this.db.transaction(); + } + async prepareInsert(tx: Knex.Transaction): Promise { // We create a temporary table to collect the hashes of the documents that // we expect to be in the documents table at the end. The table is deleted diff --git a/plugins/search-backend-module-pg/src/database/types.ts b/plugins/search-backend-module-pg/src/database/types.ts index 0c0596160e..0a0dc35682 100644 --- a/plugins/search-backend-module-pg/src/database/types.ts +++ b/plugins/search-backend-module-pg/src/database/types.ts @@ -26,6 +26,7 @@ export interface PgSearchQuery { export interface DatabaseStore { transaction(fn: (tx: Knex.Transaction) => Promise): Promise; + getTransaction(): Promise; prepareInsert(tx: Knex.Transaction): Promise; insertDocuments( tx: Knex.Transaction, From 2c171166a07663f9572b7e3914d2dc4a121df10e Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 26 Feb 2022 19:03:35 +0100 Subject: [PATCH 123/353] Update ElasticSearchSearchEngine to be stream-based Signed-off-by: Eric Peterson --- .../api-report.md | 31 ++- .../package.json | 1 + .../engines/ElasticSearchSearchEngine.test.ts | 91 ++++++-- .../src/engines/ElasticSearchSearchEngine.ts | 94 +++----- .../ElasticSearchSearchEngineIndexer.test.ts | 211 ++++++++++++++++++ .../ElasticSearchSearchEngineIndexer.ts | 176 +++++++++++++++ .../src/engines/index.ts | 4 + .../src/index.ts | 6 +- 8 files changed, 527 insertions(+), 87 deletions(-) create mode 100644 plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts create mode 100644 plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts diff --git a/plugins/search-backend-module-elasticsearch/api-report.md b/plugins/search-backend-module-elasticsearch/api-report.md index 6dc0256586..d4edcc6b25 100644 --- a/plugins/search-backend-module-elasticsearch/api-report.md +++ b/plugins/search-backend-module-elasticsearch/api-report.md @@ -5,6 +5,8 @@ ```ts /// +import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; +import { Client } from '@elastic/elasticsearch'; import { Config } from '@backstage/config'; import type { ConnectionOptions } from 'tls'; import { IndexableDocument } from '@backstage/search-common'; @@ -114,7 +116,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { indexPrefix, }: ElasticSearchOptions): Promise; // (undocumented) - index(type: string, documents: IndexableDocument[]): Promise; + getIndexer(type: string): Promise; newClient(create: (options: ElasticSearchClientOptions) => T): T; // (undocumented) query(query: SearchQuery): Promise; @@ -127,4 +129,31 @@ export class ElasticSearchSearchEngine implements SearchEngine { // (undocumented) protected translator(query: SearchQuery): ConcreteElasticSearchQuery; } + +// Warning: (ae-missing-release-tag) "ElasticSearchSearchEngineIndexer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { + constructor(options: ElasticSearchSearchEngineIndexerOptions); + // (undocumented) + finalize(): Promise; + // (undocumented) + index(documents: IndexableDocument[]): Promise; + // (undocumented) + readonly indexName: string; + // (undocumented) + initialize(): Promise; +} + +// Warning: (ae-missing-release-tag) "ElasticSearchSearchEngineIndexerOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ElasticSearchSearchEngineIndexerOptions = { + type: string; + indexPrefix: string; + indexSeparator: string; + alias: string; + logger: Logger_2; + elasticSearchClient: Client; +}; ``` diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 2a2dbd6f85..0a279d6b4c 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -25,6 +25,7 @@ "dependencies": { "@backstage/config": "^0.1.15", "@backstage/search-common": "^0.2.4", + "@backstage/plugin-search-backend-node": "^0.4.7", "@elastic/elasticsearch": "7.13.0", "@acuris/aws-es-connection": "^2.2.0", "aws-sdk": "^2.948.0", diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts index 1566a35b57..467e1b4e77 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts @@ -15,7 +15,8 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { Client } from '@elastic/elasticsearch'; +import { ConfigReader } from '@backstage/config'; +import { Client, errors } from '@elastic/elasticsearch'; import Mock from '@elastic/elasticsearch-mock'; import { ConcreteElasticSearchQuery, @@ -23,7 +24,7 @@ import { ElasticSearchSearchEngine, encodePageCursor, } from './ElasticSearchSearchEngine'; -import { ConfigReader } from '@backstage/config'; +import { ElasticSearchSearchEngineIndexer } from './ElasticSearchSearchEngineIndexer'; class ElasticSearchSearchEngineForTranslatorTests extends ElasticSearchSearchEngine { getTranslator() { @@ -37,6 +38,16 @@ const options = { Connection: mock.getConnection(), }; +const indexerMock = { + on: jest.fn(), + indexName: 'expected-index-name', +}; +jest.mock('./ElasticSearchSearchEngineIndexer', () => ({ + ElasticSearchSearchEngineIndexer: jest + .fn() + .mockImplementation(() => indexerMock), +})); + describe('ElasticSearchSearchEngine', () => { let testSearchEngine: ElasticSearchSearchEngine; let inspectableSearchEngine: ElasticSearchSearchEngineForTranslatorTests; @@ -542,23 +553,67 @@ describe('ElasticSearchSearchEngine', () => { }); }); - describe('index', () => { - it('should index document', async () => { - const indexSpy = jest.spyOn(testSearchEngine, 'index'); - const mockDocuments = [ - { - title: 'testTerm', - text: 'testText', - location: 'test/location', - }, - ]; + describe('indexer', () => { + it('should get indexer', async () => { + const indexer = await testSearchEngine.getIndexer('test-index'); - // call index func and ensure the index func was invoked. - await testSearchEngine.index('test-index', mockDocuments); - expect(indexSpy).toHaveBeenCalled(); - expect(indexSpy).toHaveBeenCalledWith('test-index', [ - { title: 'testTerm', text: 'testText', location: 'test/location' }, - ]); + expect(indexer).toStrictEqual(indexerMock); + expect(ElasticSearchSearchEngineIndexer).toHaveBeenCalledWith( + expect.objectContaining({ + alias: 'test-index__search', + type: 'test-index', + indexPrefix: '', + indexSeparator: '-index__', + elasticSearchClient: client, + }), + ); + expect(indexerMock.on).toHaveBeenCalledWith( + 'error', + expect.any(Function), + ); + }); + + describe('onError', () => { + let errorHandler: Function; + const error = new Error('some error'); + + beforeEach(async () => { + mock.clearAll(); + await testSearchEngine.getIndexer('test-index'); + errorHandler = indexerMock.on.mock.calls[0][1]; + }); + + it('should check for and delete expected index', async () => { + const existsSpy = jest.fn().mockReturnValue('truthy value'); + const deleteSpy = jest.fn().mockReturnValue({}); + mock.add({ method: 'HEAD', path: '/expected-index-name' }, existsSpy); + mock.add({ method: 'DELETE', path: '/expected-index-name' }, deleteSpy); + + await errorHandler(error); + + // Check and delete HTTP requests were made. + expect(existsSpy).toHaveBeenCalled(); + expect(deleteSpy).toHaveBeenCalled(); + }); + + it('should not delete index if none exists', async () => { + // Exists call returns 404 on no index. + const existsSpy = jest.fn().mockReturnValue( + new errors.ResponseError({ + statusCode: 404, + body: { status: 404 }, + } as unknown as any), + ); + const deleteSpy = jest.fn().mockReturnValue({}); + mock.add({ method: 'HEAD', path: '/expected-index-name' }, existsSpy); + mock.add({ method: 'DELETE', path: '/expected-index-name' }, deleteSpy); + + await errorHandler(error); + + // Check request was made, but no delete request was made. + expect(existsSpy).toHaveBeenCalled(); + expect(deleteSpy).not.toHaveBeenCalled(); + }); }); }); diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index 48caf0d4f1..c16567fee0 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -29,8 +29,8 @@ import { Client } from '@elastic/elasticsearch'; import esb from 'elastic-builder'; import { isEmpty, isNaN as nan, isNumber } from 'lodash'; import { Logger } from 'winston'; - import type { ElasticSearchClientOptions } from './ElasticSearchClientOptions'; +import { ElasticSearchSearchEngineIndexer } from './ElasticSearchSearchEngineIndexer'; export type { ElasticSearchClientOptions }; @@ -58,12 +58,6 @@ type ElasticSearchResult = { _source: IndexableDocument; }; -function duration(startTimestamp: [number, number]): string { - const delta = process.hrtime(startTimestamp); - const seconds = delta[0] + delta[1] / 1e9; - return `${seconds.toFixed(1)}s`; -} - function isBlank(str: string) { return (isEmpty(str) && !isNumber(str)) || nan(str); } @@ -165,67 +159,37 @@ export class ElasticSearchSearchEngine implements SearchEngine { this.translator = translator; } - async index(type: string, documents: IndexableDocument[]): Promise { - this.logger.info( - `Started indexing ${documents.length} documents for index ${type}`, - ); - const startTimestamp = process.hrtime(); + async getIndexer(type: string) { const alias = this.constructSearchAlias(type); - const index = this.constructIndexName(type, `${Date.now()}`); - try { - const aliases = await this.elasticSearchClient.cat.aliases({ - format: 'json', - name: alias, - }); - const removableIndices = aliases.body.map( - (r: Record) => r.index, - ); + const indexer = new ElasticSearchSearchEngineIndexer({ + type, + indexPrefix: this.indexPrefix, + indexSeparator: this.indexSeparator, + alias, + elasticSearchClient: this.elasticSearchClient, + logger: this.logger, + }); - await this.elasticSearchClient.indices.create({ - index, - }); - const result = await this.elasticSearchClient.helpers.bulk({ - datasource: documents, - onDocument() { - return { - index: { _index: index }, - }; - }, - refreshOnCompletion: index, - }); - - this.logger.info( - `Indexing completed for index ${type} in ${duration(startTimestamp)}`, - result, - ); - await this.elasticSearchClient.indices.updateAliases({ - body: { - actions: [ - { remove: { index: this.constructIndexName(type, '*'), alias } }, - { add: { index, alias } }, - ], - }, - }); - - this.logger.info('Removing stale search indices', removableIndices); - if (removableIndices.length) { - await this.elasticSearchClient.indices.delete({ - index: removableIndices, - }); - } - } catch (e) { + // Attempt cleanup upon failure. + indexer.on('error', async e => { this.logger.error(`Failed to index documents for type ${type}`, e); - const response = await this.elasticSearchClient.indices.exists({ - index, - }); - const indexCreated = response.body; - if (indexCreated) { - this.logger.info(`Removing created index ${index}`); - await this.elasticSearchClient.indices.delete({ - index, + try { + const response = await this.elasticSearchClient.indices.exists({ + index: indexer.indexName, }); + const indexCreated = response.body; + if (indexCreated) { + this.logger.info(`Removing created index ${indexer.indexName}`); + await this.elasticSearchClient.indices.delete({ + index: indexer.indexName, + }); + } + } catch (error) { + this.logger.error(`Unable to clean up elastic index: ${error}`); } - } + }); + + return indexer; } async query(query: SearchQuery): Promise { @@ -268,10 +232,6 @@ export class ElasticSearchSearchEngine implements SearchEngine { private readonly indexSeparator = '-index__'; - private constructIndexName(type: string, postFix: string) { - return `${this.indexPrefix}${type}${this.indexSeparator}${postFix}`; - } - private getTypeFromIndex(index: string) { return index .substring(this.indexPrefix.length) diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts new file mode 100644 index 0000000000..0867887d9a --- /dev/null +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts @@ -0,0 +1,211 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { getVoidLogger } from '@backstage/backend-common'; +import { TestPipeline } from '@backstage/plugin-search-backend-node'; +import { Client } from '@elastic/elasticsearch'; +import Mock from '@elastic/elasticsearch-mock'; +import { range } from 'lodash'; +import { ElasticSearchSearchEngineIndexer } from './ElasticSearchSearchEngineIndexer'; + +const mock = new Mock(); +const client = new Client({ + node: 'http://localhost:9200', + Connection: mock.getConnection(), +}); + +describe('ElasticSearchSearchEngineIndexer', () => { + let indexer: ElasticSearchSearchEngineIndexer; + let bulkSpy: jest.Mock; + let catSpy: jest.Mock; + let createSpy: jest.Mock; + let aliasesSpy: jest.Mock; + let deleteSpy: jest.Mock; + + beforeEach(() => { + // Instantiate the indexer to be tested. + indexer = new ElasticSearchSearchEngineIndexer({ + type: 'some-type', + indexPrefix: '', + indexSeparator: '-index__', + alias: 'some-type-index__search', + logger: getVoidLogger(), + elasticSearchClient: client, + }); + + // Set up all requisite Elastic mocks. + mock.clearAll(); + bulkSpy = jest.fn().mockReturnValue({ took: 9, errors: false, items: [] }); + mock.add( + { + method: 'POST', + path: '/_bulk', + }, + bulkSpy, + ); + mock.add( + { + method: 'GET', + path: '/:index/_refresh', + }, + jest.fn().mockReturnValue({}), + ); + + catSpy = jest.fn().mockReturnValue([ + { + alias: 'some-type-index__search', + index: 'some-type-index__123tobedeleted', + filter: '-', + 'routing.index': '-', + 'routing.search': '-', + is_write_index: '-', + }, + ]); + mock.add( + { + method: 'GET', + path: '/_cat/aliases/some-type-index__search', + }, + catSpy, + ); + + createSpy = jest.fn().mockReturnValue({ + acknowledged: true, + shards_acknowledged: true, + index: 'single_index', + }); + mock.add( + { + method: 'PUT', + path: '/:index', + }, + createSpy, + ); + + aliasesSpy = jest.fn().mockReturnValue({}); + mock.add( + { + method: 'POST', + path: '*', + }, + aliasesSpy, + ); + + deleteSpy = jest.fn().mockReturnValue({}); + mock.add( + { + method: 'DELETE', + path: '/some-type-index__123tobedeleted', + }, + deleteSpy, + ); + }); + + it('indexes documents', async () => { + const documents = [ + { + title: 'testTerm', + text: 'testText', + location: 'test/location', + }, + { + title: 'Another test', + text: 'Some more text', + location: 'test/location/2', + }, + ]; + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + // Older indices should have been queried for. + expect(catSpy).toHaveBeenCalled(); + + // A new index should have been created. + const createdIndex = createSpy.mock.calls[0][0].path.slice(1); + expect(createdIndex).toContain('some-type-index__'); + + // Bulk helper should have been called with documents. + const bulkBody = bulkSpy.mock.calls[0][0].body; + expect(bulkBody[0]).toStrictEqual({ index: { _index: createdIndex } }); + expect(bulkBody[1]).toStrictEqual(documents[0]); + expect(bulkBody[2]).toStrictEqual({ index: { _index: createdIndex } }); + expect(bulkBody[3]).toStrictEqual(documents[1]); + + // Alias should have been rotated. + expect(aliasesSpy).toHaveBeenCalled(); + const aliasActions = aliasesSpy.mock.calls[0][0].body.actions; + expect(aliasActions[0]).toStrictEqual({ + remove: { index: 'some-type-index__*', alias: 'some-type-index__search' }, + }); + expect(aliasActions[1]).toStrictEqual({ + add: { index: createdIndex, alias: 'some-type-index__search' }, + }); + + // Old index should be cleaned up. + expect(deleteSpy).toHaveBeenCalled(); + }); + + it('handles bulk and batching during indexing', async () => { + const documents = range(550).map(i => ({ + title: `Hello World ${i}`, + location: `location-${i}`, + // Generate large document sizes to trigger ES bulk flushing. + text: range(2000).join(', '), + })); + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + // Ensure multiple bulk requests were made. + expect(bulkSpy).toHaveBeenCalledTimes(2); + + // Ensure the first and last documents were included in the payloads. + const docLocations: string[] = [ + ...bulkSpy.mock.calls[0][0].body.map((l: any) => l.location), + ...bulkSpy.mock.calls[1][0].body.map((l: any) => l.location), + ]; + expect(docLocations).toContain('location-0'); + expect(docLocations).toContain('location-549'); + }); + + it('ignores cleanup when no existing indices exist', async () => { + const documents = [ + { + title: 'testTerm', + text: 'testText', + location: 'test/location', + }, + ]; + + // Update initial alias cat to return nothing. + catSpy = jest.fn().mockReturnValue([]); + mock.clear({ + method: 'GET', + path: '/_cat/aliases/some-type-index__search', + }); + mock.add( + { + method: 'GET', + path: '/_cat/aliases/some-type-index__search', + }, + catSpy, + ); + + await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + + // Final deletion shouldn't be called. + expect(deleteSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts new file mode 100644 index 0000000000..2e4996cd2f --- /dev/null +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts @@ -0,0 +1,176 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; +import { IndexableDocument } from '@backstage/search-common'; +import { Client } from '@elastic/elasticsearch'; +import { Readable } from 'stream'; +import { Logger } from 'winston'; + +export type ElasticSearchSearchEngineIndexerOptions = { + type: string; + indexPrefix: string; + indexSeparator: string; + alias: string; + logger: Logger; + elasticSearchClient: Client; +}; + +function duration(startTimestamp: [number, number]): string { + const delta = process.hrtime(startTimestamp); + const seconds = delta[0] + delta[1] / 1e9; + return `${seconds.toFixed(1)}s`; +} + +export class ElasticSearchSearchEngineIndexer extends BatchSearchEngineIndexer { + private received: number = 0; + private processed: number = 0; + private removableIndices: string[] = []; + + private readonly startTimestamp: [number, number]; + private readonly type: string; + public readonly indexName: string; + private readonly indexPrefix: string; + private readonly indexSeparator: string; + private readonly alias: string; + private readonly logger: Logger; + private readonly sourceStream: Readable; + private readonly elasticSearchClient: Client; + private bulkResult: Promise; + + constructor(options: ElasticSearchSearchEngineIndexerOptions) { + super({ batchSize: 100 }); + this.logger = options.logger; + this.startTimestamp = process.hrtime(); + this.type = options.type; + this.indexPrefix = options.indexPrefix; + this.indexSeparator = options.indexSeparator; + this.indexName = this.constructIndexName(`${Date.now()}`); + this.alias = options.alias; + this.elasticSearchClient = options.elasticSearchClient; + + // The ES client bulk helper supports stream-based indexing, but we have to + // supply the stream directly to it at instantiation-time. We can't supply + // this class itself, so instead, we create this inline stream instead. + this.sourceStream = new Readable({ objectMode: true }); + this.sourceStream._read = () => {}; + + // eslint-disable-next-line consistent-this + const that = this; + + // Keep a reference to the ES Bulk helper so that we can know when all + // documents have been successfully written to ES. + this.bulkResult = this.elasticSearchClient.helpers.bulk({ + datasource: this.sourceStream, + onDocument() { + that.processed++; + return { + index: { _index: that.indexName }, + }; + }, + refreshOnCompletion: that.indexName, + }); + } + + async initialize(): Promise { + this.logger.info(`Started indexing documents for index ${this.type}`); + + const aliases = await this.elasticSearchClient.cat.aliases({ + format: 'json', + name: this.alias, + }); + + this.removableIndices = aliases.body.map( + (r: Record) => r.index, + ); + + await this.elasticSearchClient.indices.create({ + index: this.indexName, + }); + } + + async index(documents: IndexableDocument[]): Promise { + await this.isReady(); + documents.forEach(document => { + this.received++; + this.sourceStream.push(document); + }); + } + + async finalize(): Promise { + // Wait for all documents to be processed. + await this.isReady(); + + // Close off the underlying stream connected to ES, indicating that no more + // documents will be written. + this.sourceStream.push(null); + + // Wait for the bulk helper to finish processing. + const result = await this.bulkResult; + + // Rotate aliases upon completion. Allow errors to bubble up so that we can + // clean up the create index. + this.logger.info( + `Indexing completed for index ${this.type} in ${duration( + this.startTimestamp, + )}`, + result, + ); + await this.elasticSearchClient.indices.updateAliases({ + body: { + actions: [ + { + remove: { index: this.constructIndexName('*'), alias: this.alias }, + }, + { add: { index: this.indexName, alias: this.alias } }, + ], + }, + }); + + // If any indices are removable, remove them. Do not bubble up this error, + // as doing so would delete the now aliased index. Log instead. + if (this.removableIndices.length) { + this.logger.info('Removing stale search indices', this.removableIndices); + try { + await this.elasticSearchClient.indices.delete({ + index: this.removableIndices, + }); + } catch (e) { + this.logger.warn(`Failed to remove stale search indices: ${e}`); + } + } + } + + /** + * Ensures that the number of documents sent over the wire to ES matches the + * number of documents this stream has received so far. This helps manage + * backpressure in other parts of the indexing pipeline. + */ + private isReady(): Promise { + return new Promise(resolve => { + const interval = setInterval(() => { + if (this.received === this.processed) { + clearInterval(interval); + resolve(); + } + }, 50); + }); + } + + private constructIndexName(postFix: string) { + return `${this.indexPrefix}${this.type}${this.indexSeparator}${postFix}`; + } +} diff --git a/plugins/search-backend-module-elasticsearch/src/engines/index.ts b/plugins/search-backend-module-elasticsearch/src/engines/index.ts index d5eee37803..19c24b37ff 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/index.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/index.ts @@ -19,3 +19,7 @@ export type { ConcreteElasticSearchQuery, ElasticSearchClientOptions, } from './ElasticSearchSearchEngine'; +export type { + ElasticSearchSearchEngineIndexer, + ElasticSearchSearchEngineIndexerOptions, +} from './ElasticSearchSearchEngineIndexer'; diff --git a/plugins/search-backend-module-elasticsearch/src/index.ts b/plugins/search-backend-module-elasticsearch/src/index.ts index 8cf96de858..223141be6b 100644 --- a/plugins/search-backend-module-elasticsearch/src/index.ts +++ b/plugins/search-backend-module-elasticsearch/src/index.ts @@ -21,4 +21,8 @@ */ export { ElasticSearchSearchEngine } from './engines'; -export type { ElasticSearchClientOptions } from './engines'; +export type { + ElasticSearchClientOptions, + ElasticSearchSearchEngineIndexer, + ElasticSearchSearchEngineIndexerOptions, +} from './engines'; From 45f68efd405d42f6d3dcd02bf634ef99b5acb3b2 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 26 Feb 2022 19:35:34 +0100 Subject: [PATCH 124/353] Update Catalog Collator to be stream-based Signed-off-by: Eric Peterson --- plugins/catalog-backend/api-report.md | 32 ++- plugins/catalog-backend/package.json | 1 + .../src/search/DefaultCatalogCollator.ts | 19 +- .../DefaultCatalogCollatorFactory.test.ts | 213 ++++++++++++++++++ .../search/DefaultCatalogCollatorFactory.ts | 173 ++++++++++++++ plugins/catalog-backend/src/search/index.ts | 8 +- 6 files changed, 430 insertions(+), 16 deletions(-) create mode 100644 plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts create mode 100644 plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 6f0147a1f4..d5541678e6 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -10,7 +10,7 @@ import { CatalogApi } from '@backstage/catalog-client'; import { ConditionalPolicyDecision } from '@backstage/plugin-permission-node'; import { Conditions } from '@backstage/plugin-permission-node'; import { Config } from '@backstage/config'; -import { DocumentCollator } from '@backstage/search-common'; +import { DocumentCollatorFactory } from '@backstage/search-common'; import { Entity } from '@backstage/catalog-model'; import { EntityName } from '@backstage/catalog-model'; import { EntityPolicy } from '@backstage/catalog-model'; @@ -30,6 +30,7 @@ import { PermissionCriteria } from '@backstage/plugin-permission-common'; import { PermissionRule } from '@backstage/plugin-permission-node'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { Readable } from 'stream'; import { Router } from 'express'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { TokenManager } from '@backstage/backend-common'; @@ -415,8 +416,8 @@ export function createRandomRefreshInterval(options: { // @public export function createRouter(options: RouterOptions): Promise; -// @public (undocumented) -export class DefaultCatalogCollator implements DocumentCollator { +// @public @deprecated (undocumented) +export class DefaultCatalogCollator { constructor(options: { discovery: PluginEndpointDiscovery; tokenManager: TokenManager; @@ -456,6 +457,31 @@ export class DefaultCatalogCollator implements DocumentCollator { readonly visibilityPermission: Permission; } +// @public (undocumented) +export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { + // (undocumented) + static fromConfig( + _config: Config, + options: DefaultCatalogCollatorFactoryOptions, + ): DefaultCatalogCollatorFactory; + // (undocumented) + getCollator(): Promise; + // (undocumented) + readonly type: string; + // (undocumented) + readonly visibilityPermission: Permission; +} + +// @public (undocumented) +export type DefaultCatalogCollatorFactoryOptions = { + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + locationTemplate?: string; + filter?: GetEntitiesRequest['filter']; + batchSize?: number; + catalogClient?: CatalogApi; +}; + // @public (undocumented) export class DefaultCatalogProcessingOrchestrator implements CatalogProcessingOrchestrator diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index be0772e199..f7d946c531 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -73,6 +73,7 @@ "@backstage/backend-test-utils": "^0.1.19", "@backstage/cli": "^0.14.1", "@backstage/plugin-permission-common": "^0.5.1", + "@backstage/plugin-search-backend-node": "0.4.7", "@backstage/test-utils": "^0.2.6", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts index 96704a6c1a..c59318d0af 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts @@ -23,7 +23,6 @@ import { stringifyEntityRef, UserEntity, } from '@backstage/catalog-model'; -import { IndexableDocument, DocumentCollator } from '@backstage/search-common'; import { Config } from '@backstage/config'; import { CatalogApi, @@ -31,18 +30,14 @@ import { GetEntitiesRequest, } from '@backstage/catalog-client'; import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common'; +import { CatalogEntityDocument } from './DefaultCatalogCollatorFactory'; -/** @public */ -export interface CatalogEntityDocument extends IndexableDocument { - componentType: string; - namespace: string; - kind: string; - lifecycle: string; - owner: string; -} - -/** @public */ -export class DefaultCatalogCollator implements DocumentCollator { +/** + * @public + * @deprecated Upgrade to a more recent `@backstage/search-backend-node` and + * use `DefaultCatalogCollatorFactory` instead. + */ +export class DefaultCatalogCollator { protected discovery: PluginEndpointDiscovery; protected locationTemplate: string; protected filter?: GetEntitiesRequest['filter']; diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts new file mode 100644 index 0000000000..ccb61b8680 --- /dev/null +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts @@ -0,0 +1,213 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import { TestPipeline } from '@backstage/plugin-search-backend-node'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { Readable } from 'stream'; +import { DefaultCatalogCollatorFactory } from './DefaultCatalogCollatorFactory'; + +const server = setupServer(); + +const expectedEntities: Entity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test-entity', + description: 'The expected description', + }, + spec: { + type: 'some-type', + lifecycle: 'experimental', + owner: 'someone', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + title: 'Test Entity', + name: 'test-entity-2', + description: 'The expected description 2', + }, + spec: { + type: 'some-type', + lifecycle: 'experimental', + owner: 'someone', + }, + }, +]; + +describe('DefaultCatalogCollatorFactory', () => { + const config = new ConfigReader({}); + const mockDiscoveryApi: jest.Mocked = { + getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7007'), + getExternalBaseUrl: jest.fn(), + }; + const mockTokenManager: jest.Mocked = { + getToken: jest.fn().mockResolvedValue({ token: '' }), + authenticate: jest.fn(), + }; + const options = { + discovery: mockDiscoveryApi, + tokenManager: mockTokenManager, + }; + + beforeAll(() => { + server.listen(); + }); + + beforeEach(() => { + server.use( + rest.get('http://localhost:7007/entities', (req, res, ctx) => { + if (req.url.searchParams.has('filter')) { + const filter = req.url.searchParams.get('filter'); + if (filter === 'kind=Foo,kind=Bar') { + // When filtering on the 'Foo,Bar' kinds we simply return no items, to simulate a filter + return res(ctx.json([])); + } + throw new Error('Unexpected filter parameter'); + } + + // Imitate offset/limit pagination. + const offset = parseInt(req.url.searchParams.get('offset') || '0', 10); + const limit = parseInt(req.url.searchParams.get('limit') || '500', 10); + return res(ctx.json(expectedEntities.slice(offset, limit + offset))); + }), + ); + }); + + afterAll(() => { + server.close(); + }); + + afterEach(() => server.resetHandlers()); + + it('has expected type', () => { + const factory = DefaultCatalogCollatorFactory.fromConfig(config, options); + expect(factory.type).toBe('software-catalog'); + }); + + describe('getCollator', () => { + let factory: DefaultCatalogCollatorFactory; + let collator: Readable; + + beforeEach(async () => { + factory = DefaultCatalogCollatorFactory.fromConfig(config, options); + collator = await factory.getCollator(); + }); + + it('returns a readable stream', async () => { + expect(collator).toBeInstanceOf(Readable); + }); + + it('fetches from the configured catalog service', async () => { + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + expect(mockDiscoveryApi.getBaseUrl).toHaveBeenCalledWith('catalog'); + expect(documents).toHaveLength(expectedEntities.length); + }); + + it('maps a returned entity to an expected CatalogEntityDocument', async () => { + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + + expect(documents[0]).toMatchObject({ + title: expectedEntities[0].metadata.name, + location: '/catalog/default/component/test-entity', + text: expectedEntities[0].metadata.description, + namespace: 'default', + componentType: expectedEntities[0]!.spec!.type, + lifecycle: expectedEntities[0]!.spec!.lifecycle, + owner: expectedEntities[0]!.spec!.owner, + authorization: { + resourceRef: 'component:default/test-entity', + }, + }); + expect(documents[1]).toMatchObject({ + title: expectedEntities[1].metadata.title, + location: '/catalog/default/component/test-entity-2', + text: expectedEntities[1].metadata.description, + namespace: 'default', + componentType: expectedEntities[1]!.spec!.type, + lifecycle: expectedEntities[1]!.spec!.lifecycle, + owner: expectedEntities[1]!.spec!.owner, + authorization: { + resourceRef: 'component:default/test-entity-2', + }, + }); + }); + + it('maps a returned entity with a custom locationTemplate', async () => { + // Provide an alternate location template. + factory = DefaultCatalogCollatorFactory.fromConfig(new ConfigReader({}), { + discovery: mockDiscoveryApi, + tokenManager: mockTokenManager, + locationTemplate: '/software/:name', + }); + collator = await factory.getCollator(); + + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + expect(documents[0]).toMatchObject({ + location: '/software/test-entity', + }); + }); + + it('allows filtering of the retrieved catalog entities', async () => { + // Provide a custom filter. + factory = DefaultCatalogCollatorFactory.fromConfig(new ConfigReader({}), { + discovery: mockDiscoveryApi, + tokenManager: mockTokenManager, + filter: { + kind: ['Foo', 'Bar'], + }, + }); + collator = await factory.getCollator(); + + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + + // The simulated 'Foo,Bar' filter should return in an empty list + expect(documents).toHaveLength(0); + }); + + it('paginates through catalog entities using batchSize', async () => { + factory = DefaultCatalogCollatorFactory.fromConfig(config, { + ...options, + batchSize: 1, + }); + collator = await factory.getCollator(); + + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + + expect(documents).toHaveLength(expectedEntities.length); + expect(documents[0].location).toBe( + '/catalog/default/component/test-entity', + ); + expect(documents[1].location).toBe( + '/catalog/default/component/test-entity-2', + ); + }); + }); +}); diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts new file mode 100644 index 0000000000..99377b2ca0 --- /dev/null +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts @@ -0,0 +1,173 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { + CatalogApi, + CatalogClient, + GetEntitiesRequest, +} from '@backstage/catalog-client'; +import { + Entity, + stringifyEntityRef, + UserEntity, +} from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { + DocumentCollatorFactory, + IndexableDocument, +} from '@backstage/search-common'; +import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common'; +import { Readable } from 'stream'; + +/** @public */ +export interface CatalogEntityDocument extends IndexableDocument { + componentType: string; + namespace: string; + kind: string; + lifecycle: string; + owner: string; +} + +/** @public */ +export type DefaultCatalogCollatorFactoryOptions = { + discovery: PluginEndpointDiscovery; + tokenManager: TokenManager; + locationTemplate?: string; + filter?: GetEntitiesRequest['filter']; + batchSize?: number; + catalogClient?: CatalogApi; +}; + +/** @public */ +export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { + public readonly type: string = 'software-catalog'; + public readonly visibilityPermission = catalogEntityReadPermission; + + private locationTemplate: string; + private filter?: GetEntitiesRequest['filter']; + private batchSize: number; + private readonly catalogClient: CatalogApi; + private tokenManager: TokenManager; + + static fromConfig( + _config: Config, + options: DefaultCatalogCollatorFactoryOptions, + ) { + return new DefaultCatalogCollatorFactory(options); + } + + private constructor(options: DefaultCatalogCollatorFactoryOptions) { + const { + batchSize, + discovery, + locationTemplate, + filter, + catalogClient, + tokenManager, + } = options; + + this.locationTemplate = + locationTemplate || '/catalog/:namespace/:kind/:name'; + this.filter = filter; + this.batchSize = batchSize || 500; + this.catalogClient = + catalogClient || new CatalogClient({ discoveryApi: discovery }); + this.tokenManager = tokenManager; + } + + async getCollator(): Promise { + return Readable.from(this.execute()); + } + + private applyArgsToFormat( + format: string, + args: Record, + ): string { + let formatted = format; + for (const [key, value] of Object.entries(args)) { + formatted = formatted.replace(`:${key}`, value); + } + return formatted.toLowerCase(); + } + + private isUserEntity(entity: Entity): entity is UserEntity { + return entity.kind.toLocaleUpperCase('en-US') === 'USER'; + } + + private getDocumentText(entity: Entity): string { + let documentText = entity.metadata.description || ''; + if (this.isUserEntity(entity)) { + if (entity.spec?.profile?.displayName && documentText) { + // combine displayName and description + const displayName = entity.spec?.profile?.displayName; + documentText = displayName.concat(' : ', documentText); + } else { + documentText = entity.spec?.profile?.displayName || documentText; + } + } + return documentText; + } + + private async *execute(): AsyncGenerator { + const { token } = await this.tokenManager.getToken(); + let entitiesRetrieved = 0; + let moreEntitiesToGet = true; + + // Offset/limit pagination is used on the Catalog Client in order to + // limit (and allow some control over) memory used by the search backend + // at index-time. + while (moreEntitiesToGet) { + const entities = ( + await this.catalogClient.getEntities( + { + filter: this.filter, + limit: this.batchSize, + offset: entitiesRetrieved, + }, + { token }, + ) + ).items; + + // Control looping through entity batches. + moreEntitiesToGet = entities.length === this.batchSize; + entitiesRetrieved += entities.length; + + for (const entity of entities) { + yield { + title: entity.metadata.title ?? entity.metadata.name, + location: this.applyArgsToFormat(this.locationTemplate, { + namespace: entity.metadata.namespace || 'default', + kind: entity.kind, + name: entity.metadata.name, + }), + text: this.getDocumentText(entity), + componentType: entity.spec?.type?.toString() || 'other', + namespace: entity.metadata.namespace || 'default', + kind: entity.kind, + lifecycle: (entity.spec?.lifecycle as string) || '', + owner: (entity.spec?.owner as string) || '', + authorization: { + resourceRef: stringifyEntityRef(entity), + }, + }; + } + } + } +} diff --git a/plugins/catalog-backend/src/search/index.ts b/plugins/catalog-backend/src/search/index.ts index 5641670333..93ff0b8b32 100644 --- a/plugins/catalog-backend/src/search/index.ts +++ b/plugins/catalog-backend/src/search/index.ts @@ -14,5 +14,11 @@ * limitations under the License. */ +export { DefaultCatalogCollatorFactory } from './DefaultCatalogCollatorFactory'; +export type { DefaultCatalogCollatorFactoryOptions } from './DefaultCatalogCollatorFactory'; +export type { CatalogEntityDocument } from './DefaultCatalogCollatorFactory'; + +/** + * todo(backstage/techdocs-core): stop exporting this in a future release. + */ export { DefaultCatalogCollator } from './DefaultCatalogCollator'; -export type { CatalogEntityDocument } from './DefaultCatalogCollator'; From 5185e27bdf3f37c535faca052838864af9b92e16 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 2 Mar 2022 17:23:42 +0100 Subject: [PATCH 125/353] chore: fix api-report Signed-off-by: blam --- .changeset/large-dancers-learn.md | 2 +- plugins/catalog-react/api-report.md | 17 ++++++++++------- .../src/components/EntityRefLink/humanize.ts | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.changeset/large-dancers-learn.md b/.changeset/large-dancers-learn.md index 43f9d27b11..9f25990924 100644 --- a/.changeset/large-dancers-learn.md +++ b/.changeset/large-dancers-learn.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder': minor --- -- **BREAKING**: Removed the `FavouriteEntity` export in favor of the `FavoriteEntity` from `@backstage/plguin-catalog-react`. Please migrate any usages to that component instead if you are creating your own `TemplateCard` page. +- **BREAKING**: Removed the `FavouriteTemplate` export in favor of the `FavoriteEntity` from `@backstage/plugin-catalog-react`. Please migrate any usages to that component instead if you are creating your own `TemplateCard` page. diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 78ad5d4b97..394a45f278 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -413,13 +413,8 @@ export const favoriteEntityTooltip: ( isStarred: boolean, ) => 'Remove from favorites' | 'Add to favorites'; -// @public (undocumented) -export function formatEntityRefTitle( - entityRef: Entity | EntityName, - opts?: { - defaultKind?: string; - }, -): string; +// @public @deprecated (undocumented) +export const formatEntityRefTitle: typeof humanizeEntityRef; // @public @deprecated (undocumented) export function getEntityMetadataEditUrl(entity: Entity): string | undefined; @@ -442,6 +437,14 @@ export function getEntitySourceLocation( scmIntegrationsApi: ScmIntegrationRegistry, ): EntitySourceLocation | undefined; +// @public (undocumented) +export function humanizeEntityRef( + entityRef: Entity | EntityName, + opts?: { + defaultKind?: string; + }, +): string; + // @public export function InspectEntityDialog(props: { open: boolean; diff --git a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts index af70ae71f4..e2984c34ab 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts @@ -20,7 +20,7 @@ import { DEFAULT_NAMESPACE, } from '@backstage/catalog-model'; -/** @deprecated please use {@link humanizeEntityRef} instead */ +/** @public @deprecated please use {@link humanizeEntityRef} instead */ export const formatEntityRefTitle = humanizeEntityRef; /** @public */ From 0087554f5ca9f95de4d1338f4d1099b5077d09e6 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 26 Feb 2022 20:01:30 +0100 Subject: [PATCH 126/353] Update TechDocs Collator to be stream-based Signed-off-by: Eric Peterson --- plugins/techdocs-backend/api-report.md | 35 ++- plugins/techdocs-backend/package.json | 1 + plugins/techdocs-backend/src/index.ts | 10 +- .../search/DefaultTechDocsCollator.test.ts | 12 - .../src/search/DefaultTechDocsCollator.ts | 5 +- .../DefaultTechDocsCollatorFactory.test.ts | 245 +++++++++++++++++ .../search/DefaultTechDocsCollatorFactory.ts | 254 ++++++++++++++++++ plugins/techdocs-backend/src/search/index.ts | 8 +- 8 files changed, 550 insertions(+), 20 deletions(-) create mode 100644 plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts create mode 100644 plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts diff --git a/plugins/techdocs-backend/api-report.md b/plugins/techdocs-backend/api-report.md index 2bf4664f77..155f096614 100644 --- a/plugins/techdocs-backend/api-report.md +++ b/plugins/techdocs-backend/api-report.md @@ -3,9 +3,11 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; -import { DocumentCollator } from '@backstage/search-common'; +import { DocumentCollatorFactory } from '@backstage/search-common'; import { Entity } from '@backstage/catalog-model'; import express from 'express'; import { GeneratorBuilder } from '@backstage/techdocs-common'; @@ -16,14 +18,15 @@ import { PluginCacheManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { PreparerBuilder } from '@backstage/techdocs-common'; import { PublisherBase } from '@backstage/techdocs-common'; +import { Readable } from 'stream'; import { TechDocsDocument } from '@backstage/techdocs-common'; import { TokenManager } from '@backstage/backend-common'; // @public export function createRouter(options: RouterOptions): Promise; -// @public -export class DefaultTechDocsCollator implements DocumentCollator { +// @public @deprecated +export class DefaultTechDocsCollator { // (undocumented) protected applyArgsToFormat( format: string, @@ -42,6 +45,21 @@ export class DefaultTechDocsCollator implements DocumentCollator { readonly visibilityPermission: Permission; } +// @public +export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { + // (undocumented) + static fromConfig( + config: Config, + options: TechDocsCollatorFactoryOptions, + ): DefaultTechDocsCollatorFactory; + // (undocumented) + getCollator(): Promise; + // (undocumented) + readonly type: string; + // (undocumented) + readonly visibilityPermission: Permission; +} + // @public export interface DocsBuildStrategy { // (undocumented) @@ -81,6 +99,17 @@ export type ShouldBuildParameters = { entity: Entity; }; +// @public +export type TechDocsCollatorFactoryOptions = { + discovery: PluginEndpointDiscovery; + logger: Logger_2; + tokenManager: TokenManager; + locationTemplate?: string; + catalogClient?: CatalogApi; + parallelismLimit?: number; + legacyPathCasing?: boolean; +}; + // @public export type TechDocsCollatorOptions = { discovery: PluginEndpointDiscovery; diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 7e8241bfea..20100f4848 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -56,6 +56,7 @@ }, "devDependencies": { "@backstage/cli": "^0.14.1", + "@backstage/plugin-search-backend-node": "0.4.7", "@backstage/test-utils": "^0.2.6", "@types/dockerode": "^3.3.0", "msw": "^0.35.0", diff --git a/plugins/techdocs-backend/src/index.ts b/plugins/techdocs-backend/src/index.ts index 01acbea2cc..570da95092 100644 --- a/plugins/techdocs-backend/src/index.ts +++ b/plugins/techdocs-backend/src/index.ts @@ -29,8 +29,14 @@ export type { ShouldBuildParameters, } from './service'; -export { DefaultTechDocsCollator } from './search'; -export type { TechDocsCollatorOptions } from './search'; +export { + DefaultTechDocsCollator, + DefaultTechDocsCollatorFactory, +} from './search'; +export type { + TechDocsCollatorFactoryOptions, + TechDocsCollatorOptions, +} from './search'; /** * @deprecated Use directly from @backstage/techdocs-common diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts index d9f75767a7..887dc60b1a 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts @@ -72,18 +72,6 @@ const expectedEntities: Entity[] = [ owner: 'someone', }, }, - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'test-entity', - description: 'The expected description', - }, - spec: { - type: 'some-type', - lifecycle: 'experimental', - }, - }, ]; describe('DefaultTechDocsCollator with legacyPathCasing configuration', () => { diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts index 73c030cc51..d36e1c0ae5 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts @@ -24,7 +24,6 @@ import { RELATION_OWNED_BY, stringifyEntityRef, } from '@backstage/catalog-model'; -import { DocumentCollator } from '@backstage/search-common'; import fetch from 'node-fetch'; import unescape from 'lodash/unescape'; import { Logger } from 'winston'; @@ -69,8 +68,10 @@ type EntityInfo = { * A search collator responsible for gathering and transforming TechDocs documents. * * @public + * @deprecated Upgrade to a more recent `@backstage/search-backend-node` and + * use `DefaultTechDocsCollatorFactory` instead. */ -export class DefaultTechDocsCollator implements DocumentCollator { +export class DefaultTechDocsCollator { public readonly type: string = 'techdocs'; public readonly visibilityPermission = catalogEntityReadPermission; diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts new file mode 100644 index 0000000000..fafaae71b9 --- /dev/null +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts @@ -0,0 +1,245 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { + getVoidLogger, + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { Entity } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import { TestPipeline } from '@backstage/plugin-search-backend-node'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { Readable } from 'stream'; +import { DefaultTechDocsCollatorFactory } from './DefaultTechDocsCollatorFactory'; + +const logger = getVoidLogger(); + +const mockSearchDocIndex = { + config: { + lang: ['en'], + min_search_length: 3, + prebuild_index: false, + separator: '[\\s\\-]+', + }, + docs: [ + { + location: '', + text: 'docs docs docs', + title: 'Home', + }, + { + location: 'local-development/', + text: 'Docs for first subtitle', + title: 'Local development', + }, + { + location: 'local-development/#development', + text: 'Docs for sub-subtitle', + title: 'Development', + }, + ], +}; + +const expectedEntities: Entity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + title: 'Test Entity with Docs!', + name: 'test-entity-with-docs', + description: 'Documented description', + annotations: { + 'backstage.io/techdocs-ref': './', + }, + }, + spec: { + type: 'dog', + lifecycle: 'experimental', + owner: 'someone', + }, + }, +]; + +describe('DefaultTechDocsCollatorFactory', () => { + const config = new ConfigReader({}); + const mockDiscoveryApi: jest.Mocked = { + getBaseUrl: jest.fn().mockResolvedValue('http://test-backend'), + getExternalBaseUrl: jest.fn(), + }; + const mockTokenManager: jest.Mocked = { + getToken: jest.fn().mockResolvedValue({ token: '' }), + authenticate: jest.fn(), + }; + const options = { + discovery: mockDiscoveryApi, + logger: getVoidLogger(), + tokenManager: mockTokenManager, + }; + + it('has expected type', () => { + const factory = DefaultTechDocsCollatorFactory.fromConfig(config, options); + expect(factory.type).toBe('techdocs'); + }); + + describe('getCollator', () => { + let factory: DefaultTechDocsCollatorFactory; + let collator: Readable; + + const worker = setupServer(); + setupRequestMockHandlers(worker); + + beforeEach(async () => { + factory = DefaultTechDocsCollatorFactory.fromConfig(config, options); + collator = await factory.getCollator(); + + worker.use( + rest.get( + 'http://test-backend/static/docs/default/Component/test-entity-with-docs/search/search_index.json', + (_, res, ctx) => res(ctx.status(200), ctx.json(mockSearchDocIndex)), + ), + rest.get('http://test-backend/entities', (req, res, ctx) => { + // Imitate offset/limit pagination. + const offset = parseInt( + req.url.searchParams.get('offset') || '0', + 10, + ); + const limit = parseInt( + req.url.searchParams.get('limit') || '500', + 10, + ); + + // Limit 50 corresponds to a case testing pagination. + if (limit === 50) { + // Return 50 copies of invalid entities on the first request. + if (offset === 0) { + return res(ctx.status(200), ctx.json(Array(50).fill({}))); + } + // Then just the regular 2 on the second. + return res(ctx.status(200), ctx.json(expectedEntities)); + } + return res( + ctx.status(200), + ctx.json(expectedEntities.slice(offset, limit + offset)), + ); + }), + ); + }); + + it('returns a readable stream', async () => { + expect(collator).toBeInstanceOf(Readable); + }); + + it('fetches from the configured catalog and tech docs services', async () => { + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + expect(mockDiscoveryApi.getBaseUrl).toHaveBeenCalledWith('catalog'); + expect(mockDiscoveryApi.getBaseUrl).toHaveBeenCalledWith('techdocs'); + expect(documents).toHaveLength(mockSearchDocIndex.docs.length); + }); + + it('should create documents for each tech docs search index', async () => { + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + const entity = expectedEntities[0]; + documents.forEach((document, idx) => { + expect(document).toMatchObject({ + title: mockSearchDocIndex.docs[idx].title, + location: `/docs/default/component/${entity.metadata.name}/${mockSearchDocIndex.docs[idx].location}`, + text: mockSearchDocIndex.docs[idx].text, + namespace: 'default', + entityTitle: entity!.metadata.title, + componentType: entity!.spec!.type, + lifecycle: entity!.spec!.lifecycle, + owner: '', + kind: entity.kind.toLocaleLowerCase('en-US'), + name: entity.metadata.name, + }); + }); + }); + + it('maps a returned entity with a custom locationTemplate', async () => { + // Provide an alternate location template. + factory = DefaultTechDocsCollatorFactory.fromConfig(config, { + discovery: mockDiscoveryApi, + tokenManager: mockTokenManager, + locationTemplate: '/software/:name', + logger, + }); + collator = await factory.getCollator(); + + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + + expect(documents[0]).toMatchObject({ + location: '/software/test-entity-with-docs', + }); + }); + + it('paginates through catalog entities using batchSize', async () => { + // A parallelismLimit of 1 is a catalog limit of 50 per request. Code + // above in the /entities handler ensures valid entities are only + // returned on the second page. + factory = DefaultTechDocsCollatorFactory.fromConfig(config, { + ...options, + parallelismLimit: 1, + }); + collator = await factory.getCollator(); + + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + + // Only 1 entity with TechDocs configured multipled by 3 pages. + expect(documents).toHaveLength(3); + }); + + describe('with legacyPathCasing configuration', () => { + beforeEach(async () => { + const legacyConfig = new ConfigReader({ + techdocs: { + legacyUseCaseSensitiveTripletPaths: true, + }, + }); + factory = DefaultTechDocsCollatorFactory.fromConfig( + legacyConfig, + options, + ); + collator = await factory.getCollator(); + }); + + it('should create documents for each tech docs search index', async () => { + const pipeline = TestPipeline.withSubject(collator); + const { documents } = await pipeline.execute(); + const entity = expectedEntities[0]; + documents.forEach((document, idx) => { + expect(document).toMatchObject({ + title: mockSearchDocIndex.docs[idx].title, + location: `/docs/default/Component/${entity.metadata.name}/${mockSearchDocIndex.docs[idx].location}`, + text: mockSearchDocIndex.docs[idx].text, + namespace: 'default', + entityTitle: entity!.metadata.title, + componentType: entity!.spec!.type, + lifecycle: entity!.spec!.lifecycle, + owner: '', + kind: entity.kind, + name: entity.metadata.name, + }); + }); + }); + }); + }); +}); diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts new file mode 100644 index 0000000000..181f032b54 --- /dev/null +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts @@ -0,0 +1,254 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { + CatalogApi, + CatalogClient, + CATALOG_FILTER_EXISTS, +} from '@backstage/catalog-client'; +import { + Entity, + parseEntityRef, + RELATION_OWNED_BY, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common'; +import { DocumentCollatorFactory } from '@backstage/search-common'; +import { TechDocsDocument } from '@backstage/techdocs-common'; +import unescape from 'lodash/unescape'; +import fetch from 'node-fetch'; +import pLimit from 'p-limit'; +import { Readable } from 'stream'; +import { Logger } from 'winston'; + +interface MkSearchIndexDoc { + title: string; + text: string; + location: string; +} + +/** + * Options to configure the TechDocs collator factory + * + * @public + */ +export type TechDocsCollatorFactoryOptions = { + discovery: PluginEndpointDiscovery; + logger: Logger; + tokenManager: TokenManager; + locationTemplate?: string; + catalogClient?: CatalogApi; + parallelismLimit?: number; + legacyPathCasing?: boolean; +}; + +type EntityInfo = { + name: string; + namespace: string; + kind: string; +}; + +/** + * A search collator factory responsible for gathering and transforming + * TechDocs documents. + * + * @public + */ +export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory { + public readonly type: string = 'techdocs'; + public readonly visibilityPermission = catalogEntityReadPermission; + + private discovery: PluginEndpointDiscovery; + private locationTemplate: string; + private readonly logger: Logger; + private readonly catalogClient: CatalogApi; + private readonly tokenManager: TokenManager; + private readonly parallelismLimit: number; + private readonly legacyPathCasing: boolean; + + private constructor(options: TechDocsCollatorFactoryOptions) { + this.discovery = options.discovery; + this.locationTemplate = + options.locationTemplate || '/docs/:namespace/:kind/:name/:path'; + this.logger = options.logger; + this.catalogClient = + options.catalogClient || + new CatalogClient({ discoveryApi: options.discovery }); + this.parallelismLimit = options.parallelismLimit ?? 10; + this.legacyPathCasing = options.legacyPathCasing ?? false; + this.tokenManager = options.tokenManager; + } + + static fromConfig(config: Config, options: TechDocsCollatorFactoryOptions) { + const legacyPathCasing = + config.getOptionalBoolean( + 'techdocs.legacyUseCaseSensitiveTripletPaths', + ) || false; + return new DefaultTechDocsCollatorFactory({ ...options, legacyPathCasing }); + } + + async getCollator(): Promise { + return Readable.from(this.execute()); + } + + private async *execute(): AsyncGenerator { + const limit = pLimit(this.parallelismLimit); + const techDocsBaseUrl = await this.discovery.getBaseUrl('techdocs'); + const { token } = await this.tokenManager.getToken(); + let entitiesRetrieved = 0; + let moreEntitiesToGet = true; + + // Offset/limit pagination is used on the Catalog Client in order to + // limit (and allow some control over) memory used by the search backend + // at index-time. The batchSize is calculated as a factor of the given + // parallelism limit to simplify configuration. + const batchSize = this.parallelismLimit * 50; + while (moreEntitiesToGet) { + const entities = ( + await this.catalogClient.getEntities( + { + filter: { + 'metadata.annotations.backstage.io/techdocs-ref': + CATALOG_FILTER_EXISTS, + }, + fields: [ + 'kind', + 'namespace', + 'metadata.annotations', + 'metadata.name', + 'metadata.title', + 'metadata.namespace', + 'spec.type', + 'spec.lifecycle', + 'relations', + ], + limit: batchSize, + offset: entitiesRetrieved, + }, + { token }, + ) + ).items; + + // Control looping through entity batches. + moreEntitiesToGet = entities.length === batchSize; + entitiesRetrieved += entities.length; + + const docPromises = entities + .filter(it => it.metadata?.annotations?.['backstage.io/techdocs-ref']) + .map((entity: Entity) => + limit(async (): Promise => { + const entityInfo = + DefaultTechDocsCollatorFactory.handleEntityInfoCasing( + this.legacyPathCasing, + { + kind: entity.kind, + namespace: entity.metadata.namespace || 'default', + name: entity.metadata.name, + }, + ); + + try { + const searchIndexResponse = await fetch( + DefaultTechDocsCollatorFactory.constructDocsIndexUrl( + techDocsBaseUrl, + entityInfo, + ), + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + ); + const searchIndex = await searchIndexResponse.json(); + + return searchIndex.docs.map((doc: MkSearchIndexDoc) => ({ + title: unescape(doc.title), + text: unescape(doc.text || ''), + location: this.applyArgsToFormat( + this.locationTemplate || '/docs/:namespace/:kind/:name/:path', + { + ...entityInfo, + path: doc.location, + }, + ), + path: doc.location, + ...entityInfo, + entityTitle: entity.metadata.title, + componentType: entity.spec?.type?.toString() || 'other', + lifecycle: (entity.spec?.lifecycle as string) || '', + owner: getSimpleEntityOwnerString(entity), + authorization: { + resourceRef: stringifyEntityRef(entity), + }, + })); + } catch (e) { + this.logger.debug( + `Failed to retrieve tech docs search index for entity ${entityInfo.namespace}/${entityInfo.kind}/${entityInfo.name}`, + e, + ); + return []; + } + }), + ); + yield* (await Promise.all(docPromises)).flat(); + } + } + + private applyArgsToFormat( + format: string, + args: Record, + ): string { + let formatted = format; + for (const [key, value] of Object.entries(args)) { + formatted = formatted.replace(`:${key}`, value); + } + return formatted; + } + + private static constructDocsIndexUrl( + techDocsBaseUrl: string, + entityInfo: { kind: string; namespace: string; name: string }, + ) { + return `${techDocsBaseUrl}/static/docs/${entityInfo.namespace}/${entityInfo.kind}/${entityInfo.name}/search/search_index.json`; + } + + private static handleEntityInfoCasing( + legacyPaths: boolean, + entityInfo: EntityInfo, + ): EntityInfo { + return legacyPaths + ? entityInfo + : Object.entries(entityInfo).reduce((acc, [key, value]) => { + return { ...acc, [key]: value.toLocaleLowerCase('en-US') }; + }, {} as EntityInfo); + } +} + +function getSimpleEntityOwnerString(entity: Entity): string { + if (entity.relations) { + const owner = entity.relations.find(r => r.type === RELATION_OWNED_BY); + if (owner) { + const { name } = parseEntityRef(owner.targetRef); + return name; + } + } + return ''; +} diff --git a/plugins/techdocs-backend/src/search/index.ts b/plugins/techdocs-backend/src/search/index.ts index fbbd23b964..68e3b4edc7 100644 --- a/plugins/techdocs-backend/src/search/index.ts +++ b/plugins/techdocs-backend/src/search/index.ts @@ -13,6 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { DefaultTechDocsCollator } from './DefaultTechDocsCollator'; +export { DefaultTechDocsCollatorFactory } from './DefaultTechDocsCollatorFactory'; +export type { TechDocsCollatorFactoryOptions } from './DefaultTechDocsCollatorFactory'; + +/** + * todo(backstage/techdocs-core): stop exporting these in a future release. + */ +export { DefaultTechDocsCollator } from './DefaultTechDocsCollator'; export type { TechDocsCollatorOptions } from './DefaultTechDocsCollator'; From d0993bc6f1956b285ebeac910afa5c315809228a Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 26 Feb 2022 20:48:20 +0100 Subject: [PATCH 127/353] Wire up app/create-app to use stream-based search implementations Signed-off-by: Eric Peterson --- packages/backend/src/plugins/search.ts | 8 ++++---- .../default-app/packages/backend/src/plugins/search.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/plugins/search.ts b/packages/backend/src/plugins/search.ts index 4b5363e73d..a90b47cd81 100644 --- a/packages/backend/src/plugins/search.ts +++ b/packages/backend/src/plugins/search.ts @@ -18,7 +18,7 @@ import { useHotCleanup, } from '@backstage/backend-common'; import { Config } from '@backstage/config'; -import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; +import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; import { createRouter } from '@backstage/plugin-search-backend'; import { ElasticSearchSearchEngine } from '@backstage/plugin-search-backend-module-elasticsearch'; import { PgSearchEngine } from '@backstage/plugin-search-backend-module-pg'; @@ -27,7 +27,7 @@ import { LunrSearchEngine, SearchEngine, } from '@backstage/plugin-search-backend-node'; -import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend'; +import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; import { Logger } from 'winston'; import { PluginEnvironment } from '../types'; @@ -70,7 +70,7 @@ export default async function createPlugin({ // particular collator gathers entities from the software catalog. indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: DefaultCatalogCollator.fromConfig(config, { + factory: DefaultCatalogCollatorFactory.fromConfig(config, { discovery, tokenManager, }), @@ -78,7 +78,7 @@ export default async function createPlugin({ indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: DefaultTechDocsCollator.fromConfig(config, { + factory: DefaultTechDocsCollatorFactory.fromConfig(config, { discovery, logger, tokenManager, diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts index a0a1cc3701..c359cb4986 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts @@ -5,8 +5,8 @@ import { LunrSearchEngine, } from '@backstage/plugin-search-backend-node'; import { PluginEnvironment } from '../types'; -import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; -import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend'; +import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; +import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; export default async function createPlugin({ logger, @@ -23,7 +23,7 @@ export default async function createPlugin({ // collator gathers entities from the software catalog. indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: DefaultCatalogCollator.fromConfig(config, { + factory: DefaultCatalogCollatorFactory.fromConfig(config, { discovery, tokenManager, }), @@ -32,7 +32,7 @@ export default async function createPlugin({ // collator gathers entities from techdocs. indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: DefaultTechDocsCollator.fromConfig(config, { + factory: DefaultTechDocsCollatorFactory.fromConfig(config, { discovery, logger, tokenManager, From 022507c8603a7e8b3901d9a77ed18d63251e7767 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 26 Feb 2022 21:05:24 +0100 Subject: [PATCH 128/353] Document stream-based search Signed-off-by: Eric Peterson --- .changeset/search-blankly-have-a-nice-life.md | 14 + .changeset/search-done-me-no-favor.md | 6 + .changeset/search-just-smile-politely.md | 13 + .changeset/search-like-a-bank-teller.md | 43 +++ .changeset/search-selfless-cold-composed.md | 6 + .changeset/search-throw-me-a-right.md | 13 + .github/styles/vocab.txt | 1 + docs/features/search/concepts.md | 25 +- docs/features/search/how-to-guides.md | 274 +++++++++++++++++- 9 files changed, 380 insertions(+), 15 deletions(-) create mode 100644 .changeset/search-blankly-have-a-nice-life.md create mode 100644 .changeset/search-done-me-no-favor.md create mode 100644 .changeset/search-just-smile-politely.md create mode 100644 .changeset/search-like-a-bank-teller.md create mode 100644 .changeset/search-selfless-cold-composed.md create mode 100644 .changeset/search-throw-me-a-right.md diff --git a/.changeset/search-blankly-have-a-nice-life.md b/.changeset/search-blankly-have-a-nice-life.md new file mode 100644 index 0000000000..eb6d02110c --- /dev/null +++ b/.changeset/search-blankly-have-a-nice-life.md @@ -0,0 +1,14 @@ +--- +'@backstage/plugin-search-backend-node': minor +'@backstage/search-common': minor +--- + +The Backstage Search Platform's indexing process has been rewritten as a stream +pipeline in order to improve efficiency and performance on large document sets. + +The concepts of `Collator` and `Decorator` have been replaced with readable and +transform object streams (respectively), as well as factory classes to +instantiate them. + +Accordingly, the `SearchEngine.index()` method has also been replaced with a +`getIndexer()` factory method that resolves to a writable object stream. diff --git a/.changeset/search-done-me-no-favor.md b/.changeset/search-done-me-no-favor.md new file mode 100644 index 0000000000..620c9dd923 --- /dev/null +++ b/.changeset/search-done-me-no-favor.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-search-backend-module-pg': minor +--- + +The `PgSearchEngine` implements the new stream-based indexing process expected +by the latest `@backstage/search-backend-node`. diff --git a/.changeset/search-just-smile-politely.md b/.changeset/search-just-smile-politely.md new file mode 100644 index 0000000000..e1cb57a17e --- /dev/null +++ b/.changeset/search-just-smile-politely.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +A `DefaultTechDocsCollatorFactory`, which works with the new stream-based +search indexing subsystem, is now available. The `DefaultTechDocsCollator` will +continue to be available for those unable to upgrade to the stream-based +`@backstage/search-backend-node` (and related packages), however it is now +marked as deprecated and will be removed in a future version. + +To upgrade this plugin and the search indexing subsystem in one go, check +[this changelog](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md) +for necessary changes to your search backend plugin configuration. diff --git a/.changeset/search-like-a-bank-teller.md b/.changeset/search-like-a-bank-teller.md new file mode 100644 index 0000000000..a9b0330eb2 --- /dev/null +++ b/.changeset/search-like-a-bank-teller.md @@ -0,0 +1,43 @@ +--- +'@backstage/create-app': patch +--- + +The Backstage Search Platform's indexing process has been rewritten as a stream +pipeline in order to improve efficiency and performance on large document sets. + +To take advantage of this, upgrade to the latest version of +`@backstage/plugin-search-backend-node`, as well as any backend plugins whose +collators you are using. Then, make the following changes to your +`/packages/backend/src/plugins/search.ts` file: + +```diff +-import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; +-import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend'; ++import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; ++import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; + +// ... + + const indexBuilder = new IndexBuilder({ logger, searchEngine }); + + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, +- collator: DefaultCatalogCollator.fromConfig(config, { discovery }), ++ factory: DefaultCatalogCollatorFactory.fromConfig(config, { discovery }), + }); + + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, +- collator: DefaultTechDocsCollator.fromConfig(config, { ++ factory: DefaultTechDocsCollatorFactory.fromConfig(config, { + discovery, + logger, + }), + }); +``` + +If you've written custom collators, decorators, or search engines in your +Backstage backend instance, you will need to re-implement them as readable, +transform, and writable streams respectively (including factory classes for +instantiating them). [A how-to guide for refactoring](https://backstage.io/docs/features/search/how-to-guides#rewriting-alpha-style-collators-for-beta) +existing implementations is available. diff --git a/.changeset/search-selfless-cold-composed.md b/.changeset/search-selfless-cold-composed.md new file mode 100644 index 0000000000..31c0e86baf --- /dev/null +++ b/.changeset/search-selfless-cold-composed.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-search-backend-module-elasticsearch': minor +--- + +The `ElasticSearchSearchEngine` implements the new stream-based indexing +process expected by the latest `@backstage/search-backend-node`. diff --git a/.changeset/search-throw-me-a-right.md b/.changeset/search-throw-me-a-right.md new file mode 100644 index 0000000000..e504888cfb --- /dev/null +++ b/.changeset/search-throw-me-a-right.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +A `DefaultCatalogCollatorFactory`, which works with the new stream-based +search indexing subsystem, is now available. The `DefaultCatalogCollator` will +continue to be available for those unable to upgrade to the stream-based +`@backstage/search-backend-node` (and related packages), however it is now +marked as deprecated and will be removed in a future version. + +To upgrade this plugin and the search indexing subsystem in one go, check +[this changelog](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md) +for necessary changes to your search backend plugin configuration. diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 6359493d4b..e4b457dc1d 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -213,6 +213,7 @@ parallelization Patrik Peloton performant +Performant plantuml Platformize Podman diff --git a/docs/features/search/concepts.md b/docs/features/search/concepts.md index 8cb17b12e6..052f71376f 100644 --- a/docs/features/search/concepts.md +++ b/docs/features/search/concepts.md @@ -54,13 +54,14 @@ An index is a collection of such documents of a given type. ### Collators You need to be able to search something! Collators are the way to define what -can be searched. Specifically, they're classes which return documents conforming -to a minimum set of fields (including a document title, location, and text), but -which can contain any other fields as defined by the collator itself. One -collator is responsible for defining and collecting documents of a type. +can be searched. Specifically, they're readable object streams of documents that +conform to a minimum set of fields (including a document title, location, and +text), but which can contain any other fields as defined by the collator itself. +One collator is responsible for defining and collecting documents of a type. -Some plugins, like the Catalog Backend, provide so-called "default" collators -which you can use out-of-the-box to start searching across Backstage quickly. +Some plugins, like the Catalog Backend, provide so-called "default" collator +factories which you can use out-of-the-box to start searching across Backstage +quickly. ### Decorators @@ -68,9 +69,15 @@ Sometimes you want to add extra information to a set of documents in your search index that the collator may not be aware of. For example, the Software Catalog knows about software entities, but it may not know about their usage or quality. -Decorators are classes which can add extra fields to pre-collated documents. -This extra metadata could then be used to bias search results or otherwise -improve the search experience in your Backstage instance. +Decorators are transform streams which sit between a collator (read stream) and +an indexer (write stream) during the indexing process. It can be used to add +extra fields to documents as they are being collated and indexed. This extra +metadata could then be used to bias search results or otherwise improve the +search experience in your Backstage instance. + +In addition to adding extra metadata, decorators (like any transform stream) can +also be used to remove metadata, filter out, or even add extra documents at +index-time. ### The Scheduler diff --git a/docs/features/search/how-to-guides.md b/docs/features/search/how-to-guides.md index a3c42c682c..ac36d64673 100644 --- a/docs/features/search/how-to-guides.md +++ b/docs/features/search/how-to-guides.md @@ -48,10 +48,10 @@ const app = createApp({ ## How to index TechDocs documents The TechDocs plugin has supported integrations to Search, meaning that it -provides a default collator ready to be used. +provides a default collator factory ready to be used. The purpose of this guide is to walk you through how to register the -[DefaultTechDocsCollator](https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts) +[DefaultTechDocsCollatorFactory](https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts) in your App, so that you can get TechDocs documents indexed. If you have been through the @@ -60,18 +60,19 @@ you should have the `packages/backend/src/plugins/search.ts` file available. If so, you can go ahead and follow this guide - if not, start by going through the getting started guide. -1. Import the DefaultTechDocsCollator from `@backstage/plugin-techdocs-backend`. +1. Import the `DefaultTechDocsCollatorFactory` from + `@backstage/plugin-techdocs-backend`. ```typescript -import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend'; +import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; ``` -2. Register the DefaultTechDocsCollator with the IndexBuilder. +2. Register the `DefaultTechDocsCollatorFactory` with the IndexBuilder. ```typescript indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: DefaultTechDocsCollator.fromConfig(config, { + factory: DefaultTechDocsCollatorFactory.fromConfig(config, { discovery, logger, tokenManager, @@ -131,3 +132,264 @@ indexBuilder.addCollator({ As shown above, you can add a catalog entity filter to narrow down what catalog entities are indexed by the search engine. + +## How to migrate from Search Alpha to Beta + +For the purposes of this guide, Search Beta version is defined as: + +- **Search Plugin**: At least `v0.x.y` +- **Search Backend Plugin**: At least `v0.x.y` +- **Search Backend Node**: At least `v0.x.y` + +In the Beta version, the Search Platform's indexing process has been rewritten +as a stream pipeline in order to improve efficiency and performance on large +sets of documents. + +If you've not yet extended the Search Platform with custom code, and have +instead taken advantage of default collators, decorators, and search engines +provided by existing plugins, the migration process is fairly straightforward: + +1. Upgrade to at least version `0.x.y` of + `@backstage/plugin-search-backend-node`, as well as any backend plugins whose + collators you are using (e.g. at least version `0.x.y` of + `@backstage/plugin-catalog-backend` and/or version `0.x.y` of + `@backstage/plugin-techdocs-backend`). +2. Then, make the following changes to your + `/packages/backend/src/plugins/search.ts` file: + + ```diff + -import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; + -import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend'; + +import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; + +import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; + // ... + const indexBuilder = new IndexBuilder({ logger, searchEngine }); + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + - collator: DefaultCatalogCollator.fromConfig(config, { discovery }), + + factory: DefaultCatalogCollatorFactory.fromConfig(config, { discovery }), + }); + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + - collator: DefaultTechDocsCollator.fromConfig(config, { + + factory: DefaultTechDocsCollatorFactory.fromConfig(config, { + discovery, + logger, + }), + }); + ``` + +Any custom collators, decorators, or search engine implementations will require +minor refactoring. Continue on for details. + +### Rewriting alpha-style collators for beta + +In alpha versions of the Backstage Search Platform, collators were classes that +implemented an `execute` method which resolved an `IndexableDocument` array. + +In beta versions, the logic encapsulated by the aforementioned `execute` method +is contained within an [object-mode][obj-mode] `Readable` stream where each +object pushed onto the stream is of type `IndexableDocument`. Instances of this +stream are instantiated by a factory class conforming to the +`DocumentCollatorFactory` interface. + +The optimal conversion strategy will vary depending on the collator's logic, but +the simplest conversion can follow a process like this: + +1. Rename your collator class to something like `YourCollatorFactory` and update + it to implement `DocumentCollatorFactory` instead of `DocumentCollator`. +2. Update its `execute` method so that it resolves + `AsyncGenerator` instead of `YourIndexableDocument[]`. +3. Implement `DocumentCollatorFactory`'s `getCollator` method which resolves to + `Readable.from(this.execute())` (which is a utility for creating [readable + streams][read-stream] from [async generators][async-gen]). + +```ts +import { DocumentCollatorFactory } from '@backstage/plugin-search-backend-node'; +import { Readable } from 'stream'; +export class YourCollatorFactory implements DocumentCollatorFactory { + public readonly type: string = 'your-type'; + async *execute(): AsyncGenerator { + const widgets = await this.client.getWidgets(); + for (const widget of widgets) { + yield { + title: widget.name, + location: widget.url, + text: widget.description, + }; + } + } + getCollator() { + return Readable.from(this.execute()); + } +} +``` + +Note: it may be possible to simplify your collator dramatically! If your custom +collator was previously using streams under the hood (for example, by reading +newline delimited JSON from a local or remote file), you could just expose the +stream directly via a simple factory class: + +```ts +import { DocumentCollatorFactory } from '@backstage/plugin-search-backend-node'; +import { createReadStream } from 'fs'; +import { parse } from '@jsonlines/core'; +export class YourCollatorFactory implements DocumentCollatorFactory { + public readonly type: string = 'your-type'; + async getCollator() { + const parseStream = parse(); + return createReadStream('./documents.ndjson').pipe(parseStream); + } +} +``` + +### Rewriting alpha-style decorators for beta + +In alpha versions of the Backstage Search Platform, decorators were classes that +implemented an `execute` method which took an `IndexableDocument` array as an +argument, and resolved a modified array of the same type. + +In beta versions, the logic encapsulated by the aforementioned `execute` method +is contained within an object-mode `Transform` stream which reads objects of +type `IndexableDocument`, and writes objects of a conforming type. Similar to +collators, instances of this stream are instantiated by a factory class +conforming to the `DocumentDecoratorFactory` interface. + +Although you can choose to implement a `Transform` stream from scratch, the +`@backstage/plugin-search-backend-node` package provides a `DecoratorBase` class +in order to simplify the developer experience. With this base class, all that's +needed is to transfer your old decorator class logic into the base class' three +methods (`initialize`, `decorate`, and `finalize`), and implement the factory +class that instantiates the stream: + +```ts +import { DecoratorBase } from '@backstage/plugin-search-backend-node'; +export class YourDecorator extends DecoratorBase { + async initialize() { + // Setup logic. Performed once before any documents are consumed. + } + async decorate( + document: YourIndexableDocument, + ): Promise { + // Perform transformation logic here. + return document; + } + async finalize() { + // Teardown logic. Performed once after all documents have been consumed. + } +} +export class YourDecoratorFactory implements DocumentDecoratorFactory { + async getDecorator() { + return new YourDecorator(); + } +} +``` + +Note the return type of the `decorate` method and how each can be used to +different effect. + +- By resolving a single `YourIndexableDocument` object, your decorator can be + used to make simple transformations: + + ```ts + class BooleanWidgetCoolnessDecorator extends DecoratorBase { + async decorator(widget) { + // Perform a simple, 1:1 transformation. + widget.isCool = widget.isCool === 'true' ? true : false; + return widget; + } + } + ``` + +- By resolving `undefined`, your decorator can filter out documents which + shouldn't be in the index: + + ```ts + class OnlyCoolWidgetsDecorator extends DecoratorBase { + async decorator(widget) { + // Perform a simple filter operation. + return widget.isCool ? widget : undefined; + } + } + ``` + +- By resolving an array of `YourIndexableDocument` objects, you can generate + multiple documents based on the content of one: + + ```ts + class WidgetByVariantDecorator extends DecoratorBase { + async decorator(widget) { + // Generate one widget doc per widget variant. + return widget.variants.map(variant => { + // Each widget doc is the given widget plus a "variant" property + // pulled from a widget.variants string array. + return { + ...widget, + variant, + }; + }); + } + } + ``` + +In alpha versions, a decorator had access to every `IndexableDocument` +simultaneously. This is no longer possible in beta versions (precisely to make +the indexing process more efficient and performant). You will need to modify +your decorator's logic so that it does not need access to every document at +once. + +### Rewriting alpha-style search engines for beta + +Search Engines are responsible for both querying and indexing documents to an +underlying search engine technology. While the search engine query interface +didn't change between alpha and beta versions, the indexing half of the +interface _did_ change. + +In alpha versions of the Backstage Search Platform, a search engine implemented +an `index` method which took a `type` and an `IndexableDocument` array and was +responsible for writing these documents to the underlying search engine. + +In beta versions, the logic encapsulated by the aforementioned `index` method is +contained within an object-mode `Writable` stream which expects objects of type +`IndexableDocument`. On the search engine class itself, the `index` method is +replaced with a `getIndexer` factory method which still takes the `type`, but +resolves an instance of the aforementioned `Writable` stream. + +Although you can choose to implement a `Writable` stream from scratch, the +`@backstage/plugin-search-backend-node` package provides a +`BatchSearchEngineIndexer` class in order to simplify the developer experience. +With this base class, which collects documents in batches of a configurable size +on your behalf, all that's needed is to transfer your old `index` method logic +into the base class' three methods (`initialize`, `index`, and `finalize`), and +implement the factory method that instantiates the stream: + +```ts +import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; +import { SearchEngine } from '@backstage/search-common'; +export class YourSearchEngineIndexer extends BatchSearchEngineIndexer { + constructor({ type }: { type: string }) { + // Customize the number of documents passed to the index method per batch. + super({ batchSize: 500 }); + // An imaginary search engine indexing client. + this.index = new SomeSearchEngineIndex({ indexName: type }); + } + async initialize() { + // Setup logic. Performed once before any documents are consumed. + } + async index(documents: IndexableDocument[]) { + await this.index.batchOf(documents); + } + async finalize() { + // Teardown logic. Performed once after all documents have been consumed. + } +} +export class YourSearchEngine implements SearchEngine { + async getIndexer(type: string) { + return new YourSearchEngineIndexer({ type }); + } +} +``` + +[obj-mode]: https://nodejs.org/docs/latest-v14.x/api/stream.html#stream_object_mode +[read-stream]: https://nodejs.org/docs/latest-v14.x/api/stream.html#stream_readable_streams +[async-gen]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of#iterating_over_async_generators From 37b9ff3b1946d309f92d4a241ee25dfb6c73b992 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Sat, 26 Feb 2022 21:28:11 +0100 Subject: [PATCH 129/353] Update AuthorizedSearchEngine for stream-based indexing. Signed-off-by: Eric Peterson --- .../src/service/AuthorizedSearchEngine.test.ts | 2 +- .../search-backend/src/service/AuthorizedSearchEngine.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts b/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts index 0a7b45e164..137ffc24f1 100644 --- a/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts +++ b/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts @@ -62,7 +62,7 @@ describe('AuthorizedSearchEngine', () => { setTranslator: () => { throw new Error('Function not implemented. 1'); }, - index: () => { + getIndexer: () => { throw new Error('Function not implemented.2'); }, query: mockedQuery, diff --git a/plugins/search-backend/src/service/AuthorizedSearchEngine.ts b/plugins/search-backend/src/service/AuthorizedSearchEngine.ts index d0488af00d..82ed061196 100644 --- a/plugins/search-backend/src/service/AuthorizedSearchEngine.ts +++ b/plugins/search-backend/src/service/AuthorizedSearchEngine.ts @@ -25,7 +25,6 @@ import { } from '@backstage/plugin-permission-common'; import { DocumentTypeInfo, - IndexableDocument, QueryRequestOptions, QueryTranslator, SearchEngine, @@ -35,6 +34,7 @@ import { } from '@backstage/search-common'; import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; +import { Writable } from 'stream'; export function decodePageCursor(pageCursor?: string): { page: number } { if (!pageCursor) { @@ -78,8 +78,8 @@ export class AuthorizedSearchEngine implements SearchEngine { this.searchEngine.setTranslator(translator); } - async index(type: string, documents: IndexableDocument[]): Promise { - this.searchEngine.index(type, documents); + async getIndexer(type: string): Promise { + return this.searchEngine.getIndexer(type); } async query( From 0547246b84489dbe10a67f88ebf9b79342132919 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 1 Mar 2022 14:21:23 +0100 Subject: [PATCH 130/353] Clean up search-common/search-backend-node APIs, indicating beta. Signed-off-by: Eric Peterson --- packages/search-common/api-report.md | 40 +++-------- packages/search-common/src/types.ts | 19 ++++++ plugins/search-backend-node/api-report.md | 68 ++++++++++--------- .../search-backend-node/src/IndexBuilder.ts | 9 ++- plugins/search-backend-node/src/Scheduler.ts | 3 + .../src/engines/LunrSearchEngine.ts | 11 ++- .../src/engines/LunrSearchEngineIndexer.ts | 3 + .../search-backend-node/src/engines/index.ts | 5 +- plugins/search-backend-node/src/index.ts | 11 ++- .../src/indexing/BatchSearchEngineIndexer.ts | 4 ++ .../src/indexing/DecoratorBase.ts | 1 + .../src/test-utils/TestPipeline.ts | 2 + plugins/search-backend-node/src/types.ts | 12 ++++ scripts/api-extractor.ts | 2 + 14 files changed, 120 insertions(+), 70 deletions(-) diff --git a/packages/search-common/api-report.md b/packages/search-common/api-report.md index e47140d274..475d85e11c 100644 --- a/packages/search-common/api-report.md +++ b/packages/search-common/api-report.md @@ -11,33 +11,25 @@ import { Readable } from 'stream'; import { Transform } from 'stream'; import { Writable } from 'stream'; -// Warning: (ae-missing-release-tag) "DocumentCollatorFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @beta export interface DocumentCollatorFactory { getCollator(): Promise; readonly type: string; readonly visibilityPermission?: Permission; } -// Warning: (ae-missing-release-tag) "DocumentDecoratorFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @beta export interface DocumentDecoratorFactory { getDecorator(): Promise; readonly types?: string[]; } -// Warning: (ae-missing-release-tag) "DocumentTypeInfo" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @beta export type DocumentTypeInfo = { visibilityPermission?: Permission; }; -// Warning: (ae-missing-release-tag) "IndexableDocument" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @beta export interface IndexableDocument { authorization?: { resourceRef: string; @@ -47,21 +39,15 @@ export interface IndexableDocument { title: string; } -// Warning: (ae-missing-release-tag) "QueryRequestOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @beta export type QueryRequestOptions = { token?: string; }; -// Warning: (ae-missing-release-tag) "QueryTranslator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @beta export type QueryTranslator = (query: SearchQuery) => unknown; -// Warning: (ae-missing-release-tag) "SearchEngine" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @beta export interface SearchEngine { getIndexer(type: string): Promise; query( @@ -71,9 +57,7 @@ export interface SearchEngine { setTranslator(translator: QueryTranslator): void; } -// Warning: (ae-missing-release-tag) "SearchQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @beta (undocumented) export interface SearchQuery { // (undocumented) filters?: JsonObject; @@ -85,9 +69,7 @@ export interface SearchQuery { types?: string[]; } -// Warning: (ae-missing-release-tag) "SearchResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @beta (undocumented) export interface SearchResult { // (undocumented) document: IndexableDocument; @@ -95,9 +77,7 @@ export interface SearchResult { type: string; } -// Warning: (ae-missing-release-tag) "SearchResultSet" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @beta (undocumented) export interface SearchResultSet { // (undocumented) nextPageCursor?: string; diff --git a/packages/search-common/src/types.ts b/packages/search-common/src/types.ts index 6e106d3899..51ce45617b 100644 --- a/packages/search-common/src/types.ts +++ b/packages/search-common/src/types.ts @@ -18,6 +18,9 @@ import { Permission } from '@backstage/plugin-permission-common'; import { JsonObject } from '@backstage/types'; import { Readable, Transform, Writable } from 'stream'; +/** + * @beta + */ export interface SearchQuery { term: string; filters?: JsonObject; @@ -25,11 +28,17 @@ export interface SearchQuery { pageCursor?: string; } +/** + * @beta + */ export interface SearchResult { type: string; document: IndexableDocument; } +/** + * @beta + */ export interface SearchResultSet { results: SearchResult[]; nextPageCursor?: string; @@ -39,6 +48,7 @@ export interface SearchResultSet { /** * Base properties that all indexed documents must include, as well as some * common properties that documents are encouraged to use where appropriate. + * @beta */ export interface IndexableDocument { /** @@ -73,6 +83,7 @@ export interface IndexableDocument { * Information about a specific document type. Intended to be used in the * {@link @backstage/search-backend-node#IndexBuilder} to collect information * about the types stored in the index. + * @beta */ export type DocumentTypeInfo = { /** @@ -84,6 +95,7 @@ export type DocumentTypeInfo = { /** * Factory class for instantiating collators. + * @beta */ export interface DocumentCollatorFactory { /** @@ -106,6 +118,7 @@ export interface DocumentCollatorFactory { /** * Factory class for instantiating decorators. + * @beta */ export interface DocumentDecoratorFactory { /** @@ -124,9 +137,14 @@ export interface DocumentDecoratorFactory { /** * A type of function responsible for translating an abstract search query into * a concrete query relevant to a particular search engine. + * @beta */ export type QueryTranslator = (query: SearchQuery) => unknown; +/** + * Options when querying a search engine. + * @beta + */ export type QueryRequestOptions = { token?: string; }; @@ -135,6 +153,7 @@ export type QueryRequestOptions = { * Interface that must be implemented by specific search engines, responsible * for performing indexing and querying and translating abstract queries into * concrete, search engine-specific queries. + * @beta */ export interface SearchEngine { /** diff --git a/plugins/search-backend-node/api-report.md b/plugins/search-backend-node/api-report.md index d4b94a932e..37945c8902 100644 --- a/plugins/search-backend-node/api-report.md +++ b/plugins/search-backend-node/api-report.md @@ -19,9 +19,7 @@ import { SearchResultSet } from '@backstage/search-common'; import { Transform } from 'stream'; import { Writable } from 'stream'; -// Warning: (ae-missing-release-tag) "BatchSearchEngineIndexer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @beta export abstract class BatchSearchEngineIndexer extends Writable { constructor(options: BatchSearchEngineOptions); abstract finalize(): Promise; @@ -29,16 +27,19 @@ export abstract class BatchSearchEngineIndexer extends Writable { abstract initialize(): Promise; } -// Warning: (ae-missing-release-tag) "BatchSearchEngineOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @beta (undocumented) export type BatchSearchEngineOptions = { batchSize: number; }; -// Warning: (ae-missing-release-tag) "DecoratorBase" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @beta (undocumented) +export type ConcreteLunrQuery = { + lunrQueryBuilder: lunr_2.Index.QueryBuilder; + documentTypes?: string[]; + pageSize: number; +}; + +// @beta export abstract class DecoratorBase extends Transform { constructor(); abstract decorate( @@ -48,18 +49,13 @@ export abstract class DecoratorBase extends Transform { abstract initialize(): Promise; } -// Warning: (ae-missing-release-tag) "IndexBuilder" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @beta (undocumented) export class IndexBuilder { - // Warning: (ae-forgotten-export) The symbol "IndexBuilderOptions" needs to be exported by the entry point index.d.ts constructor({ logger, searchEngine }: IndexBuilderOptions); - // Warning: (ae-forgotten-export) The symbol "RegisterCollatorParameters" needs to be exported by the entry point index.d.ts addCollator({ factory, defaultRefreshIntervalSeconds, }: RegisterCollatorParameters): void; - // Warning: (ae-forgotten-export) The symbol "RegisterDecoratorParameters" needs to be exported by the entry point index.d.ts addDecorator({ factory }: RegisterDecoratorParameters): void; build(): Promise<{ scheduler: Scheduler; @@ -70,9 +66,16 @@ export class IndexBuilder { getSearchEngine(): SearchEngine; } -// Warning: (ae-missing-release-tag) "LunrSearchEngine" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @beta (undocumented) +export type IndexBuilderOptions = { + searchEngine: SearchEngine; + logger: Logger_2; +}; + +// @beta (undocumented) +export type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery; + +// @beta (undocumented) export class LunrSearchEngine implements SearchEngine { constructor({ logger }: { logger: Logger_2 }); // (undocumented) @@ -85,17 +88,13 @@ export class LunrSearchEngine implements SearchEngine { protected lunrIndices: Record; // (undocumented) query(query: SearchQuery): Promise; - // Warning: (ae-forgotten-export) The symbol "LunrQueryTranslator" needs to be exported by the entry point index.d.ts - // // (undocumented) setTranslator(translator: LunrQueryTranslator): void; // (undocumented) protected translator: QueryTranslator; } -// Warning: (ae-missing-release-tag) "LunrSearchEngineIndexer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @beta (undocumented) export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer { constructor(); // (undocumented) @@ -110,9 +109,18 @@ export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer { initialize(): Promise; } -// Warning: (ae-missing-release-tag) "Scheduler" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @beta +export interface RegisterCollatorParameters { + defaultRefreshIntervalSeconds: number; + factory: DocumentCollatorFactory; +} + +// @beta +export interface RegisterDecoratorParameters { + factory: DocumentDecoratorFactory; +} + +// @beta (undocumented) export class Scheduler { constructor({ logger }: { logger: Logger_2 }); addToSchedule(task: Function, interval: number): void; @@ -122,18 +130,14 @@ export class Scheduler { export { SearchEngine }; -// Warning: (ae-missing-release-tag) "TestPipeline" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @beta export class TestPipeline { execute(): Promise; withDocuments(documents: IndexableDocument[]): TestPipeline; static withSubject(subject: Readable | Transform | Writable): TestPipeline; } -// Warning: (ae-missing-release-tag) "TestPipelineResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public +// @beta export type TestPipelineResult = { error: unknown; documents: IndexableDocument[]; diff --git a/plugins/search-backend-node/src/IndexBuilder.ts b/plugins/search-backend-node/src/IndexBuilder.ts index 5f39c2fa33..e0a059e303 100644 --- a/plugins/search-backend-node/src/IndexBuilder.ts +++ b/plugins/search-backend-node/src/IndexBuilder.ts @@ -24,6 +24,7 @@ import { Transform, pipeline } from 'stream'; import { Logger } from 'winston'; import { Scheduler } from './index'; import { + IndexBuilderOptions, RegisterCollatorParameters, RegisterDecoratorParameters, } from './types'; @@ -33,11 +34,9 @@ interface CollatorEnvelope { refreshInterval: number; } -type IndexBuilderOptions = { - searchEngine: SearchEngine; - logger: Logger; -}; - +/** + * @beta + */ export class IndexBuilder { private collators: Record; private decorators: Record; diff --git a/plugins/search-backend-node/src/Scheduler.ts b/plugins/search-backend-node/src/Scheduler.ts index 3e356aa6aa..6debaa3dcd 100644 --- a/plugins/search-backend-node/src/Scheduler.ts +++ b/plugins/search-backend-node/src/Scheduler.ts @@ -26,6 +26,9 @@ type TaskEnvelope = { * TODO: coordination, error handling */ +/** + * @beta + */ export class Scheduler { private logger: Logger; private schedule: TaskEnvelope[]; diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index b2e131e56f..394cfffac8 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -25,6 +25,9 @@ import lunr from 'lunr'; import { Logger } from 'winston'; import { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer'; +/** + * @beta + */ export type ConcreteLunrQuery = { lunrQueryBuilder: lunr.Index.QueryBuilder; documentTypes?: string[]; @@ -36,8 +39,14 @@ type LunrResultEnvelope = { type: string; }; -type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery; +/** + * @beta + */ +export type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery; +/** + * @beta + */ export class LunrSearchEngine implements SearchEngine { protected lunrIndices: Record = {}; protected docStore: Record; diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.ts b/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.ts index 2454889745..01c95f07d4 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.ts @@ -18,6 +18,9 @@ import { IndexableDocument } from '@backstage/search-common'; import lunr from 'lunr'; import { BatchSearchEngineIndexer } from '../indexing'; +/** + * @beta + */ export class LunrSearchEngineIndexer extends BatchSearchEngineIndexer { private schemaInitialized = false; private builder: lunr.Builder; diff --git a/plugins/search-backend-node/src/engines/index.ts b/plugins/search-backend-node/src/engines/index.ts index 7b71873c64..0d710eadc2 100644 --- a/plugins/search-backend-node/src/engines/index.ts +++ b/plugins/search-backend-node/src/engines/index.ts @@ -15,5 +15,8 @@ */ export { LunrSearchEngine } from './LunrSearchEngine'; -export type { ConcreteLunrQuery } from './LunrSearchEngine'; +export type { + ConcreteLunrQuery, + LunrQueryTranslator, +} from './LunrSearchEngine'; export type { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer'; diff --git a/plugins/search-backend-node/src/index.ts b/plugins/search-backend-node/src/index.ts index 6ae716553c..2edab1b01d 100644 --- a/plugins/search-backend-node/src/index.ts +++ b/plugins/search-backend-node/src/index.ts @@ -23,7 +23,16 @@ export { IndexBuilder } from './IndexBuilder'; export { Scheduler } from './Scheduler'; export { LunrSearchEngine } from './engines'; -export type { LunrSearchEngineIndexer } from './engines'; +export type { + ConcreteLunrQuery, + LunrQueryTranslator, + LunrSearchEngineIndexer, +} from './engines'; +export type { + IndexBuilderOptions, + RegisterCollatorParameters, + RegisterDecoratorParameters, +} from './types'; export * from './indexing'; export * from './test-utils'; diff --git a/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts index 4c29d0b573..b368dd8184 100644 --- a/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts +++ b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts @@ -18,6 +18,9 @@ import { assertError } from '@backstage/errors'; import { IndexableDocument } from '@backstage/search-common'; import { Writable } from 'stream'; +/** + * @beta + */ export type BatchSearchEngineOptions = { batchSize: number; }; @@ -25,6 +28,7 @@ export type BatchSearchEngineOptions = { /** * Base class encapsulating batch-based stream processing. Useful as a base * class for search engine indexers. + * @beta */ export abstract class BatchSearchEngineIndexer extends Writable { private batchSize: number; diff --git a/plugins/search-backend-node/src/indexing/DecoratorBase.ts b/plugins/search-backend-node/src/indexing/DecoratorBase.ts index a28d652fd7..382ce443e3 100644 --- a/plugins/search-backend-node/src/indexing/DecoratorBase.ts +++ b/plugins/search-backend-node/src/indexing/DecoratorBase.ts @@ -21,6 +21,7 @@ import { Transform } from 'stream'; /** * Base class encapsulating simple async transformations. Useful as a base * class for Backstage search decorators. + * @beta */ export abstract class DecoratorBase extends Transform { private initialized: Promise; diff --git a/plugins/search-backend-node/src/test-utils/TestPipeline.ts b/plugins/search-backend-node/src/test-utils/TestPipeline.ts index 2dbdeb85ad..c0c2bf5f89 100644 --- a/plugins/search-backend-node/src/test-utils/TestPipeline.ts +++ b/plugins/search-backend-node/src/test-utils/TestPipeline.ts @@ -19,6 +19,7 @@ import { pipeline, Readable, Transform, Writable } from 'stream'; /** * Object resolved after a test pipeline is executed. + * @beta */ export type TestPipelineResult = { /** @@ -36,6 +37,7 @@ export type TestPipelineResult = { /** * Test utility for Backstage Search collators, decorators, and indexers. + * @beta */ export class TestPipeline { private collator?: Readable; diff --git a/plugins/search-backend-node/src/types.ts b/plugins/search-backend-node/src/types.ts index 4bcc8ec114..68ccfe4c7e 100644 --- a/plugins/search-backend-node/src/types.ts +++ b/plugins/search-backend-node/src/types.ts @@ -17,10 +17,21 @@ import { DocumentCollatorFactory, DocumentDecoratorFactory, + SearchEngine, } from '@backstage/search-common'; +import { Logger } from 'winston'; + +/** + * @beta + */ +export type IndexBuilderOptions = { + searchEngine: SearchEngine; + logger: Logger; +}; /** * Parameters required to register a collator. + * @beta */ export interface RegisterCollatorParameters { /** @@ -36,6 +47,7 @@ export interface RegisterCollatorParameters { /** * Parameters required to register a decorator + * @beta */ export interface RegisterDecoratorParameters { /** diff --git a/scripts/api-extractor.ts b/scripts/api-extractor.ts index 6635dd8b22..afe2c1a1bb 100644 --- a/scripts/api-extractor.ts +++ b/scripts/api-extractor.ts @@ -213,6 +213,7 @@ const NO_WARNING_PACKAGES = [ 'packages/errors', 'packages/integration', 'packages/integration-react', + 'packages/search-common', 'packages/techdocs-common', 'packages/test-utils', 'packages/theme', @@ -235,6 +236,7 @@ const NO_WARNING_PACKAGES = [ 'plugins/scaffolder-backend-module-rails', 'plugins/scaffolder-backend-module-yeoman', 'plugins/scaffolder-common', + 'plugins/search-backend-node', 'plugins/techdocs-backend', 'plugins/tech-insights', 'plugins/tech-insights-backend', From 81f27861b2ddc013405291587bc2f6b528e2ec3c Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 2 Mar 2022 17:22:43 +0100 Subject: [PATCH 131/353] More clearly mark breaking changes as such. Signed-off-by: Eric Peterson --- .changeset/search-blankly-have-a-nice-life.md | 10 +++++++--- .changeset/search-done-me-no-favor.md | 6 ++++++ .changeset/search-just-smile-politely.md | 2 +- .changeset/search-selfless-cold-composed.md | 6 ++++++ .changeset/search-throw-me-a-right.md | 2 +- 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/.changeset/search-blankly-have-a-nice-life.md b/.changeset/search-blankly-have-a-nice-life.md index eb6d02110c..63dc6051bd 100644 --- a/.changeset/search-blankly-have-a-nice-life.md +++ b/.changeset/search-blankly-have-a-nice-life.md @@ -3,12 +3,16 @@ '@backstage/search-common': minor --- +**BREAKING** + The Backstage Search Platform's indexing process has been rewritten as a stream pipeline in order to improve efficiency and performance on large document sets. The concepts of `Collator` and `Decorator` have been replaced with readable and transform object streams (respectively), as well as factory classes to -instantiate them. +instantiate them. Accordingly, the `SearchEngine.index()` method has also been +replaced with a `getIndexer()` factory method that resolves to a writable +object stream. -Accordingly, the `SearchEngine.index()` method has also been replaced with a -`getIndexer()` factory method that resolves to a writable object stream. +Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) +for further details. diff --git a/.changeset/search-done-me-no-favor.md b/.changeset/search-done-me-no-favor.md index 620c9dd923..0b4e24c4a8 100644 --- a/.changeset/search-done-me-no-favor.md +++ b/.changeset/search-done-me-no-favor.md @@ -2,5 +2,11 @@ '@backstage/plugin-search-backend-module-pg': minor --- +**BREAKING** + The `PgSearchEngine` implements the new stream-based indexing process expected by the latest `@backstage/search-backend-node`. + +When updating to this version, you must also update to the latest version of +`@backstage/search-backend-node`. Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) +for further details. diff --git a/.changeset/search-just-smile-politely.md b/.changeset/search-just-smile-politely.md index e1cb57a17e..a7831155da 100644 --- a/.changeset/search-just-smile-politely.md +++ b/.changeset/search-just-smile-politely.md @@ -9,5 +9,5 @@ continue to be available for those unable to upgrade to the stream-based marked as deprecated and will be removed in a future version. To upgrade this plugin and the search indexing subsystem in one go, check -[this changelog](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md) +[this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) for necessary changes to your search backend plugin configuration. diff --git a/.changeset/search-selfless-cold-composed.md b/.changeset/search-selfless-cold-composed.md index 31c0e86baf..7e7dafd421 100644 --- a/.changeset/search-selfless-cold-composed.md +++ b/.changeset/search-selfless-cold-composed.md @@ -2,5 +2,11 @@ '@backstage/plugin-search-backend-module-elasticsearch': minor --- +**BREAKING** + The `ElasticSearchSearchEngine` implements the new stream-based indexing process expected by the latest `@backstage/search-backend-node`. + +When updating to this version, you must also update to the latest version of +`@backstage/search-backend-node`. Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) +for further details. diff --git a/.changeset/search-throw-me-a-right.md b/.changeset/search-throw-me-a-right.md index e504888cfb..32f4e82fe4 100644 --- a/.changeset/search-throw-me-a-right.md +++ b/.changeset/search-throw-me-a-right.md @@ -9,5 +9,5 @@ continue to be available for those unable to upgrade to the stream-based marked as deprecated and will be removed in a future version. To upgrade this plugin and the search indexing subsystem in one go, check -[this changelog](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md) +[this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) for necessary changes to your search backend plugin configuration. From fc8b84aa2c50a96e1644cf043562cea295777dd2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 2 Mar 2022 18:22:34 +0100 Subject: [PATCH 132/353] vocab: removed old code of conduct words Signed-off-by: Patrik Oldsberg --- .github/styles/vocab.txt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 6359493d4b..c3097ad3f0 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -20,7 +20,6 @@ Autoscaling autoselect Avro aws -backrub backported backporting Bigtable @@ -39,8 +38,6 @@ Changesets chanwit Chanwit ci -cisphobia -cissexist classname cli cloudbuild @@ -68,7 +65,6 @@ css Datadog dataflow dayjs -deadnaming debounce Debounce declaratively @@ -171,7 +167,6 @@ Minikube Minio misconfiguration misconfigured -misgendering mkdocs Mkdocs monorepo From 8dcf469c14aae508485bcc31ad413f13a619aca3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 2 Mar 2022 19:15:02 +0100 Subject: [PATCH 133/353] home: fixed starred entities initialization in stories Signed-off-by: Patrik Oldsberg --- .../StarredEntities.stories.tsx | 24 ++++++------------- .../src/templates/DefaultTemplate.stories.tsx | 18 ++++++-------- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx b/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx index 315c0d0d16..1b01820482 100644 --- a/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx +++ b/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx @@ -15,11 +15,7 @@ */ import { HomePageStarredEntities } from '../../plugin'; -import { - wrapInTestApp, - TestApiProvider, - MockStorageApi, -} from '@backstage/test-utils'; +import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils'; import { starredEntitiesApiRef, MockStarredEntitiesApi, @@ -28,24 +24,18 @@ import { import { Grid } from '@material-ui/core'; import React, { ComponentType } from 'react'; -const mockStorageApi = MockStorageApi.create(); -mockStorageApi - .forBucket('starredEntities') - .set('entityRefs', [ - 'component:default/example-starred-entity', - 'component:default/example-starred-entity-2', - 'component:default/example-starred-entity-3', - 'component:default/example-starred-entity-4', - ]); +const starredEntitiesApi = new MockStarredEntitiesApi(); +starredEntitiesApi.toggleStarred('component:default/example-starred-entity'); +starredEntitiesApi.toggleStarred('component:default/example-starred-entity-2'); +starredEntitiesApi.toggleStarred('component:default/example-starred-entity-3'); +starredEntitiesApi.toggleStarred('component:default/example-starred-entity-4'); export default { title: 'Plugins/Home/Components/StarredEntities', decorators: [ (Story: ComponentType<{}>) => wrapInTestApp( - + , { diff --git a/plugins/home/src/templates/DefaultTemplate.stories.tsx b/plugins/home/src/templates/DefaultTemplate.stories.tsx index 5dc4ff61a0..c726294ba6 100644 --- a/plugins/home/src/templates/DefaultTemplate.stories.tsx +++ b/plugins/home/src/templates/DefaultTemplate.stories.tsx @@ -21,7 +21,7 @@ import { HomePageCompanyLogo, HomePageStarredEntities, } from '../plugin'; -import { wrapInTestApp, TestApiProvider, MockStorageApi} from '@backstage/test-utils'; +import { wrapInTestApp, TestApiProvider} from '@backstage/test-utils'; import { Content, Page, InfoCard } from '@backstage/core-components'; import { starredEntitiesApiRef, @@ -37,15 +37,11 @@ import { import { Grid, makeStyles } from '@material-ui/core'; import React, { ComponentType } from 'react'; -const mockStorageApi = MockStorageApi.create(); -mockStorageApi - .forBucket('starredEntities') - .set('entityRefs', [ - 'component:default/example-starred-entity', - 'component:default/example-starred-entity-2', - 'component:default/example-starred-entity-3', - 'component:default/example-starred-entity-4' - ]); +const starredEntitiesApi = new MockStarredEntitiesApi(); +starredEntitiesApi.toggleStarred('component:default/example-starred-entity'); +starredEntitiesApi.toggleStarred('component:default/example-starred-entity-2'); +starredEntitiesApi.toggleStarred('component:default/example-starred-entity-3'); +starredEntitiesApi.toggleStarred('component:default/example-starred-entity-4'); export default { title: 'Plugins/Home/Templates', @@ -57,7 +53,7 @@ export default { apis={[ [ starredEntitiesApiRef, - new MockStarredEntitiesApi(), + starredEntitiesApi, ], [searchApiRef, { query: () => Promise.resolve({ results: [] }) }], ]} From 6c7a8796601cc467dff1791f90b215b1a7f57b3d Mon Sep 17 00:00:00 2001 From: slougheed Date: Wed, 2 Mar 2022 14:15:10 -0500 Subject: [PATCH 134/353] Cleanup based on code review and added changeset Signed-off-by: slougheed --- .changeset/tidy-hairs-sip.md | 5 +++ .../src/actions/fetch/cookiecutter.ts | 39 +++++++------------ 2 files changed, 20 insertions(+), 24 deletions(-) create mode 100644 .changeset/tidy-hairs-sip.md diff --git a/.changeset/tidy-hairs-sip.md b/.changeset/tidy-hairs-sip.md new file mode 100644 index 0000000000..512447236d --- /dev/null +++ b/.changeset/tidy-hairs-sip.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +--- + +Fixed bug where existing cookiecutter.json file is not used. diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts index efc8b6215e..01174e0f7a 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts @@ -57,39 +57,36 @@ export class CookiecutterRunner { workspacePath, values, logStream, + imageName, + templateDir, + templateContentsDir, }: { workspacePath: string; values: JsonObject; logStream: Writable; + imageName?: string; + templateDir: string; + templateContentsDir: string; }): Promise { const intermediateDir = path.join(workspacePath, 'intermediate'); await fs.ensureDir(intermediateDir); const resultDir = path.join(workspacePath, 'result'); - const { - templateContentsDir, - templateDir, - imageName, - ...valuesForCookieCutterJson - } = values; // First lets grab the default cookiecutter.json file const cookieCutterJson = await this.fetchTemplateCookieCutter( - templateContentsDir as string, + templateContentsDir, ); const cookieInfo = { ...cookieCutterJson, - ...valuesForCookieCutterJson, + ...values, }; - await fs.writeJSON( - path.join(templateDir as string, 'cookiecutter.json'), - cookieInfo, - ); + await fs.writeJSON(path.join(templateDir, 'cookiecutter.json'), cookieInfo); // Directories to bind on container const mountDirs = { - [templateDir as string]: '/input', + [templateDir]: '/input', [intermediateDir]: '/output', }; @@ -100,13 +97,7 @@ export class CookiecutterRunner { if (cookieCutterInstalled) { await runCommand({ command: 'cookiecutter', - args: [ - '--no-input', - '-o', - intermediateDir, - templateDir as string, - '--verbose', - ], + args: ['--no-input', '-o', intermediateDir, templateDir, '--verbose'], logStream, }); } else { @@ -247,16 +238,16 @@ export function createFetchCookiecutterAction(options: { ...ctx.input.values, _copy_without_render: ctx.input.copyWithoutRender, _extensions: ctx.input.extensions, - imageName: ctx.input.imageName, - templateDir: templateDir, - templateContentsDir: templateContentsDir, }; // Will execute the template in ./template and put the result in ./result await cookiecutter.run({ workspacePath: workDir, logStream: ctx.logStream, - values, + values: values, + imageName: ctx.input.imageName, + templateDir: templateDir, + templateContentsDir: templateContentsDir, }); // Finally move the template result into the task workspace From 7d533a7b5b7db11ba8ca1358a08c660cb1f86aa7 Mon Sep 17 00:00:00 2001 From: slougheed Date: Wed, 2 Mar 2022 14:25:20 -0500 Subject: [PATCH 135/353] Removed string casting of imageName Signed-off-by: slougheed --- .../src/actions/fetch/cookiecutter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts index 4e09c81311..7575627506 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts @@ -102,7 +102,7 @@ export class CookiecutterRunner { }); } else { await this.containerRunner.runContainer({ - imageName: (imageName as string) ?? 'spotify/backstage-cookiecutter', + imageName: imageName ?? 'spotify/backstage-cookiecutter', command: 'cookiecutter', args: ['--no-input', '-o', '/output', '/input', '--verbose'], mountDirs, From 36aa63022baa3671d6ed22398b04ec9b30227475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 2 Mar 2022 21:26:22 +0100 Subject: [PATCH 136/353] deprecate EntityName, introduce CompoundEntityRef deprecate getEntityName, introduce getCompoundEntityRef MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/nasty-pets-join.md | 9 +++++ .changeset/tricky-students-promise.md | 22 ++++++++++++ packages/catalog-client/api-report.md | 6 ++-- packages/catalog-client/src/CatalogClient.ts | 4 +-- packages/catalog-client/src/types/api.ts | 4 +-- packages/catalog-model/api-report.md | 26 ++++++++------ packages/catalog-model/src/entity/Entity.ts | 4 +-- packages/catalog-model/src/entity/index.ts | 1 + packages/catalog-model/src/entity/ref.ts | 25 +++++++++---- packages/catalog-model/src/index.ts | 2 +- packages/catalog-model/src/types.ts | 13 +++++-- .../techdocs-cli-embedded-app/src/apis.ts | 10 +++--- .../components/TechDocsPage/TechDocsPage.tsx | 4 +-- packages/techdocs-common/api-report.md | 6 ++-- .../src/stages/publish/awsS3.ts | 4 +-- .../src/stages/publish/azureBlobStorage.ts | 4 +-- .../src/stages/publish/googleStorage.ts | 6 ++-- .../src/stages/publish/local.ts | 4 +-- .../src/stages/publish/openStackSwift.test.ts | 4 +-- .../src/stages/publish/openStackSwift.ts | 4 +-- .../src/stages/publish/types.ts | 6 ++-- .../src/lib/catalog/CatalogIdentityClient.ts | 4 +-- plugins/catalog-backend/api-report.md | 6 ++-- plugins/catalog-backend/src/api/common.ts | 6 ++-- .../core/BuiltinKindsEntityProcessor.ts | 4 +-- plugins/catalog-graph/api-report.md | 4 +-- plugins/catalog-graph/dev/index.tsx | 6 ++-- .../CatalogGraphCard/CatalogGraphCard.tsx | 4 +-- .../CatalogGraphPage/useCatalogGraphPage.ts | 17 ++++----- .../EntityRelationsGraph.tsx | 7 ++-- plugins/catalog-import/api-report.md | 10 +++--- plugins/catalog-import/dev/index.tsx | 4 +-- .../src/api/CatalogImportApi.ts | 4 +-- .../src/api/CatalogImportClient.ts | 4 +-- .../EntityListComponent.tsx | 9 +++-- .../src/components/useImportState.test.tsx | 4 +-- .../src/components/useImportState.ts | 6 ++-- plugins/catalog-react/api-report.md | 22 +++++++----- .../EntityRefLink/EntityRefLink.tsx | 4 +-- .../EntityRefLink/EntityRefLinks.tsx | 4 +-- .../src/components/EntityRefLink/humanize.ts | 4 +-- .../src/components/EntityTable/columns.tsx | 4 +-- .../useUnregisterEntityDialogState.ts | 8 ++--- .../src/hooks/useStarredEntities.ts | 18 ++++++---- .../src/hooks/useStarredEntity.test.tsx | 4 +-- .../src/hooks/useStarredEntity.ts | 10 ++++-- .../src/utils/getEntityRelations.ts | 8 +++-- plugins/catalog-react/src/utils/isOwnerOf.ts | 4 +-- plugins/catalog/api-report.md | 6 ++-- .../src/components/CatalogTable/types.ts | 6 ++-- .../src/service/types.ts | 6 ++-- plugins/code-coverage/src/api.ts | 19 ++++++---- plugins/code-coverage/src/types.ts | 7 ++-- .../src/components/FossaPage/FossaPage.tsx | 4 +-- plugins/jenkins-backend/api-report.md | 6 ++-- .../src/service/jenkinsInfoProvider.test.ts | 4 +-- .../src/service/jenkinsInfoProvider.ts | 6 ++-- .../src/service/standaloneServer.ts | 6 ++-- plugins/jenkins/api-report.md | 14 ++++---- plugins/jenkins/src/api/JenkinsApi.ts | 14 ++++---- .../src/components/useBuildWithSteps.ts | 4 +-- plugins/jenkins/src/components/useBuilds.ts | 6 ++-- .../processor/ScaffolderEntitiesProcessor.ts | 4 +-- .../scaffolder-backend/src/service/helpers.ts | 4 +-- .../src/service/router.ts | 8 +++-- plugins/tech-insights/api-report.md | 9 +++-- .../tech-insights/src/api/TechInsightsApi.ts | 9 +++-- .../src/api/TechInsightsClient.ts | 6 ++-- .../src/service/CachedEntityLoader.test.ts | 4 +-- .../src/service/CachedEntityLoader.ts | 6 ++-- plugins/techdocs/api-report.md | 36 ++++++++++--------- plugins/techdocs/dev/index.tsx | 7 ++-- plugins/techdocs/src/api.ts | 14 ++++---- plugins/techdocs/src/client.ts | 17 +++++---- .../src/home/components/Tables/types.ts | 4 +-- .../techdocs/src/reader/components/Reader.tsx | 34 +++++++++--------- .../reader/components/TechDocsReaderPage.tsx | 6 ++-- .../components/TechDocsReaderPageHeader.tsx | 4 +-- .../src/reader/components/useRawPage.ts | 4 +-- .../src/reader/transformers/addBaseUrl.ts | 4 +-- .../src/search/components/TechDocsSearch.tsx | 4 +-- plugins/todo-backend/api-report.md | 4 +-- plugins/todo-backend/src/service/router.ts | 4 +-- plugins/todo-backend/src/service/types.ts | 4 +-- 84 files changed, 393 insertions(+), 268 deletions(-) create mode 100644 .changeset/nasty-pets-join.md create mode 100644 .changeset/tricky-students-promise.md diff --git a/.changeset/nasty-pets-join.md b/.changeset/nasty-pets-join.md new file mode 100644 index 0000000000..1ff07274cf --- /dev/null +++ b/.changeset/nasty-pets-join.md @@ -0,0 +1,9 @@ +--- +'@backstage/catalog-model': patch +--- + +**DEPRECATION**: Deprecated the `EntityName` type, and added the better-named `CompoundEntityRef` to replace it. + +**DEPRECATION**: Deprecated the `getEntityName` function, and added the better-named `getCompoundEntityRef` to replace it. + +Please switch over to using the new symbols, as the old ones may be removed in a future release. diff --git a/.changeset/tricky-students-promise.md b/.changeset/tricky-students-promise.md new file mode 100644 index 0000000000..b9c6150a4a --- /dev/null +++ b/.changeset/tricky-students-promise.md @@ -0,0 +1,22 @@ +--- +'@backstage/catalog-client': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-graph': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-code-coverage': patch +'@backstage/plugin-code-coverage-backend': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-jenkins': patch +'@backstage/plugin-jenkins-backend': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-tech-insights': patch +'@backstage/plugin-tech-insights-backend': patch +'@backstage/plugin-techdocs': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-todo-backend': patch +--- + +Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index c957859fec..ce6397f51e 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -3,8 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; -import { EntityName } from '@backstage/catalog-model'; // @public export type AddLocationRequest = { @@ -39,7 +39,7 @@ export interface CatalogApi { options?: CatalogRequestOptions, ): Promise; getEntityByName( - name: EntityName, + name: CompoundEntityRef, options?: CatalogRequestOptions, ): Promise; getEntityFacets( @@ -91,7 +91,7 @@ export class CatalogClient implements CatalogApi { options?: CatalogRequestOptions, ): Promise; getEntityByName( - compoundName: EntityName, + compoundName: CompoundEntityRef, options?: CatalogRequestOptions, ): Promise; getEntityFacets( diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index bb3c9604b3..da580ae09c 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -18,7 +18,7 @@ import { ANNOTATION_LOCATION, ANNOTATION_ORIGIN_LOCATION, Entity, - EntityName, + CompoundEntityRef, parseEntityRef, stringifyEntityRef, stringifyLocationRef, @@ -174,7 +174,7 @@ export class CatalogClient implements CatalogApi { * {@inheritdoc CatalogApi.getEntityByName} */ async getEntityByName( - compoundName: EntityName, + compoundName: CompoundEntityRef, options?: CatalogRequestOptions, ): Promise { const { kind, namespace = 'default', name } = compoundName; diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index 4a86ca41f3..0ee2b57ffb 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; /** * This symbol can be used in place of a value when passed to filters in e.g. @@ -310,7 +310,7 @@ export interface CatalogApi { * @param options - Additional options */ getEntityByName( - name: EntityName, + name: CompoundEntityRef, options?: CatalogRequestOptions, ): Promise; diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 4574129d6f..99b4b39038 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -88,6 +88,13 @@ export { ComponentEntityV1alpha1 }; // @public export const componentEntityV1alpha1Validator: KindValidator; +// @public +export type CompoundEntityRef = { + kind: string; + namespace: string; + name: string; +}; + // @public export const DEFAULT_NAMESPACE = 'default'; @@ -166,12 +173,8 @@ export type EntityMeta = JsonObject & { links?: EntityLink[]; }; -// @public -export type EntityName = { - kind: string; - namespace: string; - name: string; -}; +// @public @deprecated +export type EntityName = CompoundEntityRef; // @public export const EntityPolicies: { @@ -196,7 +199,7 @@ export type EntityRef = // @public export type EntityRelation = { type: string; - target: EntityName; + target: CompoundEntityRef; targetRef: string; }; @@ -229,7 +232,10 @@ export class FieldFormatEntityPolicy implements EntityPolicy { } // @public -export function getEntityName(entity: Entity): EntityName; +export function getCompoundEntityRef(entity: Entity): CompoundEntityRef; + +// @public @deprecated +export const getEntityName: typeof getCompoundEntityRef; // @public export function getEntitySourceLocation(entity: Entity): { @@ -337,7 +343,7 @@ export function parseEntityName( defaultKind?: string; defaultNamespace?: string; }, -): EntityName; +): CompoundEntityRef; // @public export function parseEntityRef( @@ -352,7 +358,7 @@ export function parseEntityRef( defaultKind?: string; defaultNamespace?: string; }, -): EntityName; +): CompoundEntityRef; // @public export function parseLocationRef(ref: string): { diff --git a/packages/catalog-model/src/entity/Entity.ts b/packages/catalog-model/src/entity/Entity.ts index ef9b4352da..029d9e095b 100644 --- a/packages/catalog-model/src/entity/Entity.ts +++ b/packages/catalog-model/src/entity/Entity.ts @@ -15,7 +15,7 @@ */ import { JsonObject } from '@backstage/types'; -import { EntityName } from '../types'; +import { CompoundEntityRef } from '../types'; import { EntityStatus } from './EntityStatus'; /** @@ -201,7 +201,7 @@ export type EntityRelation = { * * @deprecated use targetRef instead */ - target: EntityName; + target: CompoundEntityRef; /** * The entity ref of the target of this relation. diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts index 3a346b217c..fb9eee0353 100644 --- a/packages/catalog-model/src/entity/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -34,6 +34,7 @@ export type { } from './EntityStatus'; export * from './policies'; export { + getCompoundEntityRef, getEntityName, parseEntityName, parseEntityRef, diff --git a/packages/catalog-model/src/entity/ref.ts b/packages/catalog-model/src/entity/ref.ts index f9bfcf38cd..4529b1174c 100644 --- a/packages/catalog-model/src/entity/ref.ts +++ b/packages/catalog-model/src/entity/ref.ts @@ -15,7 +15,7 @@ */ import { DEFAULT_NAMESPACE } from './constants'; -import { EntityName } from '../types'; +import { CompoundEntityRef } from '../types'; import { Entity } from './Entity'; function parseRefString(ref: string): { @@ -38,14 +38,25 @@ function parseRefString(ref: string): { } /** - * Extracts the kind, namespace and name that form the name triplet of the - * given entity. + * Extracts the kind, namespace and name that form the compound entity ref + * triplet of the given entity. + * + * @public + * @deprecated Use getCompoundEntityRef instead + * @param entity - An entity + * @returns The compound entity ref + */ +export const getEntityName = getCompoundEntityRef; + +/** + * Extracts the kind, namespace and name that form the compound entity ref + * triplet of the given entity. * * @public * @param entity - An entity - * @returns The complete entity name + * @returns The compound entity ref */ -export function getEntityName(entity: Entity): EntityName { +export function getCompoundEntityRef(entity: Entity): CompoundEntityRef { return { kind: entity.kind, namespace: entity.metadata.namespace || DEFAULT_NAMESPACE, @@ -77,7 +88,7 @@ export function parseEntityName( /** The default namespace, if none is given in the reference */ defaultNamespace?: string; } = {}, -): EntityName { +): CompoundEntityRef { const { kind, namespace, name } = parseEntityRef(ref, { defaultNamespace: DEFAULT_NAMESPACE, ...context, @@ -114,7 +125,7 @@ export function parseEntityRef( /** The default namespace, if none is given in the reference */ defaultNamespace?: string; }, -): EntityName { +): CompoundEntityRef { if (!ref) { throw new Error(`Entity reference must not be empty`); } diff --git a/packages/catalog-model/src/index.ts b/packages/catalog-model/src/index.ts index 986341bd33..19eacd4925 100644 --- a/packages/catalog-model/src/index.ts +++ b/packages/catalog-model/src/index.ts @@ -24,5 +24,5 @@ export * from './entity'; export { EntityPolicies } from './EntityPolicies'; export * from './kinds'; export * from './location'; -export type { EntityName, EntityRef } from './types'; +export type { EntityName, EntityRef, CompoundEntityRef } from './types'; export * from './validation'; diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts index 5b5f063ca3..e7d819ace9 100644 --- a/packages/catalog-model/src/types.ts +++ b/packages/catalog-model/src/types.ts @@ -15,16 +15,25 @@ */ /** - * A complete entity name, with the full kind-namespace-name triplet. + * All parts of a complete entity ref, forming a full kind-namespace-name + * triplet. * * @public */ -export type EntityName = { +export type CompoundEntityRef = { kind: string; namespace: string; name: string; }; +/** + * A complete entity name, with the full kind-namespace-name triplet. + * + * @deprecated Use CompoundEntityRef instead + * @public + */ +export type EntityName = CompoundEntityRef; + /** * A reference by name to an entity, either as a compact string representation, * or as a compound reference structure. diff --git a/packages/techdocs-cli-embedded-app/src/apis.ts b/packages/techdocs-cli-embedded-app/src/apis.ts index 6234e3b719..1fe6ba8086 100644 --- a/packages/techdocs-cli-embedded-app/src/apis.ts +++ b/packages/techdocs-cli-embedded-app/src/apis.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { scmIntegrationsApiRef, @@ -81,7 +81,7 @@ class TechDocsDevStorageApi implements TechDocsStorageApi { return this.configApi.getString('techdocs.builder'); } - async getEntityDocs(_entityId: EntityName, path: string) { + async getEntityDocs(_entityId: CompoundEntityRef, path: string) { const apiOrigin = await this.getApiOrigin(); // Irrespective of the entity, use mkdocs server to find the file for the path. const url = `${apiOrigin}/${path}`; @@ -97,7 +97,7 @@ class TechDocsDevStorageApi implements TechDocsStorageApi { return request.text(); } - async syncEntityDocs(_: EntityName): Promise { + async syncEntityDocs(_: CompoundEntityRef): Promise { // this is just stub of this function as we don't need to check if docs are up to date, // we always want to retrigger a new build return 'cached'; @@ -106,7 +106,7 @@ class TechDocsDevStorageApi implements TechDocsStorageApi { // Used by transformer to modify the request to assets (CSS, Image) from inside the HTML. async getBaseUrl( oldBaseUrl: string, - _entityId: EntityName, + _entityId: CompoundEntityRef, path: string, ): Promise { const apiOrigin = await this.getApiOrigin(); @@ -154,7 +154,7 @@ class TechDocsDevApi implements TechDocsApi { }; } - async getTechDocsMetadata(_entityId: EntityName) { + async getTechDocsMetadata(_entityId: CompoundEntityRef) { return { site_name: 'Live preview environment', site_description: '', diff --git a/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx b/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx index d9a518c165..f2dcefc00e 100644 --- a/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx +++ b/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx @@ -29,7 +29,7 @@ import LightIcon from '@material-ui/icons/Brightness7'; import DarkIcon from '@material-ui/icons/Brightness4'; import { lightTheme, darkTheme } from '@backstage/theme'; -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Content } from '@backstage/core-components'; @@ -127,7 +127,7 @@ const TechDocsPageContent = ({ onReady, entityRef, }: { - entityRef: EntityName; + entityRef: CompoundEntityRef; onReady: () => void; }) => { const classes = useStyles(); diff --git a/packages/techdocs-common/api-report.md b/packages/techdocs-common/api-report.md index 7c4fc95abb..13d610f66b 100644 --- a/packages/techdocs-common/api-report.md +++ b/packages/techdocs-common/api-report.md @@ -5,10 +5,10 @@ ```ts /// +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { ContainerRunner } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; -import { EntityName } from '@backstage/catalog-model'; import express from 'express'; import { IndexableDocument } from '@backstage/search-common'; import { Logger as Logger_2 } from 'winston'; @@ -157,7 +157,9 @@ export class Publisher { // @public export interface PublisherBase { docsRouter(): express.Handler; - fetchTechDocsMetadata(entityName: EntityName): Promise; + fetchTechDocsMetadata( + entityName: CompoundEntityRef, + ): Promise; getReadiness(): Promise; hasDocsBeenGenerated(entityName: Entity): Promise; migrateDocsCase?(migrateRequest: MigrateRequest): Promise; diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts index ed76edc0cb..e5914d2fdf 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { assertError, ForwardedError } from '@backstage/errors'; import aws, { Credentials } from 'aws-sdk'; @@ -321,7 +321,7 @@ export class AwsS3Publish implements PublisherBase { } async fetchTechDocsMetadata( - entityName: EntityName, + entityName: CompoundEntityRef, ): Promise { try { return await new Promise(async (resolve, reject) => { diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts index b082079be2..bcbc10a94d 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts @@ -19,7 +19,7 @@ import { ContainerClient, StorageSharedKeyCredential, } from '@azure/storage-blob'; -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { assertError, ForwardedError } from '@backstage/errors'; import express from 'express'; @@ -300,7 +300,7 @@ export class AzureBlobStoragePublish implements PublisherBase { } async fetchTechDocsMetadata( - entityName: EntityName, + entityName: CompoundEntityRef, ): Promise { const entityTriplet = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; const entityRootDir = this.legacyPathCasing diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.ts b/packages/techdocs-common/src/stages/publish/googleStorage.ts index c3d9d3330b..987dfc6f09 100644 --- a/packages/techdocs-common/src/stages/publish/googleStorage.ts +++ b/packages/techdocs-common/src/stages/publish/googleStorage.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { assertError } from '@backstage/errors'; import { File, FileExistsResponse, Storage } from '@google-cloud/storage'; @@ -238,7 +238,9 @@ export class GoogleGCSPublish implements PublisherBase { return { objects }; } - fetchTechDocsMetadata(entityName: EntityName): Promise { + fetchTechDocsMetadata( + entityName: CompoundEntityRef, + ): Promise { return new Promise((resolve, reject) => { const entityTriplet = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; const entityDir = this.legacyPathCasing diff --git a/packages/techdocs-common/src/stages/publish/local.ts b/packages/techdocs-common/src/stages/publish/local.ts index 9c146d2935..af9bb30d46 100644 --- a/packages/techdocs-common/src/stages/publish/local.ts +++ b/packages/techdocs-common/src/stages/publish/local.ts @@ -17,7 +17,7 @@ import { PluginEndpointDiscovery, resolvePackagePath, } from '@backstage/backend-common'; -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import express from 'express'; import fs from 'fs-extra'; @@ -142,7 +142,7 @@ export class LocalPublish implements PublisherBase { } async fetchTechDocsMetadata( - entityName: EntityName, + entityName: CompoundEntityRef, ): Promise { const metadataPath = this.staticEntityPathJoin( entityName.namespace, diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts index b240ce73da..da0a60343f 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts @@ -17,7 +17,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import { Entity, - EntityName, + CompoundEntityRef, DEFAULT_NAMESPACE, } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; @@ -45,7 +45,7 @@ const createMockEntity = (annotations = {}): Entity => { }; }; -const createMockEntityName = (): EntityName => ({ +const createMockEntityName = (): CompoundEntityRef => ({ kind: 'TestKind', name: 'test-component-name', namespace: 'test-namespace', diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.ts index 62b40f9a76..734a7e4451 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.ts +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import express from 'express'; import fs from 'fs-extra'; @@ -194,7 +194,7 @@ export class OpenStackSwiftPublish implements PublisherBase { } async fetchTechDocsMetadata( - entityName: EntityName, + entityName: CompoundEntityRef, ): Promise { return await new Promise(async (resolve, reject) => { const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; diff --git a/packages/techdocs-common/src/stages/publish/types.ts b/packages/techdocs-common/src/stages/publish/types.ts index c6ad032484..86772c25f4 100644 --- a/packages/techdocs-common/src/stages/publish/types.ts +++ b/packages/techdocs-common/src/stages/publish/types.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { Logger } from 'winston'; import express from 'express'; @@ -133,7 +133,9 @@ export interface PublisherBase { * Retrieve TechDocs Metadata about a site e.g. name, contributors, last updated, etc. * This API uses the techdocs_metadata.json file that co-exists along with the generated docs. */ - fetchTechDocsMetadata(entityName: EntityName): Promise; + fetchTechDocsMetadata( + entityName: CompoundEntityRef, + ): Promise; /** * Route middleware to serve static documentation files for an entity. diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index d948af2e05..502da913af 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -18,7 +18,7 @@ import { Logger } from 'winston'; import { ConflictError, NotFoundError } from '@backstage/errors'; import { CatalogApi } from '@backstage/catalog-client'; import { - EntityName, + CompoundEntityRef, parseEntityRef, RELATION_MEMBER_OF, stringifyEntityRef, @@ -96,7 +96,7 @@ export class CatalogIdentityClient { return null; } }) - .filter((ref): ref is EntityName => ref !== null); + .filter((ref): ref is CompoundEntityRef => ref !== null); const filter = resolvedEntityRefs.map(ref => ({ kind: ref.kind, diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index d5541678e6..ee7b419a71 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -7,12 +7,12 @@ import { BitbucketIntegration } from '@backstage/integration'; import { CatalogApi } from '@backstage/catalog-client'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { ConditionalPolicyDecision } from '@backstage/plugin-permission-node'; import { Conditions } from '@backstage/plugin-permission-node'; import { Config } from '@backstage/config'; import { DocumentCollatorFactory } from '@backstage/search-common'; import { Entity } from '@backstage/catalog-model'; -import { EntityName } from '@backstage/catalog-model'; import { EntityPolicy } from '@backstage/catalog-model'; import express from 'express'; import { GetEntitiesRequest } from '@backstage/catalog-client'; @@ -649,9 +649,9 @@ export type EntityProviderMutation = // @public export type EntityRelationSpec = { - source: EntityName; + source: CompoundEntityRef; type: string; - target: EntityName; + target: CompoundEntityRef; }; // @public (undocumented) diff --git a/plugins/catalog-backend/src/api/common.ts b/plugins/catalog-backend/src/api/common.ts index 1fd09aec32..f3b4a387ba 100644 --- a/plugins/catalog-backend/src/api/common.ts +++ b/plugins/catalog-backend/src/api/common.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; /** * Holds the entity location information. @@ -42,7 +42,7 @@ export type EntityRelationSpec = { /** * The source entity of this relation. */ - source: EntityName; + source: CompoundEntityRef; /** * The type of the relation. @@ -52,5 +52,5 @@ export type EntityRelationSpec = { /** * The target entity of this relation. */ - target: EntityName; + target: CompoundEntityRef; }; diff --git a/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts index 548cdf3115..9e420b9d65 100644 --- a/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts @@ -22,7 +22,7 @@ import { DomainEntity, domainEntityV1alpha1Validator, Entity, - getEntityName, + getCompoundEntityRef, GroupEntity, groupEntityV1alpha1Validator, locationEntityV1alpha1Validator, @@ -93,7 +93,7 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { _location: LocationSpec, emit: CatalogProcessorEmit, ): Promise { - const selfRef = getEntityName(entity); + const selfRef = getCompoundEntityRef(entity); /* * Utilities diff --git a/plugins/catalog-graph/api-report.md b/plugins/catalog-graph/api-report.md index 515a578d3c..82db1dac3b 100644 --- a/plugins/catalog-graph/api-report.md +++ b/plugins/catalog-graph/api-report.md @@ -6,8 +6,8 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { DependencyGraphTypes } from '@backstage/core-components'; -import { EntityName } from '@backstage/catalog-model'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; import { MouseEvent as MouseEvent_2 } from 'react'; @@ -129,7 +129,7 @@ export const EntityRelationsGraph: ({ className, zoom, }: { - rootEntityNames: EntityName | EntityName[]; + rootEntityNames: CompoundEntityRef | CompoundEntityRef[]; maxDepth?: number | undefined; unidirectional?: boolean | undefined; mergeRelations?: boolean | undefined; diff --git a/plugins/catalog-graph/dev/index.tsx b/plugins/catalog-graph/dev/index.tsx index 62d5eccef3..7413634497 100644 --- a/plugins/catalog-graph/dev/index.tsx +++ b/plugins/catalog-graph/dev/index.tsx @@ -16,7 +16,7 @@ import { GetEntitiesResponse } from '@backstage/catalog-client'; import { Entity, - EntityName, + CompoundEntityRef, DEFAULT_NAMESPACE, RELATION_API_CONSUMED_BY, RELATION_API_PROVIDED_BY, @@ -139,7 +139,9 @@ createDevApp() deps: {}, factory() { return { - async getEntityByName(name: EntityName): Promise { + async getEntityByName( + name: CompoundEntityRef, + ): Promise { return entities[stringifyEntityRef(name)]; }, async getEntities(): Promise { diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx index e6811aad62..4a21418107 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import { - getEntityName, + getCompoundEntityRef, parseEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; @@ -77,7 +77,7 @@ export const CatalogGraphCard = ({ zoom?: 'enabled' | 'disabled' | 'enable-on-click'; }) => { const { entity } = useEntity(); - const entityName = getEntityName(entity); + const entityName = getCompoundEntityRef(entity); const catalogEntityRoute = useRouteRef(entityRouteRef); const catalogGraphRoute = useRouteRef(catalogGraphRouteRef); const navigate = useNavigate(); diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts b/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts index 198cbd397f..308418f612 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/useCatalogGraphPage.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { - EntityName, + CompoundEntityRef, parseEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; @@ -32,8 +32,8 @@ import usePrevious from 'react-use/lib/usePrevious'; import { Direction } from '../EntityRelationsGraph'; export type CatalogGraphPageValue = { - rootEntityNames: EntityName[]; - setRootEntityNames: Dispatch>; + rootEntityNames: CompoundEntityRef[]; + setRootEntityNames: Dispatch>; maxDepth: number; setMaxDepth: Dispatch>; selectedRelations: string[] | undefined; @@ -82,11 +82,12 @@ export function useCatalogGraphPage({ ); // Initial state - const [rootEntityNames, setRootEntityNames] = useState(() => - (Array.isArray(query.rootEntityRefs) - ? query.rootEntityRefs - : initialState?.rootEntityRefs ?? [] - ).map(r => parseEntityRef(r)), + const [rootEntityNames, setRootEntityNames] = useState( + () => + (Array.isArray(query.rootEntityRefs) + ? query.rootEntityRefs + : initialState?.rootEntityRefs ?? [] + ).map(r => parseEntityRef(r)), ); const [maxDepth, setMaxDepth] = useState(() => typeof query.maxDepth === 'string' diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx index 76cd44394a..c5c83a54cd 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx @@ -13,7 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityName, stringifyEntityRef } from '@backstage/catalog-model'; +import { + CompoundEntityRef, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { DependencyGraph, DependencyGraphTypes, @@ -77,7 +80,7 @@ export const EntityRelationsGraph = ({ className, zoom = 'enabled', }: { - rootEntityNames: EntityName | EntityName[]; + rootEntityNames: CompoundEntityRef | CompoundEntityRef[]; maxDepth?: number; unidirectional?: boolean; mergeRelations?: boolean; diff --git a/plugins/catalog-import/api-report.md b/plugins/catalog-import/api-report.md index 164d831c96..5a9e8f03fd 100644 --- a/plugins/catalog-import/api-report.md +++ b/plugins/catalog-import/api-report.md @@ -8,11 +8,11 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { ConfigApi } from '@backstage/core-plugin-api'; import { Controller } from 'react-hook-form'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; -import { EntityName } from '@backstage/catalog-model'; import { FieldErrors } from 'react-hook-form'; import { IdentityApi } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; @@ -33,7 +33,7 @@ export type AnalyzeResult = locations: Array<{ target: string; exists?: boolean; - entities: EntityName[]; + entities: CompoundEntityRef[]; }>; } | { @@ -165,7 +165,7 @@ export interface EntityListComponentProps { // (undocumented) locations: Array<{ target: string; - entities: (Entity | EntityName)[]; + entities: (Entity | CompoundEntityRef)[]; }>; // (undocumented) onItemClick?: (target: string) => void; @@ -246,7 +246,7 @@ export type PrepareResult = locations: Array<{ exists?: boolean; target: string; - entities: EntityName[]; + entities: CompoundEntityRef[]; }>; } | { @@ -258,7 +258,7 @@ export type PrepareResult = }; locations: Array<{ target: string; - entities: EntityName[]; + entities: CompoundEntityRef[]; }>; }; diff --git a/plugins/catalog-import/dev/index.tsx b/plugins/catalog-import/dev/index.tsx index a0cf034782..e2e5e1d22c 100644 --- a/plugins/catalog-import/dev/index.tsx +++ b/plugins/catalog-import/dev/index.tsx @@ -15,7 +15,7 @@ */ import { CatalogApi } from '@backstage/catalog-client'; -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { createDevApp } from '@backstage/dev-utils'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Grid, ListItem, ListItemIcon, ListItemText } from '@material-ui/core'; @@ -32,7 +32,7 @@ import { import { ImportPage } from '../src/components/ImportPage'; import { Content, Header, InfoCard, Page } from '@backstage/core-components'; -const getEntityNames = (url: string): EntityName[] => [ +const getEntityNames = (url: string): CompoundEntityRef[] => [ { kind: 'Component', namespace: url.replace(/^.*(folder-[^/]+).*|.*()$/, '$1') || 'default', diff --git a/plugins/catalog-import/src/api/CatalogImportApi.ts b/plugins/catalog-import/src/api/CatalogImportApi.ts index 0923f4ac98..3cfd53470a 100644 --- a/plugins/catalog-import/src/api/CatalogImportApi.ts +++ b/plugins/catalog-import/src/api/CatalogImportApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { createApiRef } from '@backstage/core-plugin-api'; import { PartialEntity } from '../types'; @@ -38,7 +38,7 @@ export type AnalyzeResult = locations: Array<{ target: string; exists?: boolean; - entities: EntityName[]; + entities: CompoundEntityRef[]; }>; } | { diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index b856a4e359..c12e391ec9 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -15,7 +15,7 @@ */ import { CatalogApi } from '@backstage/catalog-client'; -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { ConfigApi, DiscoveryApi, @@ -212,7 +212,7 @@ the component will become available.\n\nFor more information, read an \ }): Promise< Array<{ target: string; - entities: EntityName[]; + entities: CompoundEntityRef[]; }> > { const { url, owner, repo, githubIntegrationConfig } = options; diff --git a/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx b/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx index ceec4b97e8..47f0139e5e 100644 --- a/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx +++ b/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { useApp } from '@backstage/core-plugin-api'; import { EntityRefLink, @@ -41,7 +41,7 @@ const useStyles = makeStyles(theme => ({ }, })); -function sortEntities(entities: Array) { +function sortEntities(entities: Array) { return entities.sort((a, b) => humanizeEntityRef(a).localeCompare(humanizeEntityRef(b)), ); @@ -53,7 +53,10 @@ function sortEntities(entities: Array) { * @public */ export interface EntityListComponentProps { - locations: Array<{ target: string; entities: (Entity | EntityName)[] }>; + locations: Array<{ + target: string; + entities: (Entity | CompoundEntityRef)[]; + }>; locationListItemIcon: (target: string) => React.ReactElement; collapsed?: boolean; firstListItem?: React.ReactElement; diff --git a/plugins/catalog-import/src/components/useImportState.test.tsx b/plugins/catalog-import/src/components/useImportState.test.tsx index 1a01c61449..155adb8290 100644 --- a/plugins/catalog-import/src/components/useImportState.test.tsx +++ b/plugins/catalog-import/src/components/useImportState.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { cleanup } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react-hooks'; import { AnalyzeResult } from '../api'; @@ -37,7 +37,7 @@ describe('useImportState', () => { locations: [ { target: 'https://0', - entities: [] as EntityName[], + entities: [] as CompoundEntityRef[], }, ], }; diff --git a/plugins/catalog-import/src/components/useImportState.ts b/plugins/catalog-import/src/components/useImportState.ts index 0cb6b59917..965f855f30 100644 --- a/plugins/catalog-import/src/components/useImportState.ts +++ b/plugins/catalog-import/src/components/useImportState.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { useReducer } from 'react'; import { AnalyzeResult } from '../api'; @@ -43,7 +43,7 @@ export type PrepareResult = locations: Array<{ exists?: boolean; target: string; - entities: EntityName[]; + entities: CompoundEntityRef[]; }>; } | { @@ -55,7 +55,7 @@ export type PrepareResult = }; locations: Array<{ target: string; - entities: EntityName[]; + entities: CompoundEntityRef[]; }>; }; diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 394a45f278..75414e3d59 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -11,8 +11,8 @@ import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client'; import { CatalogApi } from '@backstage/catalog-client'; import { ComponentEntity } from '@backstage/catalog-model'; import { ComponentProps } from 'react'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; -import { EntityName } from '@backstage/catalog-model'; import { GetEntitiesResponse } from '@backstage/catalog-client'; import { IconButton } from '@material-ui/core'; import { LinkProps } from '@backstage/core-components'; @@ -250,7 +250,7 @@ export const EntityRefLink: (props: EntityRefLinkProps) => JSX.Element; // @public export type EntityRefLinkProps = { - entityRef: Entity | EntityName | string; + entityRef: Entity | CompoundEntityRef | string; defaultKind?: string; title?: string; children?: React_2.ReactNode; @@ -265,7 +265,7 @@ export const EntityRefLinks: ({ // @public export type EntityRefLinksProps = { - entityRefs: (Entity | EntityName)[]; + entityRefs: (Entity | CompoundEntityRef)[]; defaultKind?: string; } & Omit; @@ -429,7 +429,7 @@ export function getEntityRelations( filter?: { kind: string; }, -): EntityName[]; +): CompoundEntityRef[]; // @public (undocumented) export function getEntitySourceLocation( @@ -439,7 +439,7 @@ export function getEntitySourceLocation( // @public (undocumented) export function humanizeEntityRef( - entityRef: Entity | EntityName, + entityRef: Entity | CompoundEntityRef, opts?: { defaultKind?: string; }, @@ -627,12 +627,18 @@ export type UserListPickerProps = { // @public (undocumented) export function useStarredEntities(): { starredEntities: Set; - toggleStarredEntity: (entityOrRef: Entity | EntityName | string) => void; - isStarredEntity: (entityOrRef: Entity | EntityName | string) => boolean; + toggleStarredEntity: ( + entityOrRef: Entity | CompoundEntityRef | string, + ) => void; + isStarredEntity: ( + entityOrRef: Entity | CompoundEntityRef | string, + ) => boolean; }; // @public (undocumented) -export function useStarredEntity(entityOrRef: Entity | EntityName | string): { +export function useStarredEntity( + entityOrRef: Entity | CompoundEntityRef | string, +): { toggleStarredEntity: () => void; isStarredEntity: boolean; }; diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index 4db48351a9..06a632232e 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -16,7 +16,7 @@ import { Entity, - EntityName, + CompoundEntityRef, DEFAULT_NAMESPACE, parseEntityRef, } from '@backstage/catalog-model'; @@ -33,7 +33,7 @@ import { Tooltip } from '@material-ui/core'; * @public */ export type EntityRefLinkProps = { - entityRef: Entity | EntityName | string; + entityRef: Entity | CompoundEntityRef | string; defaultKind?: string; title?: string; children?: React.ReactNode; diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.tsx index 9be970aa56..d92cd2ec87 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import React from 'react'; import { EntityRefLink } from './EntityRefLink'; import { LinkProps } from '@backstage/core-components'; @@ -25,7 +25,7 @@ import { LinkProps } from '@backstage/core-components'; * @public */ export type EntityRefLinksProps = { - entityRefs: (Entity | EntityName)[]; + entityRefs: (Entity | CompoundEntityRef)[]; defaultKind?: string; } & Omit; diff --git a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts index e2984c34ab..0ade1da0b9 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts @@ -16,7 +16,7 @@ import { Entity, - EntityName, + CompoundEntityRef, DEFAULT_NAMESPACE, } from '@backstage/catalog-model'; @@ -25,7 +25,7 @@ export const formatEntityRefTitle = humanizeEntityRef; /** @public */ export function humanizeEntityRef( - entityRef: Entity | EntityName, + entityRef: Entity | CompoundEntityRef, opts?: { defaultKind?: string }, ) { const defaultKind = opts?.defaultKind; diff --git a/plugins/catalog-react/src/components/EntityTable/columns.tsx b/plugins/catalog-react/src/components/EntityTable/columns.tsx index d5ef791831..08eb31bea3 100644 --- a/plugins/catalog-react/src/components/EntityTable/columns.tsx +++ b/plugins/catalog-react/src/components/EntityTable/columns.tsx @@ -16,7 +16,7 @@ import { Entity, - EntityName, + CompoundEntityRef, RELATION_OWNED_BY, RELATION_PART_OF, } from '@backstage/catalog-model'; @@ -81,7 +81,7 @@ export const columnFactories = Object.freeze({ defaultKind?: string; filter?: { kind: string }; }): TableColumn { - function getRelations(entity: T): EntityName[] { + function getRelations(entity: T): CompoundEntityRef[] { return getEntityRelations(entity, relation, entityFilter); } diff --git a/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.ts b/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.ts index 6420ebd5ae..c14b6523c0 100644 --- a/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.ts +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.ts @@ -16,8 +16,8 @@ import { Entity, - EntityName, - getEntityName, + CompoundEntityRef, + getCompoundEntityRef, ANNOTATION_ORIGIN_LOCATION, } from '@backstage/catalog-model'; import { catalogApiRef } from '../../api'; @@ -44,7 +44,7 @@ export type UseUnregisterEntityDialogState = | { type: 'unregister'; location: string; - colocatedEntities: EntityName[]; + colocatedEntities: CompoundEntityRef[]; unregisterLocation: () => Promise; deleteEntity: () => Promise; } @@ -141,7 +141,7 @@ export function useUnregisterEntityDialogState( return { type: 'unregister', location: locationRef!, - colocatedEntities: colocatedEntities.map(getEntityName), + colocatedEntities: colocatedEntities.map(getCompoundEntityRef), unregisterLocation, deleteEntity, }; diff --git a/plugins/catalog-react/src/hooks/useStarredEntities.ts b/plugins/catalog-react/src/hooks/useStarredEntities.ts index 5ddabfdd2e..328a26e0f8 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntities.ts +++ b/plugins/catalog-react/src/hooks/useStarredEntities.ts @@ -16,7 +16,7 @@ import { Entity, - EntityName, + CompoundEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; import { useApi } from '@backstage/core-plugin-api'; @@ -24,7 +24,9 @@ import { useCallback } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { starredEntitiesApiRef } from '../apis'; -function getEntityRef(entityOrRef: Entity | EntityName | string): string { +function getEntityRef( + entityOrRef: Entity | CompoundEntityRef | string, +): string { return typeof entityOrRef === 'string' ? entityOrRef : stringifyEntityRef(entityOrRef); @@ -33,8 +35,12 @@ function getEntityRef(entityOrRef: Entity | EntityName | string): string { /** @public */ export function useStarredEntities(): { starredEntities: Set; - toggleStarredEntity: (entityOrRef: Entity | EntityName | string) => void; - isStarredEntity: (entityOrRef: Entity | EntityName | string) => boolean; + toggleStarredEntity: ( + entityOrRef: Entity | CompoundEntityRef | string, + ) => void; + isStarredEntity: ( + entityOrRef: Entity | CompoundEntityRef | string, + ) => boolean; } { const starredEntitiesApi = useApi(starredEntitiesApiRef); @@ -44,13 +50,13 @@ export function useStarredEntities(): { ); const isStarredEntity = useCallback( - (entityOrRef: Entity | EntityName | string) => + (entityOrRef: Entity | CompoundEntityRef | string) => starredEntities.has(getEntityRef(entityOrRef)), [starredEntities], ); const toggleStarredEntity = useCallback( - (entityOrRef: Entity | EntityName | string) => + (entityOrRef: Entity | CompoundEntityRef | string) => starredEntitiesApi.toggleStarred(getEntityRef(entityOrRef)).then(), [starredEntitiesApi], ); diff --git a/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx b/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx index 8ffc891092..317a0e391c 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx +++ b/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { TestApiProvider } from '@backstage/test-utils'; import { renderHook } from '@testing-library/react-hooks'; import React, { PropsWithChildren } from 'react'; @@ -44,7 +44,7 @@ describe('useStarredEntity', () => { describe.each` title | entityOrRef ${'entity reference'} | ${'component:default/mock'} - ${'entity name'} | ${{ kind: 'component', namespace: 'default', name: 'mock' } as EntityName} + ${'entity name'} | ${{ kind: 'component', namespace: 'default', name: 'mock' } as CompoundEntityRef} ${'entity'} | ${{ apiVersion: '1', kind: 'Component', metadata: { name: 'mock' } } as Entity} `('with $title', ({ entityOrRef }) => { describe('toggleStarredEntity', () => { diff --git a/plugins/catalog-react/src/hooks/useStarredEntity.ts b/plugins/catalog-react/src/hooks/useStarredEntity.ts index 2c647163aa..23d5677512 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntity.ts +++ b/plugins/catalog-react/src/hooks/useStarredEntity.ts @@ -16,21 +16,25 @@ import { Entity, - EntityName, + CompoundEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; import { useApi } from '@backstage/core-plugin-api'; import { useCallback, useEffect, useState } from 'react'; import { starredEntitiesApiRef } from '../apis'; -function getEntityRef(entityOrRef: Entity | EntityName | string): string { +function getEntityRef( + entityOrRef: Entity | CompoundEntityRef | string, +): string { return typeof entityOrRef === 'string' ? entityOrRef : stringifyEntityRef(entityOrRef); } /** @public */ -export function useStarredEntity(entityOrRef: Entity | EntityName | string): { +export function useStarredEntity( + entityOrRef: Entity | CompoundEntityRef | string, +): { toggleStarredEntity: () => void; isStarredEntity: boolean; } { diff --git a/plugins/catalog-react/src/utils/getEntityRelations.ts b/plugins/catalog-react/src/utils/getEntityRelations.ts index 45cbc6967f..13086db4ea 100644 --- a/plugins/catalog-react/src/utils/getEntityRelations.ts +++ b/plugins/catalog-react/src/utils/getEntityRelations.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { Entity, EntityName, parseEntityRef } from '@backstage/catalog-model'; +import { + Entity, + CompoundEntityRef, + parseEntityRef, +} from '@backstage/catalog-model'; // TODO(freben): This should be returning entity refs instead /** @@ -26,7 +30,7 @@ export function getEntityRelations( entity: Entity | undefined, relationType: string, filter?: { kind: string }, -): EntityName[] { +): CompoundEntityRef[] { let entityNames = entity?.relations ?.filter(r => r.type === relationType) diff --git a/plugins/catalog-react/src/utils/isOwnerOf.ts b/plugins/catalog-react/src/utils/isOwnerOf.ts index 3ef6b4b2a0..f1ba0fa35f 100644 --- a/plugins/catalog-react/src/utils/isOwnerOf.ts +++ b/plugins/catalog-react/src/utils/isOwnerOf.ts @@ -16,7 +16,7 @@ import { Entity, - getEntityName, + getCompoundEntityRef, RELATION_MEMBER_OF, RELATION_OWNED_BY, stringifyEntityRef, @@ -31,7 +31,7 @@ export function isOwnerOf(owner: Entity, owned: Entity) { const possibleOwners = new Set( [ ...getEntityRelations(owner, RELATION_MEMBER_OF, { kind: 'group' }), - ...(owner ? [getEntityName(owner)] : []), + ...(owner ? [getCompoundEntityRef(owner)] : []), ].map(stringifyEntityRef), ); diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index f314932b9a..97a36ad020 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -7,8 +7,8 @@ import { ApiHolder } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; -import { EntityName } from '@backstage/catalog-model'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { IndexableDocument } from '@backstage/search-common'; @@ -157,9 +157,9 @@ export interface CatalogTableRow { resolved: { name: string; partOfSystemRelationTitle?: string; - partOfSystemRelations: EntityName[]; + partOfSystemRelations: CompoundEntityRef[]; ownedByRelationsTitle?: string; - ownedByRelations: EntityName[]; + ownedByRelations: CompoundEntityRef[]; }; } diff --git a/plugins/catalog/src/components/CatalogTable/types.ts b/plugins/catalog/src/components/CatalogTable/types.ts index 7eaff6faed..5bbb3af122 100644 --- a/plugins/catalog/src/components/CatalogTable/types.ts +++ b/plugins/catalog/src/components/CatalogTable/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; /** @public */ export interface CatalogTableRow { @@ -22,8 +22,8 @@ export interface CatalogTableRow { resolved: { name: string; partOfSystemRelationTitle?: string; - partOfSystemRelations: EntityName[]; + partOfSystemRelations: CompoundEntityRef[]; ownedByRelationsTitle?: string; - ownedByRelations: EntityName[]; + ownedByRelations: CompoundEntityRef[]; }; } diff --git a/plugins/code-coverage-backend/src/service/types.ts b/plugins/code-coverage-backend/src/service/types.ts index c4c622bfaf..31edad88d1 100644 --- a/plugins/code-coverage-backend/src/service/types.ts +++ b/plugins/code-coverage-backend/src/service/types.ts @@ -13,16 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; export type JsonCodeCoverage = { metadata: CoverageMetadata; - entity: EntityName; + entity: CompoundEntityRef; files: Array; }; export type JsonCoverageHistory = { - entity: EntityName; + entity: CompoundEntityRef; history: Array; }; diff --git a/plugins/code-coverage/src/api.ts b/plugins/code-coverage/src/api.ts index 61f920106b..081f1c7999 100644 --- a/plugins/code-coverage/src/api.ts +++ b/plugins/code-coverage/src/api.ts @@ -14,20 +14,25 @@ * limitations under the License. */ -import { EntityName, stringifyEntityRef } from '@backstage/catalog-model'; +import { + CompoundEntityRef, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { ResponseError } from '@backstage/errors'; import { JsonCodeCoverage, JsonCoverageHistory } from './types'; import { createApiRef, DiscoveryApi } from '@backstage/core-plugin-api'; export type CodeCoverageApi = { discovery: DiscoveryApi; - getCoverageForEntity: (entity: EntityName) => Promise; + getCoverageForEntity: ( + entity: CompoundEntityRef, + ) => Promise; getFileContentFromEntity: ( - entity: EntityName, + entity: CompoundEntityRef, filePath: string, ) => Promise; getCoverageHistoryForEntity: ( - entity: EntityName, + entity: CompoundEntityRef, limit?: number, ) => Promise; }; @@ -59,7 +64,7 @@ export class CodeCoverageRestApi implements CodeCoverageApi { } async getCoverageForEntity( - entityName: EntityName, + entityName: CompoundEntityRef, ): Promise { const entity = encodeURIComponent(stringifyEntityRef(entityName)); return (await this.fetch( @@ -68,7 +73,7 @@ export class CodeCoverageRestApi implements CodeCoverageApi { } async getFileContentFromEntity( - entityName: EntityName, + entityName: CompoundEntityRef, filePath: string, ): Promise { const entity = encodeURIComponent(stringifyEntityRef(entityName)); @@ -78,7 +83,7 @@ export class CodeCoverageRestApi implements CodeCoverageApi { } async getCoverageHistoryForEntity( - entityName: EntityName, + entityName: CompoundEntityRef, limit?: number, ): Promise { const entity = encodeURIComponent(stringifyEntityRef(entityName)); diff --git a/plugins/code-coverage/src/types.ts b/plugins/code-coverage/src/types.ts index c4c622bfaf..b03025b7ad 100644 --- a/plugins/code-coverage/src/types.ts +++ b/plugins/code-coverage/src/types.ts @@ -13,16 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; + +import { CompoundEntityRef } from '@backstage/catalog-model'; export type JsonCodeCoverage = { metadata: CoverageMetadata; - entity: EntityName; + entity: CompoundEntityRef; files: Array; }; export type JsonCoverageHistory = { - entity: EntityName; + entity: CompoundEntityRef; history: Array; }; diff --git a/plugins/fossa/src/components/FossaPage/FossaPage.tsx b/plugins/fossa/src/components/FossaPage/FossaPage.tsx index 90a36a6177..731852d4a7 100644 --- a/plugins/fossa/src/components/FossaPage/FossaPage.tsx +++ b/plugins/fossa/src/components/FossaPage/FossaPage.tsx @@ -16,7 +16,7 @@ import { Entity, - EntityName, + CompoundEntityRef, RELATION_OWNED_BY, } from '@backstage/catalog-model'; import { @@ -56,7 +56,7 @@ type FossaRow = { resolved: { name: string; ownedByRelationsTitle?: string; - ownedByRelations: EntityName[]; + ownedByRelations: CompoundEntityRef[]; loading: boolean; details?: FindingSummary; }; diff --git a/plugins/jenkins-backend/api-report.md b/plugins/jenkins-backend/api-report.md index d437a5dd75..cbc3ef4325 100644 --- a/plugins/jenkins-backend/api-report.md +++ b/plugins/jenkins-backend/api-report.md @@ -4,8 +4,8 @@ ```ts import { CatalogApi } from '@backstage/catalog-client'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { EntityName } from '@backstage/catalog-model'; import express from 'express'; import { Logger as Logger_2 } from 'winston'; @@ -25,7 +25,7 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { }): DefaultJenkinsInfoProvider; // (undocumented) getInstance(opt: { - entityRef: EntityName; + entityRef: CompoundEntityRef; jobFullName?: string; }): Promise; // (undocumented) @@ -65,7 +65,7 @@ export interface JenkinsInfo { export interface JenkinsInfoProvider { // (undocumented) getInstance(options: { - entityRef: EntityName; + entityRef: CompoundEntityRef; jobFullName?: string; }): Promise; } diff --git a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts index 62299926df..33079dfc8f 100644 --- a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts +++ b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts @@ -15,7 +15,7 @@ */ import { CatalogApi } from '@backstage/catalog-client'; -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { DefaultJenkinsInfoProvider, @@ -163,7 +163,7 @@ describe('DefaultJenkinsInfoProvider', () => { getEntityByName: jest.fn(), } as any as jest.Mocked; - const entityRef: EntityName = { + const entityRef: CompoundEntityRef = { kind: 'Component', namespace: 'foo', name: 'bar', diff --git a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts index f822f475f1..528a1d038f 100644 --- a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts +++ b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts @@ -17,7 +17,7 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Entity, - EntityName, + CompoundEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; @@ -27,7 +27,7 @@ export interface JenkinsInfoProvider { /** * The entity to get the info about. */ - entityRef: EntityName; + entityRef: CompoundEntityRef; /** * A specific job to get. This is only passed in when we know about a job name we are interested in. */ @@ -182,7 +182,7 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { } async getInstance(opt: { - entityRef: EntityName; + entityRef: CompoundEntityRef; jobFullName?: string; }): Promise { // load entity diff --git a/plugins/jenkins-backend/src/service/standaloneServer.ts b/plugins/jenkins-backend/src/service/standaloneServer.ts index 7f47d57ac3..f89f235667 100644 --- a/plugins/jenkins-backend/src/service/standaloneServer.ts +++ b/plugins/jenkins-backend/src/service/standaloneServer.ts @@ -18,7 +18,7 @@ import { createServiceBuilder } from '@backstage/backend-common'; import { Server } from 'http'; import { Logger } from 'winston'; import { createRouter } from './router'; -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { JenkinsInfo } from './jenkinsInfoProvider'; export interface ServerOptions { @@ -35,7 +35,9 @@ export async function startStandaloneServer( const router = await createRouter({ logger, jenkinsInfoProvider: { - async getInstance(_: { entityRef: EntityName }): Promise { + async getInstance(_: { + entityRef: CompoundEntityRef; + }): Promise { return { baseUrl: 'https://example.com/', jobFullName: 'build-foo' }; }, }, diff --git a/plugins/jenkins/api-report.md b/plugins/jenkins/api-report.md index 5db5fd0dfe..fc9de04a7a 100644 --- a/plugins/jenkins/api-report.md +++ b/plugins/jenkins/api-report.md @@ -7,9 +7,9 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import type { CompoundEntityRef } from '@backstage/catalog-model'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; -import type { EntityName } from '@backstage/catalog-model'; import { IdentityApi } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -48,20 +48,20 @@ export const JENKINS_ANNOTATION = 'jenkins.io/job-full-name'; export interface JenkinsApi { // Warning: (ae-forgotten-export) The symbol "Build" needs to be exported by the entry point index.d.ts getBuild(options: { - entity: EntityName; + entity: CompoundEntityRef; jobFullName: string; buildNumber: string; }): Promise; // Warning: (ae-forgotten-export) The symbol "Project" needs to be exported by the entry point index.d.ts getProjects(options: { - entity: EntityName; + entity: CompoundEntityRef; filter: { branch?: string; }; }): Promise; // (undocumented) retry(options: { - entity: EntityName; + entity: CompoundEntityRef; jobFullName: string; buildNumber: string; }): Promise; @@ -82,20 +82,20 @@ export class JenkinsClient implements JenkinsApi { }); // (undocumented) getBuild(options: { - entity: EntityName; + entity: CompoundEntityRef; jobFullName: string; buildNumber: string; }): Promise; // (undocumented) getProjects(options: { - entity: EntityName; + entity: CompoundEntityRef; filter: { branch?: string; }; }): Promise; // (undocumented) retry(options: { - entity: EntityName; + entity: CompoundEntityRef; jobFullName: string; buildNumber: string; }): Promise; diff --git a/plugins/jenkins/src/api/JenkinsApi.ts b/plugins/jenkins/src/api/JenkinsApi.ts index dd169de78b..04bd313227 100644 --- a/plugins/jenkins/src/api/JenkinsApi.ts +++ b/plugins/jenkins/src/api/JenkinsApi.ts @@ -19,7 +19,7 @@ import { DiscoveryApi, IdentityApi, } from '@backstage/core-plugin-api'; -import type { EntityName } from '@backstage/catalog-model'; +import type { CompoundEntityRef } from '@backstage/catalog-model'; import { ResponseError } from '@backstage/errors'; export const jenkinsApiRef = createApiRef({ @@ -80,7 +80,7 @@ export interface JenkinsApi { */ getProjects(options: { /** the entity whose jobs should be retrieved. */ - entity: EntityName; + entity: CompoundEntityRef; /** a filter on jobs. Currently this just takes a branch (and assumes certain structures in jenkins) */ filter: { branch?: string }; }): Promise; @@ -93,13 +93,13 @@ export interface JenkinsApi { * TODO: abstract jobFullName (so we could support differentiating between the same named job on multiple instances). */ getBuild(options: { - entity: EntityName; + entity: CompoundEntityRef; jobFullName: string; buildNumber: string; }): Promise; retry(options: { - entity: EntityName; + entity: CompoundEntityRef; jobFullName: string; buildNumber: string; }): Promise; @@ -118,7 +118,7 @@ export class JenkinsClient implements JenkinsApi { } async getProjects(options: { - entity: EntityName; + entity: CompoundEntityRef; filter: { branch?: string }; }): Promise { const { entity, filter } = options; @@ -157,7 +157,7 @@ export class JenkinsClient implements JenkinsApi { } async getBuild(options: { - entity: EntityName; + entity: CompoundEntityRef; jobFullName: string; buildNumber: string; }): Promise { @@ -182,7 +182,7 @@ export class JenkinsClient implements JenkinsApi { } async retry(options: { - entity: EntityName; + entity: CompoundEntityRef; jobFullName: string; buildNumber: string; }): Promise { diff --git a/plugins/jenkins/src/components/useBuildWithSteps.ts b/plugins/jenkins/src/components/useBuildWithSteps.ts index 2166b2a1a1..0910564384 100644 --- a/plugins/jenkins/src/components/useBuildWithSteps.ts +++ b/plugins/jenkins/src/components/useBuildWithSteps.ts @@ -19,7 +19,7 @@ import { jenkinsApiRef } from '../api'; import { useAsyncPolling } from './useAsyncPolling'; import { errorApiRef, useApi } from '@backstage/core-plugin-api'; import { useEntity } from '@backstage/plugin-catalog-react'; -import { getEntityName } from '@backstage/catalog-model'; +import { getCompoundEntityRef } from '@backstage/catalog-model'; const INTERVAL_AMOUNT = 1500; @@ -41,7 +41,7 @@ export function useBuildWithSteps({ const getBuildWithSteps = useCallback(async () => { try { - const entityName = await getEntityName(entity); + const entityName = await getCompoundEntityRef(entity); return api.getBuild({ entity: entityName, jobFullName, buildNumber }); } catch (e) { errorApi.post(e); diff --git a/plugins/jenkins/src/components/useBuilds.ts b/plugins/jenkins/src/components/useBuilds.ts index a4afeb351f..07d23cce5a 100644 --- a/plugins/jenkins/src/components/useBuilds.ts +++ b/plugins/jenkins/src/components/useBuilds.ts @@ -18,7 +18,7 @@ import useAsyncRetry from 'react-use/lib/useAsyncRetry'; import { jenkinsApiRef } from '../api'; import { errorApiRef, useApi } from '@backstage/core-plugin-api'; import { useEntity } from '@backstage/plugin-catalog-react'; -import { getEntityName } from '@backstage/catalog-model'; +import { getCompoundEntityRef } from '@backstage/catalog-model'; export enum ErrorType { CONNECTION_ERROR, @@ -33,7 +33,7 @@ export enum ErrorType { */ export function useBuilds({ branch }: { branch?: string } = {}) { const { entity } = useEntity(); - const entityName = getEntityName(entity); + const entityName = getCompoundEntityRef(entity); const api = useApi(jenkinsApiRef); const errorApi = useApi(errorApiRef); @@ -60,7 +60,7 @@ export function useBuilds({ branch }: { branch?: string } = {}) { } = useAsyncRetry(async () => { try { const build = await api.getProjects({ - entity: getEntityName(entity), + entity: getCompoundEntityRef(entity), filter: { branch }, }); diff --git a/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts b/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts index 5fcd89841e..d095b77b4c 100644 --- a/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts +++ b/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts @@ -16,7 +16,7 @@ import { Entity, - getEntityName, + getCompoundEntityRef, parseEntityRef, RELATION_OWNED_BY, RELATION_OWNER_OF, @@ -55,7 +55,7 @@ export class ScaffolderEntitiesProcessor implements CatalogProcessor { _location: LocationSpec, emit: CatalogProcessorEmit, ): Promise { - const selfRef = getEntityName(entity); + const selfRef = getCompoundEntityRef(entity); if ( entity.apiVersion === 'scaffolder.backstage.io/v1beta3' && diff --git a/plugins/scaffolder-backend/src/service/helpers.ts b/plugins/scaffolder-backend/src/service/helpers.ts index e49c28cb74..e3aae0951b 100644 --- a/plugins/scaffolder-backend/src/service/helpers.ts +++ b/plugins/scaffolder-backend/src/service/helpers.ts @@ -20,7 +20,7 @@ import { ANNOTATION_LOCATION, parseLocationRef, ANNOTATION_SOURCE_LOCATION, - EntityName, + CompoundEntityRef, DEFAULT_NAMESPACE, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; @@ -91,7 +91,7 @@ export function getEntityBaseUrl(entity: Entity): string | undefined { * Returns the matching template, or throws a NotFoundError if no such template existed. */ export async function findTemplate(options: { - entityRef: EntityName; + entityRef: CompoundEntityRef; token?: string; catalogApi: CatalogApi; }): Promise { diff --git a/plugins/tech-insights-backend/src/service/router.ts b/plugins/tech-insights-backend/src/service/router.ts index b13802358f..b86677ab90 100644 --- a/plugins/tech-insights-backend/src/service/router.ts +++ b/plugins/tech-insights-backend/src/service/router.ts @@ -27,7 +27,7 @@ import { Logger } from 'winston'; import { DateTime } from 'luxon'; import { PersistenceContext } from './persistence/persistenceContext'; import { - EntityName, + CompoundEntityRef, parseEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; @@ -99,8 +99,10 @@ export async function createRouter< }); router.post('/checks/run', async (req, res) => { - const { checks, entities }: { checks: string[]; entities: EntityName[] } = - req.body; + const { + checks, + entities, + }: { checks: string[]; entities: CompoundEntityRef[] } = req.body; const tasks = entities.map(async entity => { const entityTriplet = typeof entity === 'string' ? entity : stringifyEntityRef(entity); diff --git a/plugins/tech-insights/api-report.md b/plugins/tech-insights/api-report.md index 79854e505f..deadd06010 100644 --- a/plugins/tech-insights/api-report.md +++ b/plugins/tech-insights/api-report.md @@ -9,7 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { BulkCheckResponse } from '@backstage/plugin-tech-insights-common'; import { CheckResult } from '@backstage/plugin-tech-insights-common'; -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -52,11 +52,14 @@ export interface TechInsightsApi { ) => CheckResultRenderer | undefined; // (undocumented) runBulkChecks( - entities: EntityName[], + entities: CompoundEntityRef[], checks?: Check[], ): Promise; // (undocumented) - runChecks(entityParams: EntityName, checks?: Check[]): Promise; + runChecks( + entityParams: CompoundEntityRef, + checks?: Check[], + ): Promise; } // @public diff --git a/plugins/tech-insights/src/api/TechInsightsApi.ts b/plugins/tech-insights/src/api/TechInsightsApi.ts index b1287a62dc..0a6bb937a6 100644 --- a/plugins/tech-insights/src/api/TechInsightsApi.ts +++ b/plugins/tech-insights/src/api/TechInsightsApi.ts @@ -21,7 +21,7 @@ import { } from '@backstage/plugin-tech-insights-common'; import { Check } from './types'; import { CheckResultRenderer } from '../components/CheckResultRenderer'; -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; /** * {@link @backstage/core-plugin-api#ApiRef} for the {@link TechInsightsApi} @@ -45,9 +45,12 @@ export interface TechInsightsApi { description?: string, ) => CheckResultRenderer | undefined; getAllChecks(): Promise; - runChecks(entityParams: EntityName, checks?: Check[]): Promise; + runChecks( + entityParams: CompoundEntityRef, + checks?: Check[], + ): Promise; runBulkChecks( - entities: EntityName[], + entities: CompoundEntityRef[], checks?: Check[], ): Promise; } diff --git a/plugins/tech-insights/src/api/TechInsightsClient.ts b/plugins/tech-insights/src/api/TechInsightsClient.ts index 5e29749854..5b9af9557f 100644 --- a/plugins/tech-insights/src/api/TechInsightsClient.ts +++ b/plugins/tech-insights/src/api/TechInsightsClient.ts @@ -22,7 +22,7 @@ import { import { Check } from './types'; import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; import { ResponseError } from '@backstage/errors'; -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { CheckResultRenderer, @@ -74,7 +74,7 @@ export class TechInsightsClient implements TechInsightsApi { } async runChecks( - entityParams: EntityName, + entityParams: CompoundEntityRef, checks?: Check[], ): Promise { const url = await this.discoveryApi.getBaseUrl('tech-insights'); @@ -102,7 +102,7 @@ export class TechInsightsClient implements TechInsightsApi { } async runBulkChecks( - entities: EntityName[], + entities: CompoundEntityRef[], checks?: Check[], ): Promise { const url = await this.discoveryApi.getBaseUrl('tech-insights'); diff --git a/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts b/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts index 678a4b0539..a1542df351 100644 --- a/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts +++ b/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts @@ -16,7 +16,7 @@ import { CachedEntityLoader } from './CachedEntityLoader'; import { CatalogClient } from '@backstage/catalog-client'; import { CacheClient } from '@backstage/backend-common'; -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; describe('CachedEntityLoader', () => { const catalog: jest.Mocked = { @@ -28,7 +28,7 @@ describe('CachedEntityLoader', () => { set: jest.fn(), } as any; - const entityName: EntityName = { + const entityName: CompoundEntityRef = { kind: 'component', namespace: 'default', name: 'test', diff --git a/plugins/techdocs-backend/src/service/CachedEntityLoader.ts b/plugins/techdocs-backend/src/service/CachedEntityLoader.ts index 81c6ec7431..cd771f7fff 100644 --- a/plugins/techdocs-backend/src/service/CachedEntityLoader.ts +++ b/plugins/techdocs-backend/src/service/CachedEntityLoader.ts @@ -17,7 +17,7 @@ import { CatalogClient } from '@backstage/catalog-client'; import { CacheClient } from '@backstage/backend-common'; import { Entity, - EntityName, + CompoundEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; @@ -37,7 +37,7 @@ export class CachedEntityLoader { } async load( - entityName: EntityName, + entityName: CompoundEntityRef, token: string | undefined, ): Promise { const cacheKey = this.getCacheKey(entityName, token); @@ -66,7 +66,7 @@ export class CachedEntityLoader { } private getCacheKey( - entityName: EntityName, + entityName: CompoundEntityRef, token: string | undefined, ): string { const key = ['catalog', stringifyEntityRef(entityName)]; diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index 917c1d5d85..f7ef6e984a 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -7,11 +7,11 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { CSSProperties } from '@material-ui/styles'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; -import { EntityName } from '@backstage/catalog-model'; import { FetchApi } from '@backstage/core-plugin-api'; import { IdentityApi } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; @@ -89,7 +89,7 @@ export type DocsTableRow = { resolved: { docsUrl: string; ownedByRelationsTitle: string; - ownedByRelations: EntityName[]; + ownedByRelations: CompoundEntityRef[]; }; }; @@ -161,7 +161,7 @@ export const Reader: (props: ReaderProps) => JSX.Element; // @public export type ReaderProps = { - entityRef: EntityName; + entityRef: CompoundEntityRef; withSearch?: boolean; onReady?: () => void; }; @@ -187,9 +187,11 @@ export type TabsConfig = TabConfig[]; export interface TechDocsApi { getApiOrigin(): Promise; // (undocumented) - getEntityMetadata(entityId: EntityName): Promise; + getEntityMetadata( + entityId: CompoundEntityRef, + ): Promise; // (undocumented) - getTechDocsMetadata(entityId: EntityName): Promise; + getTechDocsMetadata(entityId: CompoundEntityRef): Promise; } // @public @@ -208,8 +210,10 @@ export class TechDocsClient implements TechDocsApi { discoveryApi: DiscoveryApi; // (undocumented) getApiOrigin(): Promise; - getEntityMetadata(entityId: EntityName): Promise; - getTechDocsMetadata(entityId: EntityName): Promise; + getEntityMetadata( + entityId: CompoundEntityRef, + ): Promise; + getTechDocsMetadata(entityId: CompoundEntityRef): Promise; } // @public @@ -297,7 +301,7 @@ export const TechDocsReaderPageHeader: ( // @public export type TechDocsReaderPageHeaderProps = PropsWithChildren<{ - entityRef: EntityName; + entityRef: CompoundEntityRef; entityMetadata?: TechDocsEntityMetadata; techDocsMetadata?: TechDocsMetadata; }>; @@ -315,7 +319,7 @@ export type TechDocsReaderPageRenderFunction = ({ }: { techdocsMetadataValue?: TechDocsMetadata | undefined; entityMetadataValue?: TechDocsEntityMetadata | undefined; - entityRef: EntityName; + entityRef: CompoundEntityRef; onReady: () => void; }) => JSX.Element; @@ -324,7 +328,7 @@ export const TechDocsSearch: (props: TechDocsSearchProps) => JSX.Element; // @public export type TechDocsSearchProps = { - entityId: EntityName; + entityId: CompoundEntityRef; debounceTime?: number; }; @@ -348,18 +352,18 @@ export interface TechDocsStorageApi { // (undocumented) getBaseUrl( oldBaseUrl: string, - entityId: EntityName, + entityId: CompoundEntityRef, path: string, ): Promise; // (undocumented) getBuilder(): Promise; // (undocumented) - getEntityDocs(entityId: EntityName, path: string): Promise; + getEntityDocs(entityId: CompoundEntityRef, path: string): Promise; // (undocumented) getStorageUrl(): Promise; // (undocumented) syncEntityDocs( - entityId: EntityName, + entityId: CompoundEntityRef, logHandler?: (line: string) => void, ): Promise; } @@ -384,18 +388,18 @@ export class TechDocsStorageClient implements TechDocsStorageApi { // (undocumented) getBaseUrl( oldBaseUrl: string, - entityId: EntityName, + entityId: CompoundEntityRef, path: string, ): Promise; // (undocumented) getBuilder(): Promise; - getEntityDocs(entityId: EntityName, path: string): Promise; + getEntityDocs(entityId: CompoundEntityRef, path: string): Promise; // (undocumented) getStorageUrl(): Promise; // (undocumented) identityApi: IdentityApi; syncEntityDocs( - entityId: EntityName, + entityId: CompoundEntityRef, logHandler?: (line: string) => void, ): Promise; } diff --git a/plugins/techdocs/dev/index.tsx b/plugins/techdocs/dev/index.tsx index 8dc0e5d30b..d08e914736 100644 --- a/plugins/techdocs/dev/index.tsx +++ b/plugins/techdocs/dev/index.tsx @@ -17,7 +17,7 @@ import { createDevApp } from '@backstage/dev-utils'; import { NotFoundError } from '@backstage/errors'; import React from 'react'; -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Reader, SyncResult, @@ -82,7 +82,10 @@ function createPage({ }); } - async syncEntityDocs(_: EntityName, logHandler?: (line: string) => void) { + async syncEntityDocs( + _: CompoundEntityRef, + logHandler?: (line: string) => void, + ) { if (syncDocsDelay) { for (let i = 0; i < 10; i++) { setTimeout( diff --git a/plugins/techdocs/src/api.ts b/plugins/techdocs/src/api.ts index 4566bbf028..b349431b0d 100644 --- a/plugins/techdocs/src/api.ts +++ b/plugins/techdocs/src/api.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { TechDocsEntityMetadata, TechDocsMetadata } from './types'; import { createApiRef } from '@backstage/core-plugin-api'; @@ -55,14 +55,14 @@ export interface TechDocsStorageApi { getApiOrigin(): Promise; getStorageUrl(): Promise; getBuilder(): Promise; - getEntityDocs(entityId: EntityName, path: string): Promise; + getEntityDocs(entityId: CompoundEntityRef, path: string): Promise; syncEntityDocs( - entityId: EntityName, + entityId: CompoundEntityRef, logHandler?: (line: string) => void, ): Promise; getBaseUrl( oldBaseUrl: string, - entityId: EntityName, + entityId: CompoundEntityRef, path: string, ): Promise; } @@ -77,6 +77,8 @@ export interface TechDocsApi { * Set to techdocs.requestUrl as the URL for techdocs-backend API. */ getApiOrigin(): Promise; - getTechDocsMetadata(entityId: EntityName): Promise; - getEntityMetadata(entityId: EntityName): Promise; + getTechDocsMetadata(entityId: CompoundEntityRef): Promise; + getEntityMetadata( + entityId: CompoundEntityRef, + ): Promise; } diff --git a/plugins/techdocs/src/client.ts b/plugins/techdocs/src/client.ts index eac5da0b6b..f260f00547 100644 --- a/plugins/techdocs/src/client.ts +++ b/plugins/techdocs/src/client.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { DiscoveryApi, @@ -62,7 +62,9 @@ export class TechDocsClient implements TechDocsApi { * * @param entityId - Object containing entity data like name, namespace, etc. */ - async getTechDocsMetadata(entityId: EntityName): Promise { + async getTechDocsMetadata( + entityId: CompoundEntityRef, + ): Promise { const { kind, namespace, name } = entityId; const apiOrigin = await this.getApiOrigin(); @@ -84,7 +86,7 @@ export class TechDocsClient implements TechDocsApi { * @param entityId - Object containing entity data like name, namespace, etc. */ async getEntityMetadata( - entityId: EntityName, + entityId: CompoundEntityRef, ): Promise { const { kind, namespace, name } = entityId; @@ -149,7 +151,10 @@ export class TechDocsStorageClient implements TechDocsStorageApi { * @returns HTML content of the docs page as string * @throws Throws error when the page is not found. */ - async getEntityDocs(entityId: EntityName, path: string): Promise { + async getEntityDocs( + entityId: CompoundEntityRef, + path: string, + ): Promise { const { kind, namespace, name } = entityId; const storageUrl = await this.getStorageUrl(); @@ -190,7 +195,7 @@ export class TechDocsStorageClient implements TechDocsStorageApi { * @throws Throws error on error from sync endpoint in Techdocs Backend */ async syncEntityDocs( - entityId: EntityName, + entityId: CompoundEntityRef, logHandler: (line: string) => void = () => {}, ): Promise { const { kind, namespace, name } = entityId; @@ -243,7 +248,7 @@ export class TechDocsStorageClient implements TechDocsStorageApi { async getBaseUrl( oldBaseUrl: string, - entityId: EntityName, + entityId: CompoundEntityRef, path: string, ): Promise { const { kind, namespace, name } = entityId; diff --git a/plugins/techdocs/src/home/components/Tables/types.ts b/plugins/techdocs/src/home/components/Tables/types.ts index c076d4c12d..6c06526d07 100644 --- a/plugins/techdocs/src/home/components/Tables/types.ts +++ b/plugins/techdocs/src/home/components/Tables/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, EntityName } from '@backstage/catalog-model'; +import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; /** * Generic representing the metadata structure for a docs table row. @@ -26,6 +26,6 @@ export type DocsTableRow = { resolved: { docsUrl: string; ownedByRelationsTitle: string; - ownedByRelations: EntityName[]; + ownedByRelations: CompoundEntityRef[]; }; }; diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 4042585b6d..6aa5c99be4 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -34,7 +34,7 @@ import { alpha, } from '@material-ui/core'; -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { useApi, configApiRef } from '@backstage/core-plugin-api'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { BackstageTheme } from '@backstage/theme'; @@ -68,7 +68,7 @@ import { useReaderState } from './useReaderState'; * @public */ export type ReaderProps = { - entityRef: EntityName; + entityRef: CompoundEntityRef; withSearch?: boolean; onReady?: () => void; }; @@ -95,7 +95,7 @@ const TechDocsReaderContext = createContext( const TechDocsReaderProvider = ({ children, entityRef, -}: PropsWithChildren<{ entityRef: EntityName }>) => { +}: PropsWithChildren<{ entityRef: CompoundEntityRef }>) => { const { '*': path } = useParams(); const { kind, namespace, name } = entityRef; const value = useReaderState(kind, namespace, name, path); @@ -116,7 +116,7 @@ const TechDocsReaderProvider = ({ * @internal */ export const withTechDocsReaderProvider = - (Component: ComponentType, entityRef: EntityName) => + (Component: ComponentType, entityRef: CompoundEntityRef) => (props: T) => ( @@ -157,7 +157,9 @@ const headings: TypographyHeadingsKeys[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; * todo: Make public or stop exporting (see others: "altReaderExperiments") * @internal */ -export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { +export const useTechDocsReaderDom = ( + entityRef: CompoundEntityRef, +): Element | null => { const navigate = useNavigate(); const theme = useTheme(); const techdocsStorageApi = useApi(techdocsStorageApiRef); @@ -322,7 +324,7 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { --md-source-version-icon: url('data:image/svg+xml;charset=utf-8,'); --md-version-icon: url('data:image/svg+xml;charset=utf-8,'); } - + :host > * { /* CODE */ --md-code-fg-color: ${theme.palette.text.primary}; @@ -438,7 +440,7 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { .md-main__inner { margin-top: 0; } - + .md-sidebar { height: calc(100% - 100px); position: fixed; @@ -450,13 +452,13 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { .md-sidebar--secondary { right: ${theme.spacing(3)}px; } - + .md-content { max-width: calc(100% - 16rem * 2); margin-left: 16rem; margin-bottom: 50px; } - + .md-footer { position: fixed; bottom: 0px; @@ -471,7 +473,7 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { .md-dialog { background-color: unset; } - + @media screen and (max-width: 76.1875em) { .md-nav { transition: none !important; @@ -567,7 +569,7 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { }), injectCss({ // Typeset - css: ` + css: ` .md-typeset { font-size: var(--md-typeset-font-size); } @@ -600,11 +602,11 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { .md-typeset .md-content__button { color: var(--md-default-fg-color); } - + .md-typeset hr { border-bottom: 0.05rem dotted ${theme.palette.divider}; } - + .md-typeset details { font-size: var(--md-typeset-font-size) !important; } @@ -621,7 +623,7 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { .md-typeset details[open] > summary:after { transform: rotate(90deg) translateX(-50%) !important; } - + .md-typeset blockquote { color: var(--md-default-fg-color--light); border-left: 0.2rem solid var(--md-default-fg-color--light); @@ -667,13 +669,13 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { .highlight .md-clipboard:after { content: unset; } - + .highlight .nx { color: ${isDarkTheme ? '#ff53a3' : '#ec407a'}; } /* CODE HILITE */ - .codehilite .gd { + .codehilite .gd { background-color: ${ isDarkTheme ? 'rgba(248,81,73,0.65)' : '#fdd' }; diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage.tsx index d03ea2ef61..184ab24b6d 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage.tsx @@ -21,12 +21,12 @@ import useAsync from 'react-use/lib/useAsync'; import { techdocsApiRef } from '../../api'; import { LegacyTechDocsPage } from './LegacyTechDocsPage'; import { TechDocsEntityMetadata, TechDocsMetadata } from '../../types'; -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { useApi, useApp } from '@backstage/core-plugin-api'; import { Page } from '@backstage/core-components'; /** - * Helper function that gives the children of {@link TechDocsReaderPage} acccess to techdocs and entity metadata + * Helper function that gives the children of {@link TechDocsReaderPage} access to techdocs and entity metadata * * @public */ @@ -37,7 +37,7 @@ export type TechDocsReaderPageRenderFunction = ({ }: { techdocsMetadataValue?: TechDocsMetadata | undefined; entityMetadataValue?: TechDocsEntityMetadata | undefined; - entityRef: EntityName; + entityRef: CompoundEntityRef; onReady: () => void; }) => JSX.Element; diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader.tsx index 928833efed..5867641c48 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader.tsx @@ -19,7 +19,7 @@ import CodeIcon from '@material-ui/icons/Code'; import { useRouteRef } from '@backstage/core-plugin-api'; import { Header, HeaderLabel } from '@backstage/core-components'; -import { EntityName, RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { CompoundEntityRef, RELATION_OWNED_BY } from '@backstage/catalog-model'; import { EntityRefLink, EntityRefLinks, @@ -35,7 +35,7 @@ import { TechDocsEntityMetadata, TechDocsMetadata } from '../../types'; * @public */ export type TechDocsReaderPageHeaderProps = PropsWithChildren<{ - entityRef: EntityName; + entityRef: CompoundEntityRef; entityMetadata?: TechDocsEntityMetadata; techDocsMetadata?: TechDocsMetadata; }>; diff --git a/plugins/techdocs/src/reader/components/useRawPage.ts b/plugins/techdocs/src/reader/components/useRawPage.ts index 61b04a5a82..311f8554a9 100644 --- a/plugins/techdocs/src/reader/components/useRawPage.ts +++ b/plugins/techdocs/src/reader/components/useRawPage.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import useAsyncRetry from 'react-use/lib/useAsyncRetry'; import { AsyncState } from 'react-use/lib/useAsync'; import { techdocsStorageApiRef } from '../../api'; @@ -22,7 +22,7 @@ import { useApi } from '@backstage/core-plugin-api'; export type RawPage = { content: string; path: string; - entityId: EntityName; + entityId: CompoundEntityRef; }; export function useRawPage( diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts index f3906b7b91..2e3b853e42 100644 --- a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { TechDocsStorageApi } from '../../api'; import type { Transformer } from './transformer'; type AddBaseUrlOptions = { techdocsStorageApi: TechDocsStorageApi; - entityId: EntityName; + entityId: CompoundEntityRef; path: string; }; diff --git a/plugins/techdocs/src/search/components/TechDocsSearch.tsx b/plugins/techdocs/src/search/components/TechDocsSearch.tsx index c817fb3217..30cc692b7c 100644 --- a/plugins/techdocs/src/search/components/TechDocsSearch.tsx +++ b/plugins/techdocs/src/search/components/TechDocsSearch.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { SearchContextProvider, useSearch } from '@backstage/plugin-search'; import { makeStyles, @@ -42,7 +42,7 @@ const useStyles = makeStyles({ * @public */ export type TechDocsSearchProps = { - entityId: EntityName; + entityId: CompoundEntityRef; debounceTime?: number; }; diff --git a/plugins/todo-backend/api-report.md b/plugins/todo-backend/api-report.md index 71094e03f8..f3f2af3fb9 100644 --- a/plugins/todo-backend/api-report.md +++ b/plugins/todo-backend/api-report.md @@ -4,8 +4,8 @@ ```ts import { CatalogApi } from '@backstage/catalog-client'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { EntityName } from '@backstage/catalog-model'; import express from 'express'; import { Logger as Logger_2 } from 'winston'; import { ScmIntegrations } from '@backstage/integration'; @@ -19,7 +19,7 @@ export function createTodoParser(options?: TodoParserOptions): TodoParser; // @public (undocumented) export type ListTodosRequest = { - entity?: EntityName; + entity?: CompoundEntityRef; offset?: number; limit?: number; orderBy?: { diff --git a/plugins/todo-backend/src/service/router.ts b/plugins/todo-backend/src/service/router.ts index a0e6a6ec94..33356a83d6 100644 --- a/plugins/todo-backend/src/service/router.ts +++ b/plugins/todo-backend/src/service/router.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EntityName, parseEntityRef } from '@backstage/catalog-model'; +import { CompoundEntityRef, parseEntityRef } from '@backstage/catalog-model'; import { InputError } from '@backstage/errors'; import express from 'express'; import Router from 'express-promise-router'; @@ -52,7 +52,7 @@ export async function createRouter( if (entityRef && typeof entityRef !== 'string') { throw new InputError(`entity query must be a string`); } - let entity: EntityName | undefined = undefined; + let entity: CompoundEntityRef | undefined = undefined; if (entityRef) { try { entity = parseEntityRef(entityRef); diff --git a/plugins/todo-backend/src/service/types.ts b/plugins/todo-backend/src/service/types.ts index 90ee6bc913..88d2f2b340 100644 --- a/plugins/todo-backend/src/service/types.ts +++ b/plugins/todo-backend/src/service/types.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { EntityName } from '@backstage/catalog-model'; +import { CompoundEntityRef } from '@backstage/catalog-model'; import { TodoItem } from '../lib'; /** @public */ export type ListTodosRequest = { - entity?: EntityName; + entity?: CompoundEntityRef; offset?: number; limit?: number; orderBy?: { From 5b7343be6780942b18541ffba776dc199b925ab2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Mar 2022 04:07:37 +0000 Subject: [PATCH 137/353] chore(deps): bump ldapjs from 2.3.1 to 2.3.2 Bumps [ldapjs](https://github.com/ldapjs/node-ldapjs) from 2.3.1 to 2.3.2. - [Release notes](https://github.com/ldapjs/node-ldapjs/releases) - [Changelog](https://github.com/ldapjs/node-ldapjs/blob/master/CHANGES.md) - [Commits](https://github.com/ldapjs/node-ldapjs/compare/v2.3.1...v2.3.2) --- updated-dependencies: - dependency-name: ldapjs dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index c2f299faa3..13371a2df3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16251,9 +16251,9 @@ ldap-filter@^0.3.3: assert-plus "^1.0.0" ldapjs@^2.2.0: - version "2.3.1" - resolved "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.1.tgz#04136815fb1f21d692ac87fab5961a04d86e8b04" - integrity sha512-kf0tHHLrpwKaBAQOhYHXgdeh2PkFuCCxWgLb1MRn67ZQVo787D2pij3mmHVZx193GIdM8xcfi8HF6AIYYnj0fQ== + version "2.3.2" + resolved "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.2.tgz#a599d081519f70462941cc33a50e9354c32f35b7" + integrity sha512-FU+GR/qbQ96WUZ2DUb7FzaEybYvv3240wTVPcbsdELB3o4cK92zGVjntsh68siVkLeCmlCcsd/cIQzyGXSS7LA== dependencies: abstract-logging "^2.0.0" asn1 "^0.2.4" From 5617b2f815f71cf1adf9f5f297f860009091c656 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Mar 2022 04:08:04 +0000 Subject: [PATCH 138/353] chore(deps): bump eslint-config-prettier from 8.3.0 to 8.5.0 Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 8.3.0 to 8.5.0. - [Release notes](https://github.com/prettier/eslint-config-prettier/releases) - [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/eslint-config-prettier/compare/v8.3.0...v8.5.0) --- updated-dependencies: - dependency-name: eslint-config-prettier dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index c2f299faa3..63ddc00adf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11548,9 +11548,9 @@ escodegen@^2.0.0: source-map "~0.6.1" eslint-config-prettier@^8.3.0: - version "8.3.0" - resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a" - integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew== + version "8.5.0" + resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1" + integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== eslint-formatter-friendly@^7.0.0: version "7.0.0" From 458d16869c028d060be9a009616917fdd53082cb Mon Sep 17 00:00:00 2001 From: Peiman Jafari Date: Wed, 2 Mar 2022 20:44:58 -0800 Subject: [PATCH 139/353] scaffolder-backend: Allow more options in publish:github action Signed-off-by: Peiman Jafari --- .changeset/twenty-fireants-turn.md | 5 +++ .../actions/builtin/publish/github.test.ts | 16 +++++++++ .../actions/builtin/publish/github.ts | 36 +++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 .changeset/twenty-fireants-turn.md diff --git a/.changeset/twenty-fireants-turn.md b/.changeset/twenty-fireants-turn.md new file mode 100644 index 0000000000..fd9dc17530 --- /dev/null +++ b/.changeset/twenty-fireants-turn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Allow passing more repo configuration for `publish:github` action diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index 1e58635bbe..ec564e93dc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -88,6 +88,10 @@ describe('publish:github', () => { name: 'repo', org: 'owner', private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + allow_merge_commit: true, + allow_rebase_merge: true, visibility: 'private', }); @@ -103,6 +107,10 @@ describe('publish:github', () => { name: 'repo', org: 'owner', private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + allow_merge_commit: true, + allow_rebase_merge: true, visibility: 'public', }); }); @@ -123,6 +131,10 @@ describe('publish:github', () => { description: 'description', name: 'repo', private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + allow_merge_commit: true, + allow_rebase_merge: true, }); await action.handler({ @@ -138,6 +150,10 @@ describe('publish:github', () => { description: 'description', name: 'repo', private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + allow_merge_commit: true, + allow_rebase_merge: true, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index 926a63915a..32969e1be3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -46,6 +46,10 @@ export function createPublishGithubAction(options: { description?: string; access?: string; defaultBranch?: string; + deleteBranchOnMerge: boolean; + allowRebaseMerge: boolean; + allowSquashMerge: boolean; + allowMergeCommit: boolean; sourcePath?: string; requireCodeOwnerReviews?: boolean; repoVisibility?: 'private' | 'internal' | 'public'; @@ -94,6 +98,26 @@ export function createPublishGithubAction(options: { type: 'string', description: `Sets the default branch on the repository. The default value is 'master'`, }, + deleteBranchOnMerge: { + title: 'Delete Branch On Merge', + type: 'boolean', + description: `Delete the branch after merging the PR. The default value is 'false'`, + }, + allowMergeCommit: { + title: 'Allow Merge Commits', + type: 'boolean', + description: `Allow merge commits. The default value is 'true'`, + }, + allowSquashMerge: { + title: 'Allow Squash Merges', + type: 'boolean', + description: `Allow squash merges. The default value is 'true'`, + }, + allowRebaseMerge: { + title: 'Allow Rebase Merges', + type: 'boolean', + description: `Allow rebase merges. The default value is 'true'`, + }, sourcePath: { title: 'Source Path', description: @@ -156,6 +180,10 @@ export function createPublishGithubAction(options: { requireCodeOwnerReviews = false, repoVisibility = 'private', defaultBranch = 'master', + deleteBranchOnMerge = false, + allowMergeCommit = true, + allowSquashMerge = true, + allowRebaseMerge = true, collaborators, topics, token: providedToken, @@ -188,11 +216,19 @@ export function createPublishGithubAction(options: { private: repoVisibility === 'private', visibility: repoVisibility, description: description, + delete_branch_on_merge: deleteBranchOnMerge, + allow_merge_commit: allowMergeCommit, + allow_squash_merge: allowSquashMerge, + allow_rebase_merge: allowRebaseMerge, }) : client.rest.repos.createForAuthenticatedUser({ name: repo, private: repoVisibility === 'private', description: description, + delete_branch_on_merge: deleteBranchOnMerge, + allow_merge_commit: allowMergeCommit, + allow_squash_merge: allowSquashMerge, + allow_rebase_merge: allowRebaseMerge, }); const { data: newRepo } = await repoCreationPromise; From bb2ba5f10d84477eee9807d5140a314b1236e474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 3 Mar 2022 10:02:14 +0100 Subject: [PATCH 140/353] remove deprecated catalog-client request/response types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/brown-dryers-serve.md | 10 +++++ packages/catalog-client/api-report.md | 12 ------ .../catalog-client/src/types/deprecated.ts | 43 ------------------- packages/catalog-client/src/types/index.ts | 1 - 4 files changed, 10 insertions(+), 56 deletions(-) create mode 100644 .changeset/brown-dryers-serve.md delete mode 100644 packages/catalog-client/src/types/deprecated.ts diff --git a/.changeset/brown-dryers-serve.md b/.changeset/brown-dryers-serve.md new file mode 100644 index 0000000000..40c02538b8 --- /dev/null +++ b/.changeset/brown-dryers-serve.md @@ -0,0 +1,10 @@ +--- +'@backstage/catalog-client': minor +--- + +**BREAKING**: Removed the old deprecated request/response types: + +- `CatalogEntitiesRequest` - please use `GetEntitiesRequest` instead +- `CatalogEntityAncestorsRequest` - please use `GetEntityAncestorsRequest` instead +- `CatalogEntityAncestorsResponse` - please use `GetEntityAncestorsResponse` instead +- `CatalogListResponse` - please use `GetEntitiesResponse` instead diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index ce6397f51e..ef19effe90 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -130,18 +130,6 @@ export class CatalogClient implements CatalogApi { ): Promise; } -// @public @deprecated (undocumented) -export type CatalogEntitiesRequest = GetEntitiesRequest; - -// @public @deprecated (undocumented) -export type CatalogEntityAncestorsRequest = GetEntityAncestorsRequest; - -// @public @deprecated (undocumented) -export type CatalogEntityAncestorsResponse = GetEntityAncestorsResponse; - -// @public @deprecated (undocumented) -export type CatalogListResponse<_Entity> = GetEntitiesResponse; - // @public export interface CatalogRequestOptions { // (undocumented) diff --git a/packages/catalog-client/src/types/deprecated.ts b/packages/catalog-client/src/types/deprecated.ts deleted file mode 100644 index 0b145f3024..0000000000 --- a/packages/catalog-client/src/types/deprecated.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * 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 { - GetEntitiesRequest, - GetEntitiesResponse, - GetEntityAncestorsRequest, - GetEntityAncestorsResponse, -} from './api'; - -/** - * @public - * @deprecated use GetEntitiesRequest instead - */ -export type CatalogEntitiesRequest = GetEntitiesRequest; -/** - * @public - * @deprecated use GetEntitiesResponse instead - */ -export type CatalogListResponse<_Entity> = GetEntitiesResponse; -/** - * @public - * @deprecated use GetEntityAncestorsRequest instead - */ -export type CatalogEntityAncestorsRequest = GetEntityAncestorsRequest; -/** - * @public - * @deprecated use GetEntityAncestorsResponse instead - */ -export type CatalogEntityAncestorsResponse = GetEntityAncestorsResponse; diff --git a/packages/catalog-client/src/types/index.ts b/packages/catalog-client/src/types/index.ts index 39c8962b68..8bf4e34da2 100644 --- a/packages/catalog-client/src/types/index.ts +++ b/packages/catalog-client/src/types/index.ts @@ -28,5 +28,4 @@ export type { GetEntityFacetsRequest, GetEntityFacetsResponse, } from './api'; -export * from './deprecated'; export { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from './status'; From 880967948080d2bb56376be17265cc6ae70e41f7 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 10:21:46 +0100 Subject: [PATCH 141/353] chore: renaming refreshInterval to processingInterval Signed-off-by: blam --- .../DefaultProcessingDatabase.test.ts | 4 +- .../catalog-backend/src/processing/index.ts | 10 ++++- .../catalog-backend/src/processing/refresh.ts | 23 ++++++++++ .../src/service/CatalogBuilder.ts | 42 ++++++++++++++++--- 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts index 729be442aa..c7cd6b7026 100644 --- a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts +++ b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts @@ -28,7 +28,7 @@ import { DbRefreshStateRow, DbRelationsRow, } from './tables'; -import { createRandomRefreshInterval } from '../processing/refresh'; +import { createRandomProcessingInterval } from '../processing/refresh'; import { timestampToDateTime } from './conversion'; import { generateStableHash } from './util'; @@ -49,7 +49,7 @@ describe('Default Processing Database', () => { db: new DefaultProcessingDatabase({ database: knex, logger, - refreshInterval: createRandomRefreshInterval({ + refreshInterval: createRandomProcessingInterval({ minSeconds: 100, maxSeconds: 150, }), diff --git a/plugins/catalog-backend/src/processing/index.ts b/plugins/catalog-backend/src/processing/index.ts index dc8b07eceb..aec6a5d71c 100644 --- a/plugins/catalog-backend/src/processing/index.ts +++ b/plugins/catalog-backend/src/processing/index.ts @@ -23,5 +23,11 @@ export type { } from './types'; export { DefaultCatalogProcessingOrchestrator } from './DefaultCatalogProcessingOrchestrator'; -export { createRandomRefreshInterval } from './refresh'; -export type { RefreshIntervalFunction } from './refresh'; +export { + createRandomRefreshInterval, + createRandomProcessingInterval, +} from './refresh'; +export type { + RefreshIntervalFunction, + ProcessingIntervalFunction, +} from './refresh'; diff --git a/plugins/catalog-backend/src/processing/refresh.ts b/plugins/catalog-backend/src/processing/refresh.ts index 3e03e3add8..941c338912 100644 --- a/plugins/catalog-backend/src/processing/refresh.ts +++ b/plugins/catalog-backend/src/processing/refresh.ts @@ -16,13 +16,21 @@ /** * Function that returns the catalog refresh interval in seconds. + * @deprecated use {@link ProcessingIntervalFunction} instead * @public */ export type RefreshIntervalFunction = () => number; +/** + * Function that returns the catalog processing interval in seconds. + * @public + */ +export type ProcessingIntervalFunction = () => number; + /** * Creates a function that returns a random refresh interval between minSeconds and maxSeconds. * @returns A {@link RefreshIntervalFunction} that provides the next refresh interval + * @deprecated use {@link createRandomProcessingInterval} instead * @public */ export function createRandomRefreshInterval(options: { @@ -34,3 +42,18 @@ export function createRandomRefreshInterval(options: { return Math.random() * (maxSeconds - minSeconds) + minSeconds; }; } + +/** + * Creates a function that returns a random processing interval between minSeconds and maxSeconds. + * @returns A {@link ProcessingIntervalFunction} that provides the next processing interval + * @public + */ +export function createRandomProcessingInterval(options: { + minSeconds: number; + maxSeconds: number; +}): ProcessingIntervalFunction { + const { minSeconds, maxSeconds } = options; + return () => { + return Math.random() * (maxSeconds - minSeconds) + minSeconds; + }; +} diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index f62377decb..ce55bc5937 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -76,8 +76,9 @@ import { DefaultEntitiesCatalog } from './DefaultEntitiesCatalog'; import { DefaultCatalogProcessingOrchestrator } from '../processing/DefaultCatalogProcessingOrchestrator'; import { Stitcher } from '../stitching/Stitcher'; import { - createRandomRefreshInterval, + createRandomProcessingInterval, RefreshIntervalFunction, + ProcessingIntervalFunction, } from '../processing/refresh'; import { createRouter } from './createRouter'; import { DefaultRefreshService } from './DefaultRefreshService'; @@ -139,8 +140,8 @@ export class CatalogBuilder { private processors: CatalogProcessor[]; private processorsReplace: boolean; private parser: CatalogProcessorParser | undefined; - private refreshInterval: RefreshIntervalFunction = - createRandomRefreshInterval({ + private processingInterval: ProcessingIntervalFunction = + createRandomProcessingInterval({ minSeconds: 100, maxSeconds: 150, }); @@ -192,9 +193,25 @@ export class CatalogBuilder { * Seconds provided will be multiplied by 1.5 * The default refresh duration is 100-150 seconds. * setting this too low will potentially deplete request quotas to upstream services. + * + * @deprecated use {@link CatalogBuilder#setProcessingIntervalSeconds} instead */ setRefreshIntervalSeconds(seconds: number): CatalogBuilder { - this.refreshInterval = createRandomRefreshInterval({ + this.processingInterval = createRandomProcessingInterval({ + minSeconds: seconds, + maxSeconds: seconds * 1.5, + }); + return this; + } + + /** + * Processing interval determines how often entities should be processed. + * Seconds provided will be multiplied by 1.5 + * The default processing duration is 100-150 seconds. + * setting this too low will potentially deplete request quotas to upstream services. + */ + setProcessingIntervalSeconds(seconds: number): CatalogBuilder { + this.processingInterval = createRandomProcessingInterval({ minSeconds: seconds, maxSeconds: seconds * 1.5, }); @@ -204,9 +221,22 @@ export class CatalogBuilder { /** * Overwrites the default refresh interval function used to spread * entity updates in the catalog. + * + * @deprecated use {@link CatalogBuilder#setProcessingInterval} instead */ setRefreshInterval(refreshInterval: RefreshIntervalFunction): CatalogBuilder { - this.refreshInterval = refreshInterval; + this.processingInterval = refreshInterval; + return this; + } + + /** + * Overwrites the default processing interval function used to spread + * entity updates in the catalog. + */ + setProcessingInterval( + processingInterval: ProcessingIntervalFunction, + ): CatalogBuilder { + this.processingInterval = processingInterval; return this; } @@ -396,7 +426,7 @@ export class CatalogBuilder { const processingDatabase = new DefaultProcessingDatabase({ database: dbClient, logger, - refreshInterval: this.refreshInterval, + refreshInterval: this.processingInterval, }); const integrations = ScmIntegrations.fromConfig(config); const rulesEnforcer = DefaultCatalogRulesEnforcer.fromConfig(config); From b753d22a564249f547c24f601d782acf0c98a82f Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 10:26:49 +0100 Subject: [PATCH 142/353] chore: added changeset and added logging Signed-off-by: blam --- .changeset/wild-dolphins-lick.md | 7 +++++++ plugins/catalog-backend/src/service/CatalogBuilder.ts | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 .changeset/wild-dolphins-lick.md diff --git a/.changeset/wild-dolphins-lick.md b/.changeset/wild-dolphins-lick.md new file mode 100644 index 0000000000..cdd5827b00 --- /dev/null +++ b/.changeset/wild-dolphins-lick.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +**DEPRECATION**: Deprecated the `RefreshIntervalFunction` and `createRandomRefreshInterval` in favour of the `ProcessingIntervalFunction` and `createRandomProcessingInterval` type and method respectively. Please migrate to use the new names. + +**DEPRECATION**: Deprecated the `setRefreshInterval` and `setRefreshIntervalSeconds` methods on the `CatalogBuilder` for the new `setProcessingInterval` and `setProcessingIntervalSeconds` methods. Please migrate to use the new names. diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index ce55bc5937..3d7de6ba95 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -196,7 +196,10 @@ export class CatalogBuilder { * * @deprecated use {@link CatalogBuilder#setProcessingIntervalSeconds} instead */ - setRefreshIntervalSeconds(seconds: number): CatalogBuilder { + setRefreshIntervalSecon0ds(seconds: number): CatalogBuilder { + this.env.logger.warn( + '[DEPRECATION] - CatalogBuilder.setRefreshIntervalSeconds is deprecated. Use CatalogBuilder.setProcessingIntervalSeconds instead.', + ); this.processingInterval = createRandomProcessingInterval({ minSeconds: seconds, maxSeconds: seconds * 1.5, @@ -225,6 +228,9 @@ export class CatalogBuilder { * @deprecated use {@link CatalogBuilder#setProcessingInterval} instead */ setRefreshInterval(refreshInterval: RefreshIntervalFunction): CatalogBuilder { + this.env.logger.warn( + '[DEPRECATION] - CatalogBuilder.setRefreshInterval is deprecated. Use CatalogBuilder.setProcessingInterval instead.', + ); this.processingInterval = refreshInterval; return this; } From db5bf53ffe145b882b69332c0d5228d4eefd63e3 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 10:28:42 +0100 Subject: [PATCH 143/353] chore: updating api-report with deprecations Signed-off-by: blam --- plugins/catalog-backend/api-report.md | 19 +++++++++++++++++-- .../src/service/CatalogBuilder.ts | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index ee7b419a71..a0e5678e97 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -218,8 +218,14 @@ export class CatalogBuilder { key: string, resolver: PlaceholderResolver, ): CatalogBuilder; + setProcessingInterval( + processingInterval: ProcessingIntervalFunction, + ): CatalogBuilder; + setProcessingIntervalSeconds(seconds: number): CatalogBuilder; + // @deprecated setRefreshInterval(refreshInterval: RefreshIntervalFunction): CatalogBuilder; - setRefreshIntervalSeconds(seconds: number): CatalogBuilder; + // @deprecated + setRefreshIntervalSecon0ds(seconds: number): CatalogBuilder; } // @alpha @@ -408,6 +414,12 @@ export const createCatalogPolicyDecision: ( ) => ConditionalPolicyDecision; // @public +export function createRandomProcessingInterval(options: { + minSeconds: number; + maxSeconds: number; +}): ProcessingIntervalFunction; + +// @public @deprecated export function createRandomRefreshInterval(options: { minSeconds: number; maxSeconds: number; @@ -989,6 +1001,9 @@ export type PlaceholderResolverResolveUrl = ( base: string, ) => string; +// @public +export type ProcessingIntervalFunction = () => number; + // @public export const processingResult: Readonly<{ readonly notFoundError: ( @@ -1023,7 +1038,7 @@ export type RecursivePartial = { : T[P]; }; -// @public +// @public @deprecated export type RefreshIntervalFunction = () => number; // @public diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 3d7de6ba95..179233cb28 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -210,7 +210,7 @@ export class CatalogBuilder { /** * Processing interval determines how often entities should be processed. * Seconds provided will be multiplied by 1.5 - * The default processing duration is 100-150 seconds. + * The default processing interval is 100-150 seconds. * setting this too low will potentially deplete request quotas to upstream services. */ setProcessingIntervalSeconds(seconds: number): CatalogBuilder { From 2986f8e09d4d16bf6ddef0678afc3ca93c91c0bb Mon Sep 17 00:00:00 2001 From: Diego Bardari Date: Thu, 3 Mar 2022 10:38:31 +0100 Subject: [PATCH 144/353] Fixed EntityOwnerPicker and OwnershipCard url filter issue Signed-off-by: Diego Bardari --- .changeset/strong-beds-attack.md | 6 ++++++ .../catalog-react/src/hooks/useEntityListProvider.tsx | 2 +- .../Cards/OwnershipCard/OwnershipCard.test.tsx | 4 ++-- .../components/Cards/OwnershipCard/OwnershipCard.tsx | 11 ++++++++--- 4 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 .changeset/strong-beds-attack.md diff --git a/.changeset/strong-beds-attack.md b/.changeset/strong-beds-attack.md new file mode 100644 index 0000000000..f16c709daa --- /dev/null +++ b/.changeset/strong-beds-attack.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-org': patch +--- + +Fixed EntityOwnerPicker and OwnershipCard url filter issue with more than 21 owners diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index f448e41b82..72dfb7d1c3 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -195,7 +195,7 @@ export const EntityListProvider = ({ }); const newParams = qs.stringify( { ...oldParams, filters: queryParams }, - { addQueryPrefix: true }, + { addQueryPrefix: true, arrayFormat: 'repeat' }, ); const newUrl = `${window.location.pathname}${newParams}`; // We use direct history manipulation since useSearchParams and diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx index 3fc770e6b5..f6aaef6344 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx @@ -248,7 +248,7 @@ describe('OwnershipCard', () => { expect(getByText('OPENAPI').closest('a')).toHaveAttribute( 'href', - '/create/?filters%5Bkind%5D=API&filters%5Btype%5D=openapi&filters%5Bowners%5D%5B0%5D=my-team&filters%5Buser%5D=all', + '/create/?filters%5Bkind%5D=API&filters%5Btype%5D=openapi&filters%5Bowners%5D=my-team&filters%5Buser%5D=all', ); }); @@ -295,7 +295,7 @@ describe('OwnershipCard', () => { expect(getByText('OPENAPI').closest('a')).toHaveAttribute( 'href', - '/create/?filters%5Bkind%5D=API&filters%5Btype%5D=openapi&filters%5Bowners%5D%5B0%5D=user%3Athe-user&filters%5Bowners%5D%5B1%5D=my-team&filters%5Buser%5D=all', + '/create/?filters%5Bkind%5D=API&filters%5Btype%5D=openapi&filters%5Bowners%5D=user%3Athe-user&filters%5Bowners%5D=my-team&filters%5Buser%5D=all', ); }); }); diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx index 2b292cbe8b..1553bcac97 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx @@ -120,9 +120,14 @@ const getQueryParams = ( const user = owner as UserEntity; filters.owners = [...filters.owners, ...user.spec.memberOf]; } - const queryParams = qs.stringify({ - filters, - }); + const queryParams = qs.stringify( + { + filters, + }, + { + arrayFormat: 'repeat', + }, + ); return queryParams; }; From 11bf36fcbc7b9043843cc7263fe666201560beae Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 10:39:39 +0100 Subject: [PATCH 145/353] chore: move some more things to alpha Signed-off-by: blam --- .changeset/sharp-spies-study.md | 5 +++++ .../catalog-backend/src/permissions/rules/hasAnnotation.ts | 2 +- plugins/catalog-backend/src/permissions/rules/hasLabel.ts | 2 +- plugins/catalog-backend/src/permissions/rules/hasSpec.ts | 2 +- .../catalog-backend/src/permissions/rules/isEntityKind.ts | 2 +- .../catalog-backend/src/permissions/rules/isEntityOwner.ts | 2 +- 6 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .changeset/sharp-spies-study.md diff --git a/.changeset/sharp-spies-study.md b/.changeset/sharp-spies-study.md new file mode 100644 index 0000000000..3027aa1bd8 --- /dev/null +++ b/.changeset/sharp-spies-study.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Moved some more permissions things in the `catalog-backend` to `@alpha` diff --git a/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts index 81ebd52789..c6c5951153 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts @@ -21,7 +21,7 @@ import { createCatalogPermissionRule } from './util'; * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which * filters for the presence of an annotation on a given entity. * - * @public + * @alpha */ export const hasAnnotation = createCatalogPermissionRule({ name: 'HAS_ANNOTATION', diff --git a/plugins/catalog-backend/src/permissions/rules/hasLabel.ts b/plugins/catalog-backend/src/permissions/rules/hasLabel.ts index 04b00d68fa..7296eecd89 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasLabel.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasLabel.ts @@ -20,7 +20,7 @@ import { createCatalogPermissionRule } from './util'; /** * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which * filters for entities with a specified label in its metadata. - * @public + * @alpha */ export const hasLabel = createCatalogPermissionRule({ name: 'HAS_LABEL', diff --git a/plugins/catalog-backend/src/permissions/rules/hasSpec.ts b/plugins/catalog-backend/src/permissions/rules/hasSpec.ts index 891cf1d58c..63ae67d01c 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasSpec.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasSpec.ts @@ -23,6 +23,6 @@ import { createPropertyRule } from './createPropertyRule'; * * The key argument to the `apply` and `toQuery` methods can be nested, such as * 'field.nestedfield'. - * @public + * @alpha */ export const hasSpec = createPropertyRule('spec'); diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts b/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts index 6356c94dc4..c330c903d9 100644 --- a/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts +++ b/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts @@ -20,7 +20,7 @@ import { createCatalogPermissionRule } from './util'; /** * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which * filters for entities with a specified kind. - * @public + * @alpha */ export const isEntityKind = createCatalogPermissionRule({ name: 'IS_ENTITY_KIND', diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts index a6dda13c29..23f118abc4 100644 --- a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts +++ b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts @@ -21,7 +21,7 @@ import { createCatalogPermissionRule } from './util'; * A catalog {@link @backstage/plugin-permission-node#PermissionRule} which * filters for entities with a specified owner. * - * @public + * @alpha */ export const isEntityOwner = createCatalogPermissionRule({ name: 'IS_ENTITY_OWNER', From 9c238ca4c6e5e6c8a70b95d6bad01f9d0c09ae32 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 10:53:27 +0100 Subject: [PATCH 146/353] chore: moving isOwnerOf to alpha and documentation the limitations Signed-off-by: blam --- plugins/catalog-react/api-report.md | 4 ++-- plugins/catalog-react/src/utils/isOwnerOf.ts | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 75414e3d59..b482bed9a6 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -452,8 +452,8 @@ export function InspectEntityDialog(props: { onClose: () => void; }): JSX.Element | null; -// @public -export function isOwnerOf(owner: Entity, owned: Entity): boolean; +// @alpha +export function isOwnerOf(owner: Entity, entity: Entity): boolean; // @public @deprecated export function loadCatalogOwnerRefs( diff --git a/plugins/catalog-react/src/utils/isOwnerOf.ts b/plugins/catalog-react/src/utils/isOwnerOf.ts index f1ba0fa35f..2b38ace247 100644 --- a/plugins/catalog-react/src/utils/isOwnerOf.ts +++ b/plugins/catalog-react/src/utils/isOwnerOf.ts @@ -24,10 +24,15 @@ import { import { getEntityRelations } from './getEntityRelations'; /** - * Get the related entity references. - * @public + * Returns true if the `owner` argument is a direct owner on the `entity` argument. + * + * @alpha + * @remarks + * + * Note that this ownership is not the same as using the claims in the auth-resolver, it only will take into account ownership as expressed by direct entity relations. + * It doesn't know anything about the additional groups that a user might belong to which the claims contain. */ -export function isOwnerOf(owner: Entity, owned: Entity) { +export function isOwnerOf(owner: Entity, entity: Entity) { const possibleOwners = new Set( [ ...getEntityRelations(owner, RELATION_MEMBER_OF, { kind: 'group' }), @@ -35,7 +40,7 @@ export function isOwnerOf(owner: Entity, owned: Entity) { ].map(stringifyEntityRef), ); - const owners = getEntityRelations(owned, RELATION_OWNED_BY).map( + const owners = getEntityRelations(entity, RELATION_OWNED_BY).map( stringifyEntityRef, ); From 72431d7bedb26787c5b4cf22191fc5673fd0fd0e Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 10:55:08 +0100 Subject: [PATCH 147/353] chore: added changeset Signed-off-by: blam --- .changeset/polite-houses-wink.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/polite-houses-wink.md diff --git a/.changeset/polite-houses-wink.md b/.changeset/polite-houses-wink.md new file mode 100644 index 0000000000..4adceef6ee --- /dev/null +++ b/.changeset/polite-houses-wink.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +- Moving `isOwnerOf` to `@alpha` and documenting the limitations of this function with regards to only supporting direct relations. From 1de7dd85b00185472a07edf9b03d5fe2c36b39e4 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 11:08:57 +0100 Subject: [PATCH 148/353] chore: moving the CatalogEntityDocument to catalog-common Signed-off-by: blam --- plugins/catalog-backend/api-report.md | 16 ++-------- .../search/DefaultCatalogCollatorFactory.ts | 20 +++++-------- plugins/catalog-common/api-report.md | 15 ++++++++++ plugins/catalog-common/package.json | 3 +- plugins/catalog-common/src/index.ts | 2 ++ .../src/search/CatalogEntityDocument.ts | 29 +++++++++++++++++++ plugins/catalog-common/src/search/index.ts | 16 ++++++++++ 7 files changed, 74 insertions(+), 27 deletions(-) create mode 100644 plugins/catalog-common/src/search/CatalogEntityDocument.ts create mode 100644 plugins/catalog-common/src/search/index.ts diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index ee7b419a71..9dcb2db752 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -7,6 +7,7 @@ import { BitbucketIntegration } from '@backstage/integration'; import { CatalogApi } from '@backstage/catalog-client'; +import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { ConditionalPolicyDecision } from '@backstage/plugin-permission-node'; import { Conditions } from '@backstage/plugin-permission-node'; @@ -18,7 +19,6 @@ import express from 'express'; import { GetEntitiesRequest } from '@backstage/catalog-client'; import { GithubCredentialsProvider } from '@backstage/integration'; import { GitHubIntegrationConfig } from '@backstage/integration'; -import { IndexableDocument } from '@backstage/search-common'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { Location as Location_2 } from '@backstage/catalog-client'; @@ -248,19 +248,7 @@ export const catalogConditions: Conditions<{ >; }>; -// @public (undocumented) -export interface CatalogEntityDocument extends IndexableDocument { - // (undocumented) - componentType: string; - // (undocumented) - kind: string; - // (undocumented) - lifecycle: string; - // (undocumented) - namespace: string; - // (undocumented) - owner: string; -} +export { CatalogEntityDocument }; // @public (undocumented) export type CatalogEnvironment = { diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts index 99377b2ca0..811ac10210 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts @@ -29,21 +29,17 @@ import { UserEntity, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; +import { DocumentCollatorFactory } from '@backstage/search-common'; import { - DocumentCollatorFactory, - IndexableDocument, -} from '@backstage/search-common'; -import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common'; + catalogEntityReadPermission, + CatalogEntityDocument, +} from '@backstage/plugin-catalog-common'; import { Readable } from 'stream'; -/** @public */ -export interface CatalogEntityDocument extends IndexableDocument { - componentType: string; - namespace: string; - kind: string; - lifecycle: string; - owner: string; -} +/** + * @deprecated import from `@backstage/plugin-catalog-common` instead + */ +export type { CatalogEntityDocument }; /** @public */ export type DefaultCatalogCollatorFactoryOptions = { diff --git a/plugins/catalog-common/api-report.md b/plugins/catalog-common/api-report.md index 57f8c3983e..8ae9571d4c 100644 --- a/plugins/catalog-common/api-report.md +++ b/plugins/catalog-common/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { IndexableDocument } from '@backstage/search-common'; import { Permission } from '@backstage/plugin-permission-common'; // @alpha @@ -11,6 +12,20 @@ export const catalogEntityCreatePermission: Permission; // @alpha export const catalogEntityDeletePermission: Permission; +// @public +export interface CatalogEntityDocument extends IndexableDocument { + // (undocumented) + componentType: string; + // (undocumented) + kind: string; + // (undocumented) + lifecycle: string; + // (undocumented) + namespace: string; + // (undocumented) + owner: string; +} + // @alpha export const catalogEntityReadPermission: Permission; diff --git a/plugins/catalog-common/package.json b/plugins/catalog-common/package.json index acf4ed4aaa..bc0d3750ba 100644 --- a/plugins/catalog-common/package.json +++ b/plugins/catalog-common/package.json @@ -34,7 +34,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/plugin-permission-common": "^0.5.1" + "@backstage/plugin-permission-common": "^0.5.1", + "@backstage/search-common": "^0.2.4" }, "devDependencies": { "@backstage/cli": "^0.14.0" diff --git a/plugins/catalog-common/src/index.ts b/plugins/catalog-common/src/index.ts index 9eddf37452..08184eb871 100644 --- a/plugins/catalog-common/src/index.ts +++ b/plugins/catalog-common/src/index.ts @@ -31,3 +31,5 @@ export { catalogLocationCreatePermission, catalogLocationDeletePermission, } from './permissions'; + +export * from './search'; diff --git a/plugins/catalog-common/src/search/CatalogEntityDocument.ts b/plugins/catalog-common/src/search/CatalogEntityDocument.ts new file mode 100644 index 0000000000..e48cde431a --- /dev/null +++ b/plugins/catalog-common/src/search/CatalogEntityDocument.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { IndexableDocument } from '@backstage/search-common'; + +/** + * The Document format for an Entity in the Catalog for search + * + * @public + */ +export interface CatalogEntityDocument extends IndexableDocument { + componentType: string; + namespace: string; + kind: string; + lifecycle: string; + owner: string; +} diff --git a/plugins/catalog-common/src/search/index.ts b/plugins/catalog-common/src/search/index.ts new file mode 100644 index 0000000000..82e713aba0 --- /dev/null +++ b/plugins/catalog-common/src/search/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 type { CatalogEntityDocument } from './CatalogEntityDocument'; From ab7b6cb7b1116ea5376e997ba2bfadb272f6dc5b Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 11:13:15 +0100 Subject: [PATCH 149/353] chore: reworking exports Signed-off-by: blam --- .changeset/small-hornets-dress.md | 6 ++++++ .../src/search/DefaultCatalogCollator.ts | 11 ++++------- plugins/catalog-backend/src/search/index.ts | 4 +++- 3 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 .changeset/small-hornets-dress.md diff --git a/.changeset/small-hornets-dress.md b/.changeset/small-hornets-dress.md new file mode 100644 index 0000000000..6d6817977d --- /dev/null +++ b/.changeset/small-hornets-dress.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-common': patch +--- + +Moved the `CatalogEntityDocument` to `@backstage/plugin-catalog-common` and deprecated the export from `@backstage/plugin-catalog-backend` diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts index c59318d0af..a24c6d4f67 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts @@ -29,14 +29,11 @@ import { CatalogClient, GetEntitiesRequest, } from '@backstage/catalog-client'; -import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common'; -import { CatalogEntityDocument } from './DefaultCatalogCollatorFactory'; +import { + catalogEntityReadPermission, + CatalogEntityDocument, +} from '@backstage/plugin-catalog-common'; -/** - * @public - * @deprecated Upgrade to a more recent `@backstage/search-backend-node` and - * use `DefaultCatalogCollatorFactory` instead. - */ export class DefaultCatalogCollator { protected discovery: PluginEndpointDiscovery; protected locationTemplate: string; diff --git a/plugins/catalog-backend/src/search/index.ts b/plugins/catalog-backend/src/search/index.ts index 93ff0b8b32..bd54e33e59 100644 --- a/plugins/catalog-backend/src/search/index.ts +++ b/plugins/catalog-backend/src/search/index.ts @@ -16,7 +16,9 @@ export { DefaultCatalogCollatorFactory } from './DefaultCatalogCollatorFactory'; export type { DefaultCatalogCollatorFactoryOptions } from './DefaultCatalogCollatorFactory'; -export type { CatalogEntityDocument } from './DefaultCatalogCollatorFactory'; + +/** @public @deprecated use the export from `plugin-catalog-common` instead */ +export type { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; /** * todo(backstage/techdocs-core): stop exporting this in a future release. From 8b3678e120b3acd105b78bd29d6d9b2f0b3a3368 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 11:28:20 +0100 Subject: [PATCH 150/353] chore: reworking the api-report Signed-off-by: blam --- plugins/catalog-backend/api-report.md | 2 +- plugins/catalog-backend/src/search/DefaultCatalogCollator.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 9dcb2db752..ad07b002ea 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -404,7 +404,7 @@ export function createRandomRefreshInterval(options: { // @public export function createRouter(options: RouterOptions): Promise; -// @public @deprecated (undocumented) +// @public export class DefaultCatalogCollator { constructor(options: { discovery: PluginEndpointDiscovery; diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts index a24c6d4f67..a577147082 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts @@ -34,6 +34,10 @@ import { CatalogEntityDocument, } from '@backstage/plugin-catalog-common'; +/** + * The DefaultCatalogCollator for Search + * @public + */ export class DefaultCatalogCollator { protected discovery: PluginEndpointDiscovery; protected locationTemplate: string; From 633019509ac78b80ab2d7fcfc339857eab710d9b Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 11:38:26 +0100 Subject: [PATCH 151/353] chore: reworking some more things Signed-off-by: blam --- plugins/catalog-backend/src/search/DefaultCatalogCollator.ts | 1 + .../src/search/DefaultCatalogCollatorFactory.ts | 1 + plugins/catalog-common/api-report.md | 4 +++- plugins/catalog-common/src/search/CatalogEntityDocument.ts | 2 ++ 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts index a577147082..7700048b14 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts @@ -126,6 +126,7 @@ export class DefaultCatalogCollator { }), text: this.getDocumentText(entity), componentType: entity.spec?.type?.toString() || 'other', + type: entity.spec?.type?.toString() || 'other', namespace: entity.metadata.namespace || 'default', kind: entity.kind, lifecycle: (entity.spec?.lifecycle as string) || '', diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts index 811ac10210..ca6395b60a 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts @@ -155,6 +155,7 @@ export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory { }), text: this.getDocumentText(entity), componentType: entity.spec?.type?.toString() || 'other', + type: entity.spec?.type?.toString() || 'other', namespace: entity.metadata.namespace || 'default', kind: entity.kind, lifecycle: (entity.spec?.lifecycle as string) || '', diff --git a/plugins/catalog-common/api-report.md b/plugins/catalog-common/api-report.md index 8ae9571d4c..a73cae9994 100644 --- a/plugins/catalog-common/api-report.md +++ b/plugins/catalog-common/api-report.md @@ -14,7 +14,7 @@ export const catalogEntityDeletePermission: Permission; // @public export interface CatalogEntityDocument extends IndexableDocument { - // (undocumented) + // @deprecated (undocumented) componentType: string; // (undocumented) kind: string; @@ -24,6 +24,8 @@ export interface CatalogEntityDocument extends IndexableDocument { namespace: string; // (undocumented) owner: string; + // (undocumented) + type: string; } // @alpha diff --git a/plugins/catalog-common/src/search/CatalogEntityDocument.ts b/plugins/catalog-common/src/search/CatalogEntityDocument.ts index e48cde431a..005bc79c92 100644 --- a/plugins/catalog-common/src/search/CatalogEntityDocument.ts +++ b/plugins/catalog-common/src/search/CatalogEntityDocument.ts @@ -21,7 +21,9 @@ import { IndexableDocument } from '@backstage/search-common'; * @public */ export interface CatalogEntityDocument extends IndexableDocument { + /** @deprecated use `type` as well, as `componentType` will be removed after a few releases but we dont want to break indexing */ componentType: string; + type: string; namespace: string; kind: string; lifecycle: string; From 50b09a5fffb1bfa4540014ba11fdf4c3d7609e8d Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 11:43:31 +0100 Subject: [PATCH 152/353] chore: fix Signed-off-by: blam --- plugins/catalog-backend/api-report.md | 2 +- plugins/catalog-backend/src/search/DefaultCatalogCollator.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index ad07b002ea..9dcb2db752 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -404,7 +404,7 @@ export function createRandomRefreshInterval(options: { // @public export function createRouter(options: RouterOptions): Promise; -// @public +// @public @deprecated (undocumented) export class DefaultCatalogCollator { constructor(options: { discovery: PluginEndpointDiscovery; diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts index 7700048b14..ac9a33bf62 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts @@ -35,8 +35,9 @@ import { } from '@backstage/plugin-catalog-common'; /** - * The DefaultCatalogCollator for Search * @public + * @deprecated Upgrade to a more recent `@backstage/search-backend-node` and + * use `DefaultCatalogCollatorFactory` instead. */ export class DefaultCatalogCollator { protected discovery: PluginEndpointDiscovery; From ac2365b451439c25ce566e6f3e068dc5ef6b6624 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 11:46:27 +0100 Subject: [PATCH 153/353] chore: -0 Signed-off-by: blam --- plugins/catalog-backend/api-report.md | 2 +- plugins/catalog-backend/src/service/CatalogBuilder.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index a0e5678e97..a1125ec529 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -225,7 +225,7 @@ export class CatalogBuilder { // @deprecated setRefreshInterval(refreshInterval: RefreshIntervalFunction): CatalogBuilder; // @deprecated - setRefreshIntervalSecon0ds(seconds: number): CatalogBuilder; + setRefreshIntervalSeconds(seconds: number): CatalogBuilder; } // @alpha diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 179233cb28..b53335c469 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -196,7 +196,7 @@ export class CatalogBuilder { * * @deprecated use {@link CatalogBuilder#setProcessingIntervalSeconds} instead */ - setRefreshIntervalSecon0ds(seconds: number): CatalogBuilder { + setRefreshIntervalSeconds(seconds: number): CatalogBuilder { this.env.logger.warn( '[DEPRECATION] - CatalogBuilder.setRefreshIntervalSeconds is deprecated. Use CatalogBuilder.setProcessingIntervalSeconds instead.', ); From debfcd9515ffdce61648937310702d5388bf975a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 3 Mar 2022 11:50:46 +0100 Subject: [PATCH 154/353] move @types/json-schema to a dev dep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/beige-snails-drum.md | 5 +++++ packages/catalog-model/package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/beige-snails-drum.md diff --git a/.changeset/beige-snails-drum.md b/.changeset/beige-snails-drum.md new file mode 100644 index 0000000000..3476c9383b --- /dev/null +++ b/.changeset/beige-snails-drum.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': patch +--- + +Move `@types/json-schema` to be a dev dependency diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 70ed0fbe4d..f9781b14a2 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -37,7 +37,6 @@ "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", "@backstage/types": "^0.1.3", - "@types/json-schema": "^7.0.5", "ajv": "^7.0.3", "json-schema": "^0.4.0", "lodash": "^4.17.21", @@ -46,6 +45,7 @@ "devDependencies": { "@backstage/cli": "^0.14.1", "@types/jest": "^26.0.7", + "@types/json-schema": "^7.0.5", "@types/lodash": "^4.14.151", "yaml": "^1.9.2" }, From a52f69987a5aa958e8da1aa214629f1d50c56be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 3 Mar 2022 10:32:41 +0100 Subject: [PATCH 155/353] getEntityByName -> getEntityByRef MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/rotten-windows-worry.md | 5 ++ packages/catalog-client/api-report.md | 9 ++++ .../catalog-client/src/CatalogClient.test.ts | 54 +++++++++++++++++++ packages/catalog-client/src/CatalogClient.ts | 21 ++++++++ packages/catalog-client/src/types/api.ts | 14 +++++ 5 files changed, 103 insertions(+) create mode 100644 .changeset/rotten-windows-worry.md diff --git a/.changeset/rotten-windows-worry.md b/.changeset/rotten-windows-worry.md new file mode 100644 index 0000000000..a1ab353ce3 --- /dev/null +++ b/.changeset/rotten-windows-worry.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': patch +--- + +**DEPRECATION**: Deprecated `getEntityByName` from `CatalogApi` and added `getEntityByRef` instead, which accepts both string and compound ref forms. diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index ef19effe90..9bbb24463b 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -38,10 +38,15 @@ export interface CatalogApi { request: GetEntityAncestorsRequest, options?: CatalogRequestOptions, ): Promise; + // @deprecated getEntityByName( name: CompoundEntityRef, options?: CatalogRequestOptions, ): Promise; + getEntityByRef( + entityRef: string | CompoundEntityRef, + options?: CatalogRequestOptions, + ): Promise; getEntityFacets( request: GetEntityFacetsRequest, options?: CatalogRequestOptions, @@ -94,6 +99,10 @@ export class CatalogClient implements CatalogApi { compoundName: CompoundEntityRef, options?: CatalogRequestOptions, ): Promise; + getEntityByRef( + entityRef: string | CompoundEntityRef, + options?: CatalogRequestOptions, + ): Promise; getEntityFacets( request: GetEntityFacetsRequest, options?: CatalogRequestOptions, diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index 0035691f2c..0b838eb4c2 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -195,6 +195,60 @@ describe('CatalogClient', () => { }); }); + describe('getEntityByRef', () => { + const existingEntity: Entity = { + apiVersion: 'v1', + kind: 'CustomKind', + metadata: { + namespace: 'default', + name: 'exists', + }, + }; + + beforeEach(() => { + server.use( + rest.get( + `${mockBaseUrl}/entities/by-name/customkind/default/exists`, + (_, res, ctx) => { + return res(ctx.json(existingEntity)); + }, + ), + rest.get( + `${mockBaseUrl}/entities/by-name/customkind/default/missing`, + (_, res, ctx) => { + return res(ctx.status(404)); + }, + ), + ); + }); + + it('finds by string and compound', async () => { + await expect( + client.getEntityByRef('customkind:default/exists'), + ).resolves.toEqual(existingEntity); + await expect( + client.getEntityByRef({ + kind: 'CustomKind', + namespace: 'default', + name: 'exists', + }), + ).resolves.toEqual(existingEntity); + }); + + it('returns undefined for 404s', async () => { + await expect( + client.getEntityByRef('customkind:default/missing'), + ).resolves.toBeUndefined(); + await expect( + client.getEntityByRef({ + kind: 'CustomKind', + namespace: 'default', + name: 'missing', + }), + ).resolves.toBeUndefined(); + }); + }); + describe('getLocationById', () => { const defaultResponse = { data: { diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index da580ae09c..f2bc3ed8f6 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -170,6 +170,27 @@ export class CatalogClient implements CatalogApi { return { items: entities.sort(refCompare) }; } + /** + * {@inheritdoc CatalogApi.getEntityByRef} + */ + async getEntityByRef( + entityRef: string | CompoundEntityRef, + options?: CatalogRequestOptions, + ): Promise { + const { kind, namespace, name } = parseEntityRef(entityRef); + return this.requestOptional( + 'GET', + `/entities/by-name/${encodeURIComponent(kind)}/${encodeURIComponent( + namespace, + )}/${encodeURIComponent(name)}`, + options, + ); + } + + // NOTE(freben): When we deprecate getEntityByName from the interface, we may + // still want to leave this implementation in place for quite some time + // longer, to minimize the risk for breakages. Suggested date for removal: + // August 2022 /** * {@inheritdoc CatalogApi.getEntityByName} */ diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index 0ee2b57ffb..26af3889e2 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -306,6 +306,20 @@ export interface CatalogApi { * Gets a single entity from the catalog by its ref (kind, namespace, name) * triplet. * + * @param entityRef - A complete entity ref, either on string or compound form + * @param options - Additional options + * @returns The matching entity, or undefined if there was no entity with that ref + */ + getEntityByRef( + entityRef: string | CompoundEntityRef, + options?: CatalogRequestOptions, + ): Promise; + + /** + * Gets a single entity from the catalog by its ref (kind, namespace, name) + * triplet. + * + * @deprecated Use getEntityRef instead * @param name - A complete entity ref * @param options - Additional options */ From 0ca28cc2c702fa403b263bd5750156d54ffabc0e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Mar 2022 12:41:26 +0100 Subject: [PATCH 156/353] Update polite-houses-wink.md Signed-off-by: Patrik Oldsberg --- .changeset/polite-houses-wink.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/polite-houses-wink.md b/.changeset/polite-houses-wink.md index 4adceef6ee..3e177f5383 100644 --- a/.changeset/polite-houses-wink.md +++ b/.changeset/polite-houses-wink.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-react': patch --- -- Moving `isOwnerOf` to `@alpha` and documenting the limitations of this function with regards to only supporting direct relations. +- **BREAKING**: The `isOwnerOf` function has been marked as `@alpha` and is now only available via the `@backstage/plugin-catalog-react/alpha` import. The limitations of this function with regards to only supporting direct relations have also been documented. From c820a49426647212298e78eaf286a5e2e4232e05 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Fri, 25 Feb 2022 21:47:07 +0100 Subject: [PATCH 157/353] feat(msgraph): add `groupExpand` config option Add `groupExpand` allowing to use the `$expand` query parameter by the Microsoft Graph API to expand a single relationship. Relates-to: issue #9819 Relates-to: PR #9826 Signed-off-by: Patrick Jungermann --- .changeset/fluffy-trees-occur.md | 5 + .../catalog-backend-module-msgraph/README.md | 6 ++ .../api-report.md | 2 + .../src/microsoftGraph/config.test.ts | 2 + .../src/microsoftGraph/config.ts | 8 ++ .../src/microsoftGraph/read.test.ts | 98 ++++++++++++++++++- .../src/microsoftGraph/read.ts | 13 ++- .../MicrosoftGraphOrgReaderProcessor.ts | 1 + 8 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 .changeset/fluffy-trees-occur.md diff --git a/.changeset/fluffy-trees-occur.md b/.changeset/fluffy-trees-occur.md new file mode 100644 index 0000000000..6e41a61069 --- /dev/null +++ b/.changeset/fluffy-trees-occur.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +add config option `groupExpand` to allow expanding a single relationship diff --git a/plugins/catalog-backend-module-msgraph/README.md b/plugins/catalog-backend-module-msgraph/README.md index daaf1ec225..a253359471 100644 --- a/plugins/catalog-backend-module-msgraph/README.md +++ b/plugins/catalog-backend-module-msgraph/README.md @@ -51,6 +51,12 @@ catalog: # This and userFilter are mutually exclusive, only one can be specified # See https://docs.microsoft.com/en-us/graph/search-query-parameter userGroupMemberFilter: "displayName eq 'Backstage Users'" + # Optional parameter to include the expanded resource or collection referenced + # by a single relationship (navigation property) in your results. + # Only one relationship can be expanded in a single request. + # See https://docs.microsoft.com/en-us/graph/query-parameters#expand-parameter + # Can be combined with userGroupMember[...] instead of userFilter. + groupExpand: member # Optional search for users, use group membership to get users. # (Search for groups and fetch their members.) # This and userFilter are mutually exclusive, only one can be specified diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index 574b948dea..8df06f54b1 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -164,6 +164,7 @@ export type MicrosoftGraphProviderConfig = { userExpand?: string; userGroupMemberFilter?: string; userGroupMemberSearch?: string; + groupExpand?: string; groupFilter?: string; groupSearch?: string; }; @@ -198,6 +199,7 @@ export function readMicrosoftGraphOrg( userFilter?: string; userGroupMemberSearch?: string; userGroupMemberFilter?: string; + groupExpand?: string; groupSearch?: string; groupFilter?: string; userTransformer?: UserTransformer; diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts index 700e5cd8fe..cfc5c1cbb0 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts @@ -55,6 +55,7 @@ describe('readMicrosoftGraphConfig', () => { authority: 'https://login.example.com/', userExpand: 'manager', userFilter: 'accountEnabled eq true', + groupExpand: 'member', groupFilter: 'securityEnabled eq false', }, ], @@ -69,6 +70,7 @@ describe('readMicrosoftGraphConfig', () => { authority: 'https://login.example.com', userExpand: 'manager', userFilter: 'accountEnabled eq true', + groupExpand: 'member', groupFilter: 'securityEnabled eq false', }, ]; diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts index c2789d767a..b7fbcfb8a9 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts @@ -70,6 +70,12 @@ export type MicrosoftGraphProviderConfig = { * E.g. "\"displayName:-team\"" would only match groups which contain '-team' */ userGroupMemberSearch?: string; + /** + * The "expand" argument to apply to groups. + * + * E.g. "member" + */ + groupExpand?: string; /** * The filter to apply to extract groups. * @@ -115,6 +121,7 @@ export function readMicrosoftGraphConfig( const userGroupMemberSearch = providerConfig.getOptionalString( 'userGroupMemberSearch', ); + const groupExpand = providerConfig.getOptionalString('groupExpand'); const groupFilter = providerConfig.getOptionalString('groupFilter'); const groupSearch = providerConfig.getOptionalString('groupSearch'); @@ -139,6 +146,7 @@ export function readMicrosoftGraphConfig( userFilter, userGroupMemberFilter, userGroupMemberSearch, + groupExpand, groupFilter, groupSearch, }); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts index dba3741c73..e3d6897d66 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts @@ -236,7 +236,7 @@ describe('read microsoft graph', () => { expect(client.getUserPhotoWithSizeLimit).toBeCalledWith('userid', 120); }); - it('should read users with userExpand and custom transformer', async () => { + it('should read users with userExpand, groupExpand and custom transformer', async () => { async function* getExampleGroups() { yield { id: 'groupid', @@ -272,6 +272,7 @@ describe('read microsoft graph', () => { const { users } = await readMicrosoftGraphUsersInGroups(client, { userExpand: 'manager', userGroupMemberFilter: 'securityEnabled eq true', + groupExpand: 'member', transformer: async () => ({ apiVersion: 'backstage.io/v1alpha1', kind: 'User', @@ -292,6 +293,7 @@ describe('read microsoft graph', () => { expect(client.getGroups).toBeCalledTimes(1); expect(client.getGroups).toBeCalledWith({ + expand: 'member', filter: 'securityEnabled eq true', }); expect(client.getGroupMembers).toBeCalledTimes(1); @@ -453,6 +455,100 @@ describe('read microsoft graph', () => { // expect(client.getGroupPhotoWithSizeLimit).toBeCalledWith('groupid', 120); }); + it('should read groups with groupExpand', async () => { + async function* getExampleGroups() { + yield { + id: 'groupid', + displayName: 'Group Name', + description: 'Group Description', + mail: 'group@example.com', + }; + } + + async function* getExampleGroupMembers(): AsyncIterable { + yield { + '@odata.type': '#microsoft.graph.group', + id: 'childgroupid', + }; + yield { + '@odata.type': '#microsoft.graph.user', + id: 'userid', + }; + } + + client.getGroups.mockImplementation(getExampleGroups); + client.getGroupMembers.mockImplementation(getExampleGroupMembers); + client.getOrganization.mockResolvedValue({ + id: 'tenantid', + displayName: 'Organization Name', + }); + client.getGroupPhotoWithSizeLimit.mockResolvedValue( + 'data:image/jpeg;base64,...', + ); + + const { groups, groupMember, groupMemberOf, rootGroup } = + await readMicrosoftGraphGroups(client, 'tenantid', { + groupExpand: 'member', + groupFilter: 'securityEnabled eq false', + }); + + const expectedRootGroup = group({ + metadata: { + annotations: { + 'graph.microsoft.com/tenant-id': 'tenantid', + }, + name: 'organization_name', + description: 'Organization Name', + }, + spec: { + type: 'root', + profile: { + displayName: 'Organization Name', + }, + children: [], + }, + }); + expect(groups).toEqual([ + expectedRootGroup, + group({ + metadata: { + annotations: { + 'graph.microsoft.com/group-id': 'groupid', + }, + name: 'group_name', + description: 'Group Description', + }, + spec: { + type: 'team', + profile: { + displayName: 'Group Name', + email: 'group@example.com', + // TODO: Loading groups photos doesn't work right now as Microsoft + // Graph doesn't allows this yet + /* picture: 'data:image/jpeg;base64,...',*/ + }, + children: [], + }, + }), + ]); + expect(rootGroup).toEqual(expectedRootGroup); + expect(groupMember.get('groupid')).toEqual(new Set(['childgroupid'])); + expect(groupMemberOf.get('userid')).toEqual(new Set(['groupid'])); + expect(groupMember.get('organization_name')).toEqual(new Set()); + + expect(client.getGroups).toBeCalledTimes(1); + expect(client.getGroups).toBeCalledWith({ + expand: 'member', + filter: 'securityEnabled eq false', + }); + expect(client.getGroupMembers).toBeCalledTimes(1); + expect(client.getGroupMembers).toBeCalledWith('groupid'); + // TODO: Loading groups photos doesn't work right now as Microsoft Graph + // doesn't allows this yet + // expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1); + // expect(client.getGroupPhotoWithSizeLimit).toBeCalledWith('groupid', 120); + }); + it('should read security groups', async () => { async function* getExampleGroups() { yield { diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index 333eaa6b0a..ed6ca76e68 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -140,6 +140,7 @@ export async function readMicrosoftGraphUsersInGroups( userExpand?: string; userGroupMemberSearch?: string; userGroupMemberFilter?: string; + groupExpand?: string; transformer?: UserTransformer; logger: Logger; }, @@ -150,15 +151,16 @@ export async function readMicrosoftGraphUsersInGroups( const limiter = limiterFactory(10); - const transformer = options?.transformer ?? defaultUserTransformer; + const transformer = options.transformer ?? defaultUserTransformer; const userGroupMemberPromises: Promise[] = []; const userPromises: Promise[] = []; const groupMemberUsers: Set = new Set(); for await (const group of client.getGroups({ - search: options?.userGroupMemberSearch, - filter: options?.userGroupMemberFilter, + expand: options.groupExpand, + search: options.userGroupMemberSearch, + filter: options.userGroupMemberFilter, })) { // Process all groups in parallel, otherwise it can take quite some time userGroupMemberPromises.push( @@ -329,8 +331,9 @@ export async function readMicrosoftGraphGroups( client: MicrosoftGraphClient, tenantId: string, options?: { - groupSearch?: string; + groupExpand?: string; groupFilter?: string; + groupSearch?: string; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; }, @@ -357,6 +360,7 @@ export async function readMicrosoftGraphGroups( const promises: Promise[] = []; for await (const group of client.getGroups({ + expand: options?.groupExpand, search: options?.groupSearch, filter: options?.groupFilter, })) { @@ -513,6 +517,7 @@ export async function readMicrosoftGraphOrg( userFilter?: string; userGroupMemberSearch?: string; userGroupMemberFilter?: string; + groupExpand?: string; groupSearch?: string; groupFilter?: string; userTransformer?: UserTransformer; diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts index 942bde19eb..9037bd8728 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts @@ -109,6 +109,7 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { userFilter: provider.userFilter, userGroupMemberFilter: provider.userGroupMemberFilter, userGroupMemberSearch: provider.userGroupMemberSearch, + groupExpand: provider.groupExpand, groupFilter: provider.groupFilter, groupSearch: provider.groupSearch, userTransformer: this.userTransformer, From c870c3bb99faeecc39cd918c34c310fd1d1582d9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Mar 2022 12:44:42 +0100 Subject: [PATCH 158/353] changesets: removed changeset catalog permission alpha tweaks Signed-off-by: Patrik Oldsberg --- .changeset/sharp-spies-study.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/sharp-spies-study.md diff --git a/.changeset/sharp-spies-study.md b/.changeset/sharp-spies-study.md deleted file mode 100644 index 3027aa1bd8..0000000000 --- a/.changeset/sharp-spies-study.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Moved some more permissions things in the `catalog-backend` to `@alpha` From 5da4d0cfbd9feccdca1be3e2f7129a3c6edd4a6e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Mar 2022 12:46:47 +0100 Subject: [PATCH 159/353] catalog-backend: also mark hasMetadata as alpha Signed-off-by: Patrik Oldsberg --- plugins/catalog-backend/src/permissions/rules/hasMetadata.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-backend/src/permissions/rules/hasMetadata.ts b/plugins/catalog-backend/src/permissions/rules/hasMetadata.ts index f5f25a5ecf..cf14faea14 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasMetadata.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasMetadata.ts @@ -23,6 +23,6 @@ import { createPropertyRule } from './createPropertyRule'; * * The key argument to the `apply` and `toQuery` methods can be nested, such as * 'field.nestedfield'. - * @public + * @alpha */ export const hasMetadata = createPropertyRule('metadata'); From 8a687faa7bfb2616345a55a4e06e4dd618298cb4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Mar 2022 12:51:15 +0100 Subject: [PATCH 160/353] Update tasty-poems-raise.md Signed-off-by: Patrik Oldsberg --- .changeset/tasty-poems-raise.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tasty-poems-raise.md b/.changeset/tasty-poems-raise.md index 47556cb3ad..fbd81423b6 100644 --- a/.changeset/tasty-poems-raise.md +++ b/.changeset/tasty-poems-raise.md @@ -2,4 +2,4 @@ '@backstage/plugin-auth-backend': minor --- -Added validation to TokenFactory.issueToken that ensure any sub claim given is a valid entityRef. This will affect any custom resolver functions given to auth providers. +**BREAKING**: The `TokenFactory.issueToken` used by custom sign-in resolvers now ensures that the sub claim given is a full entity reference of the format `:/`. Any existing custom sign-in resolver functions that do not supply a full entity reference must be updated. From 899f196af5df1b0bc49e4135bdf07a0583faecc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 3 Mar 2022 11:26:42 +0100 Subject: [PATCH 161/353] update to use getEntityByRef MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/gentle-icons-vanish.md | 20 ++++++++++++ plugins/api-docs/dev/index.tsx | 4 +-- .../DefaultApiExplorerPage.test.tsx | 10 ++++-- .../lib/catalog/CatalogIdentityClient.test.ts | 1 + .../badges-backend/src/service/router.test.ts | 17 +++++----- plugins/badges-backend/src/service/router.ts | 4 +-- plugins/catalog-graph/dev/index.tsx | 8 +++-- .../CatalogGraphCard.test.tsx | 5 +-- .../CatalogGraphPage.test.tsx | 7 +++-- .../EntityRelationsGraph.test.tsx | 31 ++++++++++--------- .../useEntityStore.test.ts | 17 +++++----- .../EntityRelationsGraph/useEntityStore.ts | 6 ++-- .../src/api/CatalogImportClient.test.ts | 1 + .../StepPrepareCreatePullRequest.test.tsx | 1 + .../UserListPicker/UserListPicker.test.tsx | 2 +- plugins/catalog-react/src/hooks/useEntity.tsx | 2 +- .../src/hooks/useEntityListProvider.test.tsx | 2 +- .../src/hooks/useEntityOwnership.test.tsx | 14 ++++----- .../src/hooks/useEntityOwnership.ts | 2 +- plugins/catalog-react/src/hooks/useOwnUser.ts | 7 ++++- .../CatalogEntityPage/useEntityFromUrl.ts | 2 +- .../CatalogPage/DefaultCatalogPage.test.tsx | 5 +-- .../src/service/router.ts | 21 +++++-------- .../DefaultExplorePage.test.tsx | 1 + .../DomainExplorerContent.test.tsx | 1 + .../GroupsExplorerContent.test.tsx | 1 + .../components/FossaPage/FossaPage.test.tsx | 1 + plugins/jenkins-backend/README.md | 2 +- .../src/service/jenkinsInfoProvider.test.ts | 20 ++++++------ .../src/service/jenkinsInfoProvider.ts | 2 +- plugins/rollbar/src/hooks/useCatalogEntity.ts | 2 +- .../scaffolder-backend/src/service/helpers.ts | 7 +++-- .../src/service/router.test.ts | 2 +- .../src/service/CachedEntityLoader.test.ts | 12 +++---- .../src/service/CachedEntityLoader.ts | 6 ++-- .../components/DefaultTechDocsHome.test.tsx | 2 +- .../components/LegacyTechDocsHome.test.tsx | 2 +- .../components/TechDocsCustomHome.test.tsx | 2 +- .../home/components/TechDocsCustomHome.tsx | 7 ++++- .../src/service/TodoReaderService.test.ts | 7 +++-- .../src/service/TodoReaderService.ts | 2 +- 41 files changed, 157 insertions(+), 111 deletions(-) create mode 100644 .changeset/gentle-icons-vanish.md diff --git a/.changeset/gentle-icons-vanish.md b/.changeset/gentle-icons-vanish.md new file mode 100644 index 0000000000..27ea78863b --- /dev/null +++ b/.changeset/gentle-icons-vanish.md @@ -0,0 +1,20 @@ +--- +'@backstage/plugin-api-docs': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-badges-backend': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-graph': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-code-coverage-backend': patch +'@backstage/plugin-explore': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-jenkins-backend': patch +'@backstage/plugin-rollbar': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-techdocs': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-todo-backend': patch +--- + +Use `getEntityByRef` instead of `getEntityByName` in the catalog client diff --git a/plugins/api-docs/dev/index.tsx b/plugins/api-docs/dev/index.tsx index 633a285b98..9975e91e98 100644 --- a/plugins/api-docs/dev/index.tsx +++ b/plugins/api-docs/dev/index.tsx @@ -53,8 +53,8 @@ createDevApp() items: mockEntities.slice(), }; }, - async getEntityByName(name: string) { - return mockEntities.find(e => e.metadata.name === name); + async getEntityByRef(ref: string) { + return mockEntities.find(e => e.metadata.name === ref); }, } as unknown as typeof catalogApiRef.T), }) diff --git a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx index 5924af3ef3..bc413661a8 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx @@ -14,7 +14,11 @@ * limitations under the License. */ -import { Entity, RELATION_MEMBER_OF } from '@backstage/catalog-model'; +import { + Entity, + parseEntityRef, + RELATION_MEMBER_OF, +} from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/core-app-api'; import { TableColumn, TableProps } from '@backstage/core-components'; import { @@ -60,11 +64,11 @@ describe('DefaultApiExplorerPage', () => { }), getLocationByRef: () => Promise.resolve({ id: 'id', type: 'url', target: 'url' }), - getEntityByName: async entityName => { + getEntityByRef: async entityRef => { return { apiVersion: 'backstage.io/v1alpha1', kind: 'User', - metadata: { name: entityName.name }, + metadata: { name: parseEntityRef(entityRef).name }, relations: [ { type: RELATION_MEMBER_OF, diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index d2cee7bd89..08c46f2c21 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -26,6 +26,7 @@ import { CatalogIdentityClient } from './CatalogIdentityClient'; describe('CatalogIdentityClient', () => { const catalogApi: jest.Mocked = { getLocationById: jest.fn(), + getEntityByRef: jest.fn(), getEntityByName: jest.fn(), getEntities: jest.fn(), addLocation: jest.fn(), diff --git a/plugins/badges-backend/src/service/router.test.ts b/plugins/badges-backend/src/service/router.test.ts index 56dbfb0655..c492449cb3 100644 --- a/plugins/badges-backend/src/service/router.test.ts +++ b/plugins/badges-backend/src/service/router.test.ts @@ -60,6 +60,7 @@ describe('createRouter', () => { catalog = { addLocation: jest.fn(), getEntities: jest.fn(), + getEntityByRef: jest.fn(), getEntityByName: jest.fn(), getLocationByRef: jest.fn(), getLocationById: jest.fn(), @@ -103,7 +104,7 @@ describe('createRouter', () => { describe('GET /entity/:namespace/:kind/:name/badge-specs', () => { it('returns all badge specs for entity', async () => { - catalog.getEntityByName.mockResolvedValueOnce(entity); + catalog.getEntityByRef.mockResolvedValueOnce(entity); badgeBuilder.getBadges.mockResolvedValueOnce([{ id: badge.id }]); badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge); @@ -115,8 +116,8 @@ describe('createRouter', () => { expect(response.status).toEqual(200); expect(response.text).toEqual(JSON.stringify([badge], null, 2)); - expect(catalog.getEntityByName).toHaveBeenCalledTimes(1); - expect(catalog.getEntityByName).toHaveBeenCalledWith( + expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1); + expect(catalog.getEntityByRef).toHaveBeenCalledWith( { namespace: 'default', kind: 'service', @@ -142,7 +143,7 @@ describe('createRouter', () => { describe('GET /entity/:namespace/:kind/:name/badge/test-badge', () => { it('returns badge for entity', async () => { - catalog.getEntityByName.mockResolvedValueOnce(entity); + catalog.getEntityByRef.mockResolvedValueOnce(entity); const image = '...'; badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image); @@ -154,8 +155,8 @@ describe('createRouter', () => { expect(response.status).toEqual(200); expect(response.body).toEqual(Buffer.from(image)); - expect(catalog.getEntityByName).toHaveBeenCalledTimes(1); - expect(catalog.getEntityByName).toHaveBeenCalledWith( + expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1); + expect(catalog.getEntityByRef).toHaveBeenCalledWith( { namespace: 'default', kind: 'service', @@ -179,7 +180,7 @@ describe('createRouter', () => { }); it('returns badge spec for entity', async () => { - catalog.getEntityByName.mockResolvedValueOnce(entity); + catalog.getEntityByRef.mockResolvedValueOnce(entity); badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge); const url = '/entity/default/service/test/badge/test-badge?format=json'; @@ -192,7 +193,7 @@ describe('createRouter', () => { describe('Errors', () => { it('returns 404 for unknown entities', async () => { - catalog.getEntityByName.mockResolvedValue(undefined); + catalog.getEntityByRef.mockResolvedValue(undefined); async function testUrl(url: string) { const response = await request(app).get(url); expect(response.status).toEqual(404); diff --git a/plugins/badges-backend/src/service/router.ts b/plugins/badges-backend/src/service/router.ts index 196ddf36da..b7211a0aad 100644 --- a/plugins/badges-backend/src/service/router.ts +++ b/plugins/badges-backend/src/service/router.ts @@ -46,7 +46,7 @@ export async function createRouter( router.get('/entity/:namespace/:kind/:name/badge-specs', async (req, res) => { const { namespace, kind, name } = req.params; - const entity = await catalog.getEntityByName( + const entity = await catalog.getEntityByRef( { namespace, kind, name }, { token: getBearerToken(req.headers.authorization), @@ -84,7 +84,7 @@ export async function createRouter( '/entity/:namespace/:kind/:name/badge/:badgeId', async (req, res) => { const { namespace, kind, name, badgeId } = req.params; - const entity = await catalog.getEntityByName( + const entity = await catalog.getEntityByRef( { namespace, kind, name }, { token: getBearerToken(req.headers.authorization), diff --git a/plugins/catalog-graph/dev/index.tsx b/plugins/catalog-graph/dev/index.tsx index 7413634497..7d772702a1 100644 --- a/plugins/catalog-graph/dev/index.tsx +++ b/plugins/catalog-graph/dev/index.tsx @@ -139,10 +139,12 @@ createDevApp() deps: {}, factory() { return { - async getEntityByName( - name: CompoundEntityRef, + async getEntityByRef( + ref: string | CompoundEntityRef, ): Promise { - return entities[stringifyEntityRef(name)]; + return entities[ + typeof ref === 'string' ? ref : stringifyEntityRef(ref) + ]; }, async getEntities(): Promise { return { items: Object.values(entities) }; diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx index 7cebdee692..858fca1a83 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx @@ -57,7 +57,8 @@ describe('', () => { }; catalog = { getEntities: jest.fn(), - getEntityByName: jest.fn(async _ => ({ ...entity, relations: [] })), + getEntityByRef: jest.fn(async _ => ({ ...entity, relations: [] })), + getEntityByName: jest.fn(), removeEntityByUid: jest.fn(), getLocationById: jest.fn(), getLocationByRef: jest.fn(), @@ -88,7 +89,7 @@ describe('', () => { expect(await findByText('b:d/c')).toBeInTheDocument(); expect(await findAllByTestId('node')).toHaveLength(1); - expect(catalog.getEntityByName).toBeCalledTimes(1); + expect(catalog.getEntityByRef).toBeCalledTimes(1); }); test('renders with custom title', async () => { diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx index 2f686f8f62..0bf6469123 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx @@ -88,7 +88,10 @@ describe('', () => { }; catalog = { getEntities: jest.fn(), - getEntityByName: jest.fn(async n => (n.name === 'e' ? entityE : entityC)), + getEntityByRef: jest.fn(async (n: any) => + n === 'b:d/e' ? entityE : entityC, + ), + getEntityByName: jest.fn(), removeEntityByUid: jest.fn(), getLocationById: jest.fn(), getLocationByRef: jest.fn(), @@ -128,7 +131,7 @@ describe('', () => { expect(await findByText('b:d/c')).toBeInTheDocument(); expect(await findByText('b:d/e')).toBeInTheDocument(); expect(await findAllByTestId('node')).toHaveLength(2); - expect(catalog.getEntityByName).toBeCalledTimes(2); + expect(catalog.getEntityByRef).toBeCalledTimes(2); }); test('should toggle filters', async () => { diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx index 36dc816444..58ff81a91f 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Entity, RELATION_HAS_PART, RELATION_OWNED_BY, RELATION_OWNER_OF, RELATION_PART_OF, - stringifyEntityRef, } from '@backstage/catalog-model'; import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; @@ -155,7 +155,8 @@ describe('', () => { }; catalog = { getEntities: jest.fn(), - getEntityByName: jest.fn(async n => entities[stringifyEntityRef(n)]), + getEntityByRef: jest.fn(async n => entities[n as string]), + getEntityByName: jest.fn(), removeEntityByUid: jest.fn(), getLocationById: jest.fn(), getLocationByRef: jest.fn(), @@ -178,7 +179,7 @@ describe('', () => { }); test('renders a single node without exploding', async () => { - catalog.getEntityByName.mockResolvedValue({ + catalog.getEntityByRef.mockResolvedValue({ apiVersion: 'a', kind: 'b', metadata: { @@ -198,11 +199,11 @@ describe('', () => { expect(await findByText('b:d/c')).toBeInTheDocument(); expect(await findAllByTestId('node')).toHaveLength(1); - expect(catalog.getEntityByName).toBeCalledTimes(1); + expect(catalog.getEntityByRef).toBeCalledTimes(1); }); test('renders a progress indicator while loading', async () => { - catalog.getEntityByName.mockImplementation(() => new Promise(() => {})); + catalog.getEntityByRef.mockImplementation(() => new Promise(() => {})); const { findByRole } = await renderInTestApp( @@ -213,12 +214,12 @@ describe('', () => { ); expect(await findByRole('progressbar')).toBeInTheDocument(); - expect(catalog.getEntityByName).toBeCalledTimes(1); + expect(catalog.getEntityByRef).toBeCalledTimes(1); }); test('does not explode if an entity is missing', async () => { - catalog.getEntityByName.mockImplementation(async n => { - if (n.name === 'c') { + catalog.getEntityByRef.mockImplementation(async (n: any) => { + if (n === 'b:d/c') { return { apiVersion: 'a', kind: 'b', @@ -253,7 +254,7 @@ describe('', () => { expect(await findByText('b:d/c')).toBeInTheDocument(); expect(await findAllByTestId('node')).toHaveLength(1); - expect(catalog.getEntityByName).toBeCalledTimes(2); + expect(catalog.getEntityByRef).toBeCalledTimes(2); }); test('renders at max depth of one', async () => { @@ -276,7 +277,7 @@ describe('', () => { expect(await findAllByText('hasPart')).toHaveLength(1); expect(await findAllByTestId('label')).toHaveLength(2); - expect(catalog.getEntityByName).toBeCalledTimes(3); + expect(catalog.getEntityByRef).toBeCalledTimes(3); }); test('renders simplied graph at full depth', async () => { @@ -301,7 +302,7 @@ describe('', () => { expect(await findAllByText('hasPart')).toHaveLength(2); expect(await findAllByTestId('label')).toHaveLength(3); - expect(catalog.getEntityByName).toBeCalledTimes(4); + expect(catalog.getEntityByRef).toBeCalledTimes(4); }); test('renders full graph at full depth', async () => { @@ -328,7 +329,7 @@ describe('', () => { expect(await findAllByText('partOf')).toHaveLength(2); expect(await findAllByTestId('label')).toHaveLength(8); - expect(catalog.getEntityByName).toBeCalledTimes(4); + expect(catalog.getEntityByRef).toBeCalledTimes(4); }); test('renders full graph at full depth with merged relations', async () => { @@ -353,7 +354,7 @@ describe('', () => { expect(await findAllByText('hasPart')).toHaveLength(2); expect(await findAllByTestId('label')).toHaveLength(4); - expect(catalog.getEntityByName).toBeCalledTimes(4); + expect(catalog.getEntityByRef).toBeCalledTimes(4); }); test('renders a graph with multiple root nodes', async () => { @@ -379,7 +380,7 @@ describe('', () => { expect(await findAllByText('partOf')).toHaveLength(2); expect(await findAllByTestId('label')).toHaveLength(3); - expect(catalog.getEntityByName).toBeCalledTimes(4); + expect(catalog.getEntityByRef).toBeCalledTimes(4); }); test('renders a graph with filtered kinds and relations', async () => { @@ -401,7 +402,7 @@ describe('', () => { expect(await findAllByText('ownerOf')).toHaveLength(1); expect(await findAllByTestId('label')).toHaveLength(1); - expect(catalog.getEntityByName).toBeCalledTimes(2); + expect(catalog.getEntityByRef).toBeCalledTimes(2); }); test('handle clicks on a node', async () => { diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts index a5dc82eab9..5349b64a72 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts @@ -29,6 +29,7 @@ describe('useEntityStore', () => { beforeEach(() => { catalogApi = { getEntities: jest.fn(), + getEntityByRef: jest.fn(), getEntityByName: jest.fn(), removeEntityByUid: jest.fn(), getLocationById: jest.fn(), @@ -64,7 +65,7 @@ describe('useEntityStore', () => { }, }; - catalogApi.getEntityByName.mockResolvedValue(entity); + catalogApi.getEntityByRef.mockResolvedValue(entity); const { result, waitFor } = renderHook(() => useEntityStore()); @@ -84,7 +85,7 @@ describe('useEntityStore', () => { test('handles request failures', async () => { const err = new Error('Hello World'); - catalogApi.getEntityByName.mockRejectedValue(err); + catalogApi.getEntityByRef.mockRejectedValue(err); const { result, waitFor } = renderHook(() => useEntityStore()); @@ -101,7 +102,7 @@ describe('useEntityStore', () => { }); test('handles loading', async () => { - catalogApi.getEntityByName.mockReturnValue(new Promise(() => {})); + catalogApi.getEntityByRef.mockReturnValue(new Promise(() => {})); const { result } = renderHook(() => useEntityStore()); @@ -133,7 +134,7 @@ describe('useEntityStore', () => { }, }; - catalogApi.getEntityByName.mockResolvedValue(entity1); + catalogApi.getEntityByRef.mockResolvedValue(entity1); const { result, waitFor } = renderHook(() => useEntityStore()); @@ -150,7 +151,7 @@ describe('useEntityStore', () => { }); }); - catalogApi.getEntityByName.mockResolvedValue(entity2); + catalogApi.getEntityByRef.mockResolvedValue(entity2); act(() => { result.current.requestEntities([ @@ -188,7 +189,7 @@ describe('useEntityStore', () => { }, }; - catalogApi.getEntityByName.mockResolvedValue(entity1); + catalogApi.getEntityByRef.mockResolvedValue(entity1); const { result, waitFor } = renderHook(() => useEntityStore()); @@ -205,7 +206,7 @@ describe('useEntityStore', () => { }); }); - catalogApi.getEntityByName.mockResolvedValue(entity2); + catalogApi.getEntityByRef.mockResolvedValue(entity2); act(() => { result.current.requestEntities(['kind:namespace/name2']); @@ -233,6 +234,6 @@ describe('useEntityStore', () => { }); }); - expect(catalogApi.getEntityByName).toBeCalledTimes(2); + expect(catalogApi.getEntityByRef).toBeCalledTimes(2); }); }); diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts index b5ec3bef19..843b308281 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Entity, parseEntityRef } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import limiterFactory from 'p-limit'; @@ -73,9 +73,7 @@ export function useEntityStore(): { return; } - const promise = catalogClient.getEntityByName( - parseEntityRef(entityRef), - ); + const promise = catalogClient.getEntityByRef(entityRef); outstandingEntities.set(entityRef, promise); diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index f689c0a883..969ff25c00 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -93,6 +93,7 @@ describe('CatalogImportClient', () => { getEntities: jest.fn(), addLocation: jest.fn(), removeLocationById: jest.fn(), + getEntityByRef: jest.fn(), getEntityByName: jest.fn(), getLocationByRef: jest.fn(), getLocationById: jest.fn(), diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx index fc9b35136a..dd8087c51e 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx @@ -38,6 +38,7 @@ describe('', () => { const catalogApi: jest.Mocked = { getEntities: jest.fn(), addLocation: jest.fn(), + getEntityByRef: jest.fn(), getEntityByName: jest.fn(), getLocationByRef: jest.fn(), getLocationById: jest.fn(), diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index 316a0f2c06..310deabbb7 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -55,7 +55,7 @@ const mockConfigApi = { } as Partial; const mockCatalogApi = { - getEntityByName: () => Promise.resolve(mockUser), + getEntityByRef: () => Promise.resolve(mockUser), } as Partial; const mockIdentityApi = { diff --git a/plugins/catalog-react/src/hooks/useEntity.tsx b/plugins/catalog-react/src/hooks/useEntity.tsx index da3fb8f3a5..f80f858dec 100644 --- a/plugins/catalog-react/src/hooks/useEntity.tsx +++ b/plugins/catalog-react/src/hooks/useEntity.tsx @@ -115,7 +115,7 @@ export const useEntityFromUrl = (): EntityLoadingStatus => { loading, retry: refresh, } = useAsyncRetry( - () => catalogApi.getEntityByName({ kind, namespace, name }), + () => catalogApi.getEntityByRef({ kind, namespace, name }), [catalogApi, kind, namespace, name], ); diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index 27fde81cc7..e3ab09f081 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -76,7 +76,7 @@ const mockIdentityApi: Partial = { }; const mockCatalogApi: Partial = { getEntities: jest.fn().mockImplementation(async () => ({ items: entities })), - getEntityByName: async () => undefined, + getEntityByRef: async () => undefined, }; const wrapper = ({ diff --git a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx index 1bb9a649ad..4730f71a7d 100644 --- a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx @@ -30,13 +30,13 @@ import { loadCatalogOwnerRefs, useEntityOwnership } from './useEntityOwnership'; describe('useEntityOwnership', () => { type MockIdentityApi = jest.Mocked>; - type MockCatalogApi = jest.Mocked>; + type MockCatalogApi = jest.Mocked>; const mockIdentityApi: MockIdentityApi = { getBackstageIdentity: jest.fn(), }; const mockCatalogApi: MockCatalogApi = { - getEntityByName: jest.fn(), + getEntityByRef: jest.fn(), }; const identityApi = mockIdentityApi as unknown as IdentityApi; @@ -102,11 +102,11 @@ describe('useEntityOwnership', () => { describe('loadCatalogOwnerRefs', () => { it('loads the first user from the catalog', async () => { - mockCatalogApi.getEntityByName.mockResolvedValueOnce(user2Entity); + mockCatalogApi.getEntityByRef.mockResolvedValueOnce(user2Entity); await expect( loadCatalogOwnerRefs(catalogApi, ['user:default/user2']), ).resolves.toEqual(['group:default/group1']); - expect(mockCatalogApi.getEntityByName).toBeCalledWith({ + expect(mockCatalogApi.getEntityByRef).toBeCalledWith({ kind: 'user', namespace: 'default', name: 'user2', @@ -114,11 +114,11 @@ describe('useEntityOwnership', () => { }); it('gracefully handles missing user', async () => { - mockCatalogApi.getEntityByName.mockResolvedValueOnce(undefined); + mockCatalogApi.getEntityByRef.mockResolvedValueOnce(undefined); await expect( loadCatalogOwnerRefs(catalogApi, ['user:default/user2']), ).resolves.toEqual([]); - expect(mockCatalogApi.getEntityByName).toBeCalledWith({ + expect(mockCatalogApi.getEntityByRef).toBeCalledWith({ kind: 'user', namespace: 'default', name: 'user2', @@ -133,7 +133,7 @@ describe('useEntityOwnership', () => { userEntityRef: 'user:default/user1', ownershipEntityRefs: ['user:default/user1', 'group:default/group1'], }); - mockCatalogApi.getEntityByName.mockResolvedValue(undefined); + mockCatalogApi.getEntityByRef.mockResolvedValue(undefined); const { result, waitForValueToChange } = renderHook( () => useEntityOwnership(), diff --git a/plugins/catalog-react/src/hooks/useEntityOwnership.ts b/plugins/catalog-react/src/hooks/useEntityOwnership.ts index de3eca9dab..55b4e75ace 100644 --- a/plugins/catalog-react/src/hooks/useEntityOwnership.ts +++ b/plugins/catalog-react/src/hooks/useEntityOwnership.ts @@ -48,7 +48,7 @@ export async function loadCatalogOwnerRefs( const primaryUserRef = identityOwnerRefs.find(ref => ref.startsWith('user:')); if (primaryUserRef) { - const entity = await catalogApi.getEntityByName( + const entity = await catalogApi.getEntityByRef( parseEntityRef(primaryUserRef), ); if (entity) { diff --git a/plugins/catalog-react/src/hooks/useOwnUser.ts b/plugins/catalog-react/src/hooks/useOwnUser.ts index 74c44bc638..211321afdf 100644 --- a/plugins/catalog-react/src/hooks/useOwnUser.ts +++ b/plugins/catalog-react/src/hooks/useOwnUser.ts @@ -34,7 +34,12 @@ export function useOwnUser(): AsyncState { return useAsync(async () => { const identity = await identityApi.getBackstageIdentity(); - return catalogApi.getEntityByName( + // TODO(freben): Defensively parse with defaults even though getEntityByRef + // supports the string form, since some auth resolvers have been known to + // return incomplete refs (just the name part) historically. This can be + // simplified in the future to just pass the ref immediately to + // getEntityByRef. + return catalogApi.getEntityByRef( parseEntityRef(identity.userEntityRef, { defaultKind: 'User', defaultNamespace: DEFAULT_NAMESPACE, diff --git a/plugins/catalog/src/components/CatalogEntityPage/useEntityFromUrl.ts b/plugins/catalog/src/components/CatalogEntityPage/useEntityFromUrl.ts index 605305882e..f4d06b0bb2 100644 --- a/plugins/catalog/src/components/CatalogEntityPage/useEntityFromUrl.ts +++ b/plugins/catalog/src/components/CatalogEntityPage/useEntityFromUrl.ts @@ -40,7 +40,7 @@ export const useEntityFromUrl = (): EntityLoadingStatus => { loading, retry: refresh, } = useAsyncRetry( - () => catalogApi.getEntityByName({ kind, namespace, name }), + () => catalogApi.getEntityByRef({ kind, namespace, name }), [catalogApi, kind, namespace, name], ); diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx index cf2eedb14c..07bd81b287 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx @@ -17,6 +17,7 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Entity, + parseEntityRef, RELATION_MEMBER_OF, RELATION_OWNED_BY, } from '@backstage/catalog-model'; @@ -104,11 +105,11 @@ describe('DefaultCatalogPage', () => { }), getLocationByRef: () => Promise.resolve({ id: 'id', type: 'url', target: 'url' }), - getEntityByName: async entityName => { + getEntityByRef: async entityRef => { return { apiVersion: 'backstage.io/v1alpha1', kind: 'User', - metadata: { name: entityName.name }, + metadata: { name: parseEntityRef(entityRef).name }, relations: [ { type: RELATION_MEMBER_OF, diff --git a/plugins/code-coverage-backend/src/service/router.ts b/plugins/code-coverage-backend/src/service/router.ts index 4922f5c887..488103c835 100644 --- a/plugins/code-coverage-backend/src/service/router.ts +++ b/plugins/code-coverage-backend/src/service/router.ts @@ -18,7 +18,7 @@ import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import xmlparser from 'express-xml-bodyparser'; -import { CatalogClient } from '@backstage/catalog-client'; +import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; import { errorHandler, PluginDatabaseManager, @@ -33,10 +33,7 @@ import { aggregateCoverage, CoverageUtils } from './CoverageUtils'; import { Cobertura } from './converter/cobertura'; import { Jacoco } from './converter/jacoco'; import { Converter } from './converter'; -import { - getEntitySourceLocation, - parseEntityRef, -} from '@backstage/catalog-model'; +import { getEntitySourceLocation } from '@backstage/catalog-model'; export interface RouterOptions { config: Config; @@ -59,7 +56,7 @@ export const makeRouter = async ( await database.getClient(), ); const codecovUrl = await discovery.getExternalBaseUrl('code-coverage'); - const catalogApi = new CatalogClient({ discoveryApi: discovery }); + const catalogApi: CatalogApi = new CatalogClient({ discoveryApi: discovery }); const scm = ScmIntegrations.fromConfig(config); const router = Router(); @@ -77,8 +74,7 @@ export const makeRouter = async ( */ router.get('/report', async (req, res) => { const { entity } = req.query; - const entityName = parseEntityRef(entity as string); - const entityLookup = await catalogApi.getEntityByName(entityName); + const entityLookup = await catalogApi.getEntityByRef(entity as string); if (!entityLookup) { throw new NotFoundError(`No entity found matching ${entity}`); } @@ -100,8 +96,7 @@ export const makeRouter = async ( */ router.get('/history', async (req, res) => { const { entity } = req.query; - const entityName = parseEntityRef(entity as string); - const entityLookup = await catalogApi.getEntityByName(entityName); + const entityLookup = await catalogApi.getEntityByRef(entity as string); if (!entityLookup) { throw new NotFoundError(`No entity found matching ${entity}`); } @@ -119,8 +114,7 @@ export const makeRouter = async ( */ router.get('/file-content', async (req, res) => { const { entity, path } = req.query; - const entityName = parseEntityRef(entity as string); - const entityLookup = await catalogApi.getEntityByName(entityName); + const entityLookup = await catalogApi.getEntityByRef(entity as string); if (!entityLookup) { throw new NotFoundError(`No entity found matching ${entity}`); } @@ -171,8 +165,7 @@ export const makeRouter = async ( */ router.post('/report', async (req, res) => { const { entity, coverageType } = req.query; - const entityName = parseEntityRef(entity as string); - const entityLookup = await catalogApi.getEntityByName(entityName); + const entityLookup = await catalogApi.getEntityByRef(entity as string); if (!entityLookup) { throw new NotFoundError(`No entity found matching ${entity}`); } diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx index 53b225daaf..c490dd00da 100644 --- a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx @@ -28,6 +28,7 @@ describe('', () => { getLocationById: jest.fn(), removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), + getEntityByRef: jest.fn(), getEntityByName: jest.fn(), refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx index 1310d9f31e..d103541333 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx @@ -29,6 +29,7 @@ describe('', () => { getLocationById: jest.fn(), removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), + getEntityByRef: jest.fn(), getEntityByName: jest.fn(), refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx index 255db061aa..99500336cf 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx @@ -29,6 +29,7 @@ describe('', () => { getLocationById: jest.fn(), removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), + getEntityByRef: jest.fn(), getEntityByName: jest.fn(), refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), diff --git a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx index 2759afd585..f178b7de7f 100644 --- a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx +++ b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx @@ -29,6 +29,7 @@ describe('', () => { const catalogApi: jest.Mocked = { addLocation: jest.fn(), getEntities: jest.fn(), + getEntityByRef: jest.fn(), getEntityByName: jest.fn(), getLocationByRef: jest.fn(), getLocationById: jest.fn(), diff --git a/plugins/jenkins-backend/README.md b/plugins/jenkins-backend/README.md index 1413a45136..443cb33c05 100644 --- a/plugins/jenkins-backend/README.md +++ b/plugins/jenkins-backend/README.md @@ -166,7 +166,7 @@ class AcmeJenkinsInfoProvider implements JenkinsInfoProvider { const PAAS_ANNOTATION = 'acme.example.com/paas-project-name'; // lookup pass-project-name from entity annotation - const entity = await this.catalog.getEntityByName(opt.entityRef); + const entity = await this.catalog.getEntityByRef(opt.entityRef); if (!entity) { throw new Error( `Couldn't find entity with name: ${stringifyEntityRef(opt.entityRef)}`, diff --git a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts index 33079dfc8f..a3a5ba069d 100644 --- a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts +++ b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts @@ -160,7 +160,7 @@ describe('JenkinsConfig', () => { describe('DefaultJenkinsInfoProvider', () => { const mockCatalog: jest.Mocked = { - getEntityByName: jest.fn(), + getEntityByRef: jest.fn(), } as any as jest.Mocked; const entityRef: CompoundEntityRef = { @@ -171,7 +171,7 @@ describe('DefaultJenkinsInfoProvider', () => { function configureProvider(configData: any, entityData: any) { const config = new ConfigReader(configData); - mockCatalog.getEntityByName.mockReturnValueOnce( + mockCatalog.getEntityByRef.mockReturnValueOnce( Promise.resolve(entityData as Entity), ); @@ -185,7 +185,7 @@ describe('DefaultJenkinsInfoProvider', () => { const provider = configureProvider({ jenkins: {} }, undefined); await expect(provider.getInstance({ entityRef })).rejects.toThrowError(); - expect(mockCatalog.getEntityByName).toBeCalledWith(entityRef); + expect(mockCatalog.getEntityByRef).toBeCalledWith(entityRef); }); it('Reads simple config and annotation', async () => { @@ -207,7 +207,7 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByName).toBeCalledWith(entityRef); + expect(mockCatalog.getEntityByRef).toBeCalledWith(entityRef); expect(info).toStrictEqual({ baseUrl: 'https://jenkins.example.com', crumbIssuer: undefined, @@ -243,7 +243,7 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByName).toBeCalledWith(entityRef); + expect(mockCatalog.getEntityByRef).toBeCalledWith(entityRef); expect(info).toMatchObject({ baseUrl: 'https://jenkins.example.com', jobFullName: 'teamA/artistLookup-build', @@ -280,7 +280,7 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByName).toBeCalledWith(entityRef); + expect(mockCatalog.getEntityByRef).toBeCalledWith(entityRef); expect(info).toMatchObject({ baseUrl: 'https://jenkins.example.com', jobFullName: 'teamA/artistLookup-build', @@ -317,7 +317,7 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByName).toBeCalledWith(entityRef); + expect(mockCatalog.getEntityByRef).toBeCalledWith(entityRef); expect(info).toMatchObject({ baseUrl: 'https://jenkins-other.example.com', jobFullName: 'teamA/artistLookup-build', @@ -343,7 +343,7 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByName).toBeCalledWith(entityRef); + expect(mockCatalog.getEntityByRef).toBeCalledWith(entityRef); expect(info).toMatchObject({ baseUrl: 'https://jenkins.example.com', jobFullName: 'teamA/artistLookup-build', @@ -369,7 +369,7 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByName).toBeCalledWith(entityRef); + expect(mockCatalog.getEntityByRef).toBeCalledWith(entityRef); expect(info).toMatchObject({ baseUrl: 'https://jenkins.example.com', jobFullName: 'teamA/artistLookup-build', @@ -400,7 +400,7 @@ describe('DefaultJenkinsInfoProvider', () => { ); const info: JenkinsInfo = await provider.getInstance({ entityRef }); - expect(mockCatalog.getEntityByName).toBeCalledWith(entityRef); + expect(mockCatalog.getEntityByRef).toBeCalledWith(entityRef); expect(info).toMatchObject({ baseUrl: 'https://jenkins-other.example.com', jobFullName: 'teamA/artistLookup-build', diff --git a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts index 528a1d038f..a88dbf965e 100644 --- a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts +++ b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts @@ -186,7 +186,7 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { jobFullName?: string; }): Promise { // load entity - const entity = await this.catalog.getEntityByName(opt.entityRef); + const entity = await this.catalog.getEntityByRef(opt.entityRef); if (!entity) { throw new Error( `Couldn't find entity with name: ${stringifyEntityRef(opt.entityRef)}`, diff --git a/plugins/rollbar/src/hooks/useCatalogEntity.ts b/plugins/rollbar/src/hooks/useCatalogEntity.ts index 468d0e8c50..21d22dd13c 100644 --- a/plugins/rollbar/src/hooks/useCatalogEntity.ts +++ b/plugins/rollbar/src/hooks/useCatalogEntity.ts @@ -30,7 +30,7 @@ export function useCatalogEntity() { error, loading, } = useAsync( - () => catalogApi.getEntityByName({ kind: 'Component', namespace, name }), + () => catalogApi.getEntityByRef({ kind: 'Component', namespace, name }), [catalogApi, namespace, name], ); diff --git a/plugins/scaffolder-backend/src/service/helpers.ts b/plugins/scaffolder-backend/src/service/helpers.ts index e3aae0951b..a7148dac40 100644 --- a/plugins/scaffolder-backend/src/service/helpers.ts +++ b/plugins/scaffolder-backend/src/service/helpers.ts @@ -22,6 +22,7 @@ import { ANNOTATION_SOURCE_LOCATION, CompoundEntityRef, DEFAULT_NAMESPACE, + stringifyEntityRef, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { assertError, InputError, NotFoundError } from '@backstage/errors'; @@ -106,9 +107,11 @@ export async function findTemplate(options: { throw new InputError(`Invalid kind, only 'Template' kind is supported`); } - const template = await catalogApi.getEntityByName(entityRef, { token }); + const template = await catalogApi.getEntityByRef(entityRef, { token }); if (!template) { - throw new NotFoundError(`Template ${entityRef} not found`); + throw new NotFoundError( + `Template ${stringifyEntityRef(entityRef)} not found`, + ); } return template as TemplateEntityV1beta3 | TemplateEntityV1beta2; diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index fa1b649231..4041e41cf8 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -53,7 +53,7 @@ import { stringifyEntityRef } from '@backstage/catalog-model'; const createCatalogClient = (template: any) => ({ - getEntityByName: async () => template, + getEntityByRef: async () => template, } as unknown as CatalogApi); function createDatabase(): PluginDatabaseManager { diff --git a/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts b/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts index a1542df351..19684486dc 100644 --- a/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts +++ b/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts @@ -20,7 +20,7 @@ import { CompoundEntityRef } from '@backstage/catalog-model'; describe('CachedEntityLoader', () => { const catalog: jest.Mocked = { - getEntityByName: jest.fn(), + getEntityByRef: jest.fn(), } as any; const cache: jest.Mocked = { @@ -53,7 +53,7 @@ describe('CachedEntityLoader', () => { it('writes entities to cache', async () => { cache.get.mockResolvedValue(undefined); - catalog.getEntityByName.mockResolvedValue(entity); + catalog.getEntityByRef.mockResolvedValue(entity); const result = await loader.load(entityName, token); @@ -71,12 +71,12 @@ describe('CachedEntityLoader', () => { const result = await loader.load(entityName, token); expect(result).toEqual(entity); - expect(catalog.getEntityByName).not.toBeCalled(); + expect(catalog.getEntityByRef).not.toBeCalled(); }); it('does not cache missing entites', async () => { cache.get.mockResolvedValue(undefined); - catalog.getEntityByName.mockResolvedValue(undefined); + catalog.getEntityByRef.mockResolvedValue(undefined); const result = await loader.load(entityName, token); @@ -86,7 +86,7 @@ describe('CachedEntityLoader', () => { it('uses entity ref as cache key for anonymous users', async () => { cache.get.mockResolvedValue(undefined); - catalog.getEntityByName.mockResolvedValue(entity); + catalog.getEntityByRef.mockResolvedValue(entity); const result = await loader.load(entityName, undefined); @@ -103,7 +103,7 @@ describe('CachedEntityLoader', () => { setTimeout(() => resolve(undefined), 10000); }), ); - catalog.getEntityByName.mockResolvedValue(entity); + catalog.getEntityByRef.mockResolvedValue(entity); const result = await loader.load(entityName, token); diff --git a/plugins/techdocs-backend/src/service/CachedEntityLoader.ts b/plugins/techdocs-backend/src/service/CachedEntityLoader.ts index cd771f7fff..424d9b9541 100644 --- a/plugins/techdocs-backend/src/service/CachedEntityLoader.ts +++ b/plugins/techdocs-backend/src/service/CachedEntityLoader.ts @@ -37,17 +37,17 @@ export class CachedEntityLoader { } async load( - entityName: CompoundEntityRef, + entityRef: CompoundEntityRef, token: string | undefined, ): Promise { - const cacheKey = this.getCacheKey(entityName, token); + const cacheKey = this.getCacheKey(entityRef, token); let result = await this.getFromCache(cacheKey); if (result) { return result; } - result = await this.catalog.getEntityByName(entityName, { token }); + result = await this.catalog.getEntityByRef(entityRef, { token }); if (result) { this.cache.set(cacheKey, result, { ttl: 5000 }); diff --git a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx index 33f5e0a08a..14fdb88395 100644 --- a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx +++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx @@ -45,7 +45,7 @@ jest.mock('@backstage/plugin-catalog-react', () => { }); const mockCatalogApi = { - getEntityByName: () => Promise.resolve(), + getEntityByRef: () => Promise.resolve(), getEntities: async () => ({ items: [ { diff --git a/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx index a8fd472f7c..16d8d48bc3 100644 --- a/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx +++ b/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx @@ -33,7 +33,7 @@ jest.mock('@backstage/plugin-catalog-react', () => { }); const mockCatalogApi = { - getEntityByName: jest.fn(), + getEntityByRef: jest.fn(), getEntities: async () => ({ items: [ { diff --git a/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx b/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx index e9c3a55b15..2088dbf4fc 100644 --- a/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx +++ b/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx @@ -31,7 +31,7 @@ jest.mock('@backstage/plugin-catalog-react', () => { }); const mockCatalogApi = { - getEntityByName: jest.fn(), + getEntityByRef: jest.fn(), getEntities: async () => ({ items: [ { diff --git a/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx b/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx index c8149f1ff2..d2f0266af6 100644 --- a/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx +++ b/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx @@ -232,7 +232,12 @@ function useOwnUser(): AsyncState { return useAsync(async () => { const identity = await identityApi.getBackstageIdentity(); - return catalogApi.getEntityByName( + // TODO(freben): Defensively parse with defaults even though getEntityByRef + // supports the string form, since some auth resolvers have been known to + // return incomplete refs (just the name part) historically. This can be + // simplified in the future to just pass the ref immediately to + // getEntityByRef. + return catalogApi.getEntityByRef( parseEntityRef(identity.userEntityRef, { defaultKind: 'User', defaultNamespace: DEFAULT_NAMESPACE, diff --git a/plugins/todo-backend/src/service/TodoReaderService.test.ts b/plugins/todo-backend/src/service/TodoReaderService.test.ts index 985687c6bb..943462bd63 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.test.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.test.ts @@ -44,6 +44,7 @@ function mockCatalogClient(entity?: Entity): jest.Mocked { const mock = { addLocation: jest.fn(), getEntities: jest.fn(), + getEntityByRef: jest.fn(), getEntityByName: jest.fn(), getLocationByRef: jest.fn(), getLocationById: jest.fn(), @@ -54,7 +55,7 @@ function mockCatalogClient(entity?: Entity): jest.Mocked { getEntityFacets: jest.fn(), }; if (entity) { - mock.getEntityByName.mockReturnValue(entity); + mock.getEntityByRef.mockReturnValue(entity); } return mock; } @@ -93,7 +94,7 @@ describe('TodoReaderService', () => { offset: 0, limit: 10, }); - expect(catalogClient.getEntityByName).toHaveBeenCalledWith(entityName, { + expect(catalogClient.getEntityByRef).toHaveBeenCalledWith(entityName, { token: undefined, }); }); @@ -304,7 +305,7 @@ describe('TodoReaderService', () => { message: 'Entity not found, component:default/my-component', }), ); - expect(catalogClient.getEntityByName).toHaveBeenCalledWith(entityName, { + expect(catalogClient.getEntityByRef).toHaveBeenCalledWith(entityName, { token: undefined, }); }); diff --git a/plugins/todo-backend/src/service/TodoReaderService.ts b/plugins/todo-backend/src/service/TodoReaderService.ts index 86f53057da..4397f34045 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.ts @@ -66,7 +66,7 @@ export class TodoReaderService implements TodoService { throw new InputError('Entity filter is required to list TODOs'); } const token = options?.token; - const entity = await this.catalogClient.getEntityByName(req.entity, { + const entity = await this.catalogClient.getEntityByRef(req.entity, { token, }); if (!entity) { From 2255bfd1c40e155184646d62ebc56673ba8269f4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Mar 2022 13:37:33 +0100 Subject: [PATCH 162/353] Apply suggestions from code review Signed-off-by: Patrik Oldsberg --- .changeset/small-hornets-dress.md | 6 +++++- plugins/catalog-common/src/search/CatalogEntityDocument.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.changeset/small-hornets-dress.md b/.changeset/small-hornets-dress.md index 6d6817977d..2bb73b1ba3 100644 --- a/.changeset/small-hornets-dress.md +++ b/.changeset/small-hornets-dress.md @@ -3,4 +3,8 @@ '@backstage/plugin-catalog-common': patch --- -Moved the `CatalogEntityDocument` to `@backstage/plugin-catalog-common` and deprecated the export from `@backstage/plugin-catalog-backend` +Moved the `CatalogEntityDocument` to `@backstage/plugin-catalog-common` and deprecated the export from `@backstage/plugin-catalog-backend`. + +A new `type` field has also been added to `CatalogEntityDocument` as a replacement for `componentType`, which is now deprecated. Both fields are still present and should be set to the same value in order to avoid issues with indexing. + +Any search customizations need to be updated to use this new `type` field instead, including any custom frontend filters, custom frontend result components, custom search decorators, or non-default Catalog collator implementations. diff --git a/plugins/catalog-common/src/search/CatalogEntityDocument.ts b/plugins/catalog-common/src/search/CatalogEntityDocument.ts index 005bc79c92..834dde2568 100644 --- a/plugins/catalog-common/src/search/CatalogEntityDocument.ts +++ b/plugins/catalog-common/src/search/CatalogEntityDocument.ts @@ -21,7 +21,7 @@ import { IndexableDocument } from '@backstage/search-common'; * @public */ export interface CatalogEntityDocument extends IndexableDocument { - /** @deprecated use `type` as well, as `componentType` will be removed after a few releases but we dont want to break indexing */ + /** @deprecated `componentType` is being renamed to `type`. During the transition both of these fields should be set to the same value, in order to avoid issues with indexing. */ componentType: string; type: string; namespace: string; From 0874762ed0256ca2e7f6f003851005cc70622980 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Thu, 3 Mar 2022 13:43:09 +0100 Subject: [PATCH 163/353] add hack week notice Signed-off-by: Johan Haals --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1078631926..e78800ef84 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ # [Backstage](https://backstage.io) +_During March 7 to March 11 the maintainers will be taking part in Spotify's annual hack week. Development will continue as usual, but expect a slower pace for discussions and PR reviews. Why not take this opportunity to [build a plugin](https://backstage.io/docs/plugins/)?_ + [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![CNCF Status](https://img.shields.io/badge/cncf%20status-sandbox-blue.svg)](https://www.cncf.io/projects) [![Main CI Build](https://github.com/backstage/backstage/workflows/Main%20Master%20Build/badge.svg)](https://github.com/backstage/backstage/actions?query=workflow%3A%22Main+Master+Build%22) From 0513a720328553fb37b57d96bd489c62b3866dcc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Mar 2022 13:50:02 +0100 Subject: [PATCH 164/353] Revert "scripts/prepare-release: update to detect patch versions from patch branches" Signed-off-by: Patrik Oldsberg --- docs/publishing.md | 9 ++ scripts/prepare-release.js | 174 ++++++++----------------------------- 2 files changed, 46 insertions(+), 137 deletions(-) diff --git a/docs/publishing.md b/docs/publishing.md index a0d67b7d24..fbbef93493 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -67,3 +67,12 @@ process is used to release an emergency fix as version `6.5.1` in the patch rele - [ ] The fix, which you can likely cherry-pick from your patch branch: `git cherry-pick origin/patch/v1.18.0^` - [ ] An updated `CHANGELOG.md` of all patched packages from the tip of the patch branch, `git checkout origin/patch/v1.18.0 -- {packages,plugins}/*/CHANGELOG.md`. - [ ] A changeset with the message "Applied the fix from version `6.5.1` of this package, which is part of the `v1.18.1` release of Backstage." + - [ ] An entry in `.changeset/patched.json` that sets the current release version to `6.5.1`: + + ```json + { + "currentReleaseVersion": { + "@backstage/plugin-foo": "6.5.1" + } + } + ``` diff --git a/scripts/prepare-release.js b/scripts/prepare-release.js index 5cc888fd66..be82e19344 100755 --- a/scripts/prepare-release.js +++ b/scripts/prepare-release.js @@ -28,10 +28,6 @@ const execFile = promisify(execFileCb); // All of these are considered to be main-line release branches const MAIN_BRANCHES = ['master', 'origin/master', 'changeset-release/master']; -// This prefix is used for patch branches, followed by the release version WITH a 'v' prefix -// For example, `patch/v1.2.0` -const PATCH_BRANCH_PREFIX = 'patch/'; - const DEPENDENCY_TYPES = [ 'dependencies', 'devDependencies', @@ -39,84 +35,25 @@ const DEPENDENCY_TYPES = [ 'peerDependencies', ]; -/** - * Returns the most recent release version on the main branch that is not a pre-release. - */ -async function getPreviousReleaseVersion(repo) { - // TODO(Rugvip): Figure out which field to sort by to avoid manual sort after - const { stdout: tagsStr } = await execFile( - 'git', - ['tag', '--list', 'v*', '--merged=HEAD'], - { shell: true, cwd: repo.root.dir }, - ); - const tags = tagsStr.trim().split(/\r\n|\n/); - const [latestTag] = semver.rsort(tags).filter(t => !semver.prerelease(t)); - return latestTag; -} - -/** - * Finds the tip of the patch branch of a given release version. - * Returns undefined if no patch branch exists. - */ -async function findTipOfPatchBranch(repo, release) { - try { - await execFile('git', ['fetch', 'origin', PATCH_BRANCH_PREFIX + release], { - shell: true, - cwd: repo.root.dir, - }); - } catch (error) { - if (error.stderr?.match(/fatal: couldn't find remote ref/i)) { - return undefined; - } - throw error; - } - const { stdout: refStr } = await execFile('git', ['rev-parse', 'FETCH_HEAD']); - return refStr.trim(); -} - -/** - * Returns a map of packages to their versions for any package version - * in that does not match the current version in the working directory. - */ -async function detectPatchVersionsForRef(repo, ref) { - const patchVersions = new Map(); - - for (const pkg of repo.packages) { - const pkgJsonPath = path.join( - path.relative(repo.root.dir, pkg.dir), - 'package.json', - ); - const { stdout: pkgJsonStr } = await execFile('git', [ - 'show', - `${ref}:${pkgJsonPath}`, - ]); - if (pkgJsonStr) { - const releasePkgJson = JSON.parse(pkgJsonStr); - const pkgJson = pkg.packageJson; - if (releasePkgJson.name !== pkgJson.name) { - throw new Error( - `Mismatched package name at ${pkg.dir}, ${releasePkgJson.name} !== ${pkgJson.name}`, - ); - } - if (releasePkgJson.version !== pkgJson.version) { - patchVersions.set(pkgJson.name, releasePkgJson.version); - } - } - } - - return patchVersions; -} - /** * Bumps up the versions of packages to account for * the base versions that are set in .changeset/patched.json. * This may be needed when we have made emergency releases. */ -async function applyPatchVersions(repo, patchVersions) { +async function updatePatchVersions() { + const patchedJsonPath = path.resolve('.changeset', 'patched.json'); + const { currentReleaseVersion } = await fs.readJson(patchedJsonPath); + if (Object.keys(currentReleaseVersion).length === 0) { + console.log('No currentReleaseVersion overrides found, skipping.'); + return; + } + + const { packages } = await getPackages(path.resolve('.')); + const pendingVersionBumps = new Map(); - for (const [name, version] of patchVersions) { - const pkg = repo.packages.find(p => p.packageJson.name === name); + for (const [name, version] of Object.entries(currentReleaseVersion)) { + const pkg = packages.find(p => p.packageJson.name === name); if (!pkg) { throw new Error(`Package ${name} not found`); } @@ -144,7 +81,7 @@ async function applyPatchVersions(repo, patchVersions) { }); } - for (const { dir, packageJson } of [repo.root, ...repo.packages]) { + for (const { dir, packageJson } of packages) { let hasChanges = false; if (pendingVersionBumps.has(packageJson.name)) { @@ -180,44 +117,20 @@ async function applyPatchVersions(repo, patchVersions) { }); } } -} -/** - * Detects any patched packages version since the most recent release on - * the main branch, and then bumps all packages in the repo accordingly. - */ -async function updatePackageVersions(repo) { - const previousRelease = await getPreviousReleaseVersion(repo); - console.log(`Found release version: ${previousRelease}`); - - const patchRef = await findTipOfPatchBranch(repo, previousRelease); - if (patchRef) { - console.log(`Tip of the patch branch: ${patchRef}`); - - const patchVersions = await detectPatchVersionsForRef(repo, patchRef); - if (patchVersions.size > 0) { - console.log( - `Found ${patchVersions.size} packages that were patched since the last release`, - ); - for (const [name, version] of patchVersions) { - console.log(` ${name}: ${version}`); - } - - await applyPatchVersions(repo, patchVersions); - } else { - console.log('No packages were patched since the last release'); - } - } else { - console.log('No patch branch found'); - } + await fs.writeJSON( + patchedJsonPath, + { currentReleaseVersion: {} }, + { spaces: 2, encoding: 'utf8' }, + ); } /** * Returns the mode and tag that is currently set * in the .changeset/pre.json file */ -async function getPreInfo(repo) { - const pre = path.join(repo.root.dir, '.changeset', 'pre.json'); +async function getPreInfo(rootPath) { + const pre = path.join(rootPath, '.changeset', 'pre.json'); if (!(await fs.pathExists(pre))) { return { mode: undefined, tag: undefined }; } @@ -226,30 +139,26 @@ async function getPreInfo(repo) { return { mode, tag }; } -/** - * Returns the name of the current git branch - */ -async function getCurrentBranch(repo) { - const { stdout } = await execFile( - 'git', - ['rev-parse', '--abbrev-ref', 'HEAD'], - { cwd: repo.root.dir, shell: true }, - ); - return stdout.trim(); -} - /** * Bumps the release version in the root package.json. * * This takes into account whether we're in pre-release mode or on a patch branch. */ -async function updateBackstageReleaseVersion(repo, type) { - const { mode: preMode, tag: preTag } = await getPreInfo(repo); +async function updateBackstageReleaseVersion() { + const rootPath = path.resolve(__dirname, '..'); + const branchName = await execFile( + 'git', + ['rev-parse', '--abbrev-ref', 'HEAD'], + { shell: true }, + ).then(({ stdout }) => stdout.trim()); + const { mode: preMode, tag: preTag } = await getPreInfo(rootPath); - const { version: currentVersion } = repo.root.packageJson; + const packagePath = path.join(rootPath, 'package.json'); + const package = await fs.readJson(packagePath); + const { version: currentVersion } = package; let nextVersion; - if (type === 'minor') { + if (MAIN_BRANCHES.includes(branchName)) { if (preMode === 'pre') { if (semver.prerelease(currentVersion)) { nextVersion = semver.inc(currentVersion, 'pre', preTag); @@ -261,7 +170,7 @@ async function updateBackstageReleaseVersion(repo, type) { } else { nextVersion = semver.inc(currentVersion, 'minor'); } - } else if (type === 'patch') { + } else { if (preMode) { throw new Error(`Unexpected pre mode ${preMode} on branch ${branchName}`); } @@ -269,9 +178,9 @@ async function updateBackstageReleaseVersion(repo, type) { } await fs.writeJson( - path.join(repo.root.dir, 'package.json'), + packagePath, { - ...repo.root.packageJson, + ...package, version: nextVersion, }, { spaces: 2, encoding: 'utf8' }, @@ -279,17 +188,8 @@ async function updateBackstageReleaseVersion(repo, type) { } async function main() { - const repo = await getPackages(__dirname); - const branchName = await getCurrentBranch(repo); - const isMainBranch = MAIN_BRANCHES.includes(branchName); - - console.log(`Current branch: ${branchName}`); - if (isMainBranch) { - console.log('Main release, updating package versions'); - await updatePackageVersions(repo); - } - - await updateBackstageReleaseVersion(repo, isMainBranch ? 'minor' : 'patch'); + await updatePatchVersions(); + await updateBackstageReleaseVersion(); } main().catch(error => { From 48997f9bcffb2c5c07172eb6b594997a446dec25 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 14:00:23 +0100 Subject: [PATCH 165/353] fix up the exports to keep the DEPRECATION Signed-off-by: blam --- .changeset/small-hornets-dress.md | 2 +- plugins/catalog-backend/api-report.md | 7 ++++--- .../src/search/DefaultCatalogCollatorFactory.ts | 5 ----- plugins/catalog-backend/src/search/index.ts | 9 +++++++-- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.changeset/small-hornets-dress.md b/.changeset/small-hornets-dress.md index 2bb73b1ba3..474c3011bd 100644 --- a/.changeset/small-hornets-dress.md +++ b/.changeset/small-hornets-dress.md @@ -3,7 +3,7 @@ '@backstage/plugin-catalog-common': patch --- -Moved the `CatalogEntityDocument` to `@backstage/plugin-catalog-common` and deprecated the export from `@backstage/plugin-catalog-backend`. +**DEPRECATION**: Moved the `CatalogEntityDocument` to `@backstage/plugin-catalog-common` and deprecated the export from `@backstage/plugin-catalog-backend`. A new `type` field has also been added to `CatalogEntityDocument` as a replacement for `componentType`, which is now deprecated. Both fields are still present and should be set to the same value in order to avoid issues with indexing. diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 9dcb2db752..dfe9a8943e 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -7,7 +7,7 @@ import { BitbucketIntegration } from '@backstage/integration'; import { CatalogApi } from '@backstage/catalog-client'; -import { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; +import { CatalogEntityDocument as CatalogEntityDocument_2 } from '@backstage/plugin-catalog-common'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { ConditionalPolicyDecision } from '@backstage/plugin-permission-node'; import { Conditions } from '@backstage/plugin-permission-node'; @@ -248,7 +248,8 @@ export const catalogConditions: Conditions<{ >; }>; -export { CatalogEntityDocument }; +// @public @deprecated (undocumented) +export type CatalogEntityDocument = CatalogEntityDocument_2; // @public (undocumented) export type CatalogEnvironment = { @@ -423,7 +424,7 @@ export class DefaultCatalogCollator { // (undocumented) protected discovery: PluginEndpointDiscovery; // (undocumented) - execute(): Promise; + execute(): Promise; // (undocumented) protected filter?: GetEntitiesRequest['filter']; // (undocumented) diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts index ca6395b60a..440a8c8ebb 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts @@ -36,11 +36,6 @@ import { } from '@backstage/plugin-catalog-common'; import { Readable } from 'stream'; -/** - * @deprecated import from `@backstage/plugin-catalog-common` instead - */ -export type { CatalogEntityDocument }; - /** @public */ export type DefaultCatalogCollatorFactoryOptions = { discovery: PluginEndpointDiscovery; diff --git a/plugins/catalog-backend/src/search/index.ts b/plugins/catalog-backend/src/search/index.ts index bd54e33e59..eb5b1e7c22 100644 --- a/plugins/catalog-backend/src/search/index.ts +++ b/plugins/catalog-backend/src/search/index.ts @@ -17,8 +17,13 @@ export { DefaultCatalogCollatorFactory } from './DefaultCatalogCollatorFactory'; export type { DefaultCatalogCollatorFactoryOptions } from './DefaultCatalogCollatorFactory'; -/** @public @deprecated use the export from `plugin-catalog-common` instead */ -export type { CatalogEntityDocument } from '@backstage/plugin-catalog-common'; +import { CatalogEntityDocument as CatalogEntityDocumentType } from '@backstage/plugin-catalog-common'; + +/** + * @deprecated import from `@backstage/plugin-catalog-common` instead + * @public + */ +export type CatalogEntityDocument = CatalogEntityDocumentType; /** * todo(backstage/techdocs-core): stop exporting this in a future release. From b032cba7e6d0ee83dc65f948557c6711e052aa7d Mon Sep 17 00:00:00 2001 From: Bruno Viana Date: Thu, 3 Mar 2022 11:04:09 -0300 Subject: [PATCH 166/353] add Stilingue in adopters list Signed-off-by: Bruno Viana --- ADOPTERS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ADOPTERS.md b/ADOPTERS.md index eb83726b8d..107917f06e 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -99,4 +99,5 @@ _If you're using Backstage in your organization, please try to add your company | [Procore](https://www.procore.com/jobs/engineering) | [@shayon](https://github.com/Shayon), [@jamesdabbs-procore](https://github.com/jamesdabbs-procore) | Developer portal - centralized software catalog and templates to enable self serve and reduce cognitive load. | | [Bradesco](https://banco.bradesco/html/classic/sobre/index.shtm) | [@danilosoarescardoso](https://github.com/danilosoarescardoso) | Centralized developer portal with software catalog, software templates, techdocs and some plugins from community and by our own. | | [SeatGeek](https://seatgeek.com) | [@zhammer](https://github.com/zhammer) | Software catalog and documentation platform | -| [Grupo Boticário](https://www.grupoboticario.com.br/) | [@chicoribas](https://github.com/chicoribas), [@fndiaz](https://github.com/fndiaz), [@digogid](https://github.com/digogid), [@manumbs](https://github.com/manumbs), [@haooliveira84](https://github.com/haooliveira84), [@Rhullyam](https://github.com/Rhullyam) | Centralized developer portal with catalog, documentation, and software templates to build a ready to go application, including pipelines (CI/CD) and cloud services provisioning | \ No newline at end of file +| [Grupo Boticário](https://www.grupoboticario.com.br/) | [@chicoribas](https://github.com/chicoribas), [@fndiaz](https://github.com/fndiaz), [@digogid](https://github.com/digogid), [@manumbs](https://github.com/manumbs), [@haooliveira84](https://github.com/haooliveira84), [@Rhullyam](https://github.com/Rhullyam) | Centralized developer portal with catalog, documentation, and software templates to build a ready to go application, including pipelines (CI/CD) and cloud services provisioning | +| [Stilingue](https://www.stilingue.com.br/) | [@stilingue-inteligencia-artificial](https://github.com/Stilingue-IA), [@bbviana](https://github.com/bbviana) | Developer portal, services catalog and centralization of metrics from Grafana, Sentry and GCP. Furthermore, centralization of documentation and infra details like DNS, Network services, SSL and so on. | From 10c7fc4c3e40a8255f3ba7c4be782adc4c937794 Mon Sep 17 00:00:00 2001 From: Rob Cresswell Date: Thu, 3 Mar 2022 14:35:22 +0000 Subject: [PATCH 167/353] chore: Add Snyk to ADOPTERS.md o/ This patch adds Snyk as an adopter; we're using Backstage via Roadie's SaaS platform Signed-off-by: Rob Cresswell --- ADOPTERS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ADOPTERS.md b/ADOPTERS.md index eb83726b8d..27078b4ee9 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -99,4 +99,5 @@ _If you're using Backstage in your organization, please try to add your company | [Procore](https://www.procore.com/jobs/engineering) | [@shayon](https://github.com/Shayon), [@jamesdabbs-procore](https://github.com/jamesdabbs-procore) | Developer portal - centralized software catalog and templates to enable self serve and reduce cognitive load. | | [Bradesco](https://banco.bradesco/html/classic/sobre/index.shtm) | [@danilosoarescardoso](https://github.com/danilosoarescardoso) | Centralized developer portal with software catalog, software templates, techdocs and some plugins from community and by our own. | | [SeatGeek](https://seatgeek.com) | [@zhammer](https://github.com/zhammer) | Software catalog and documentation platform | -| [Grupo Boticário](https://www.grupoboticario.com.br/) | [@chicoribas](https://github.com/chicoribas), [@fndiaz](https://github.com/fndiaz), [@digogid](https://github.com/digogid), [@manumbs](https://github.com/manumbs), [@haooliveira84](https://github.com/haooliveira84), [@Rhullyam](https://github.com/Rhullyam) | Centralized developer portal with catalog, documentation, and software templates to build a ready to go application, including pipelines (CI/CD) and cloud services provisioning | \ No newline at end of file +| [Grupo Boticário](https://www.grupoboticario.com.br/) | [@chicoribas](https://github.com/chicoribas), [@fndiaz](https://github.com/fndiaz), [@digogid](https://github.com/digogid), [@manumbs](https://github.com/manumbs), [@haooliveira84](https://github.com/haooliveira84), [@Rhullyam](https://github.com/Rhullyam) | Centralized developer portal with catalog, documentation, and software templates to build a ready to go application, including pipelines (CI/CD) and cloud services provisioning | +| [Snyk](https://snyk.io/) | [@robcresswell](https://github.com/robcresswell) | Developer portal for software discovery, API documentation and templating | From 5a74ea82bbbc65a522cd1dd966d532c2a500eb3b Mon Sep 17 00:00:00 2001 From: Hasan Oezdemir <21654050+nodify-at@users.noreply.github.com> Date: Thu, 3 Mar 2022 15:53:54 +0100 Subject: [PATCH 168/353] (feature): improve the change set and remove typings Signed-off-by: Hasan Oezdemir <21654050+nodify-at@users.noreply.github.com> --- .changeset/gold-garlics-sing.md | 2 +- .changeset/orange-cobras-shave.md | 2 +- plugins/jenkins-backend/api-report.md | 4 ++-- plugins/jenkins-backend/package.json | 2 +- plugins/jenkins-backend/src/service/router.ts | 6 +++--- plugins/jenkins-common/package.json | 2 +- plugins/jenkins/package.json | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.changeset/gold-garlics-sing.md b/.changeset/gold-garlics-sing.md index 012d6f92fb..1162cca2d4 100644 --- a/.changeset/gold-garlics-sing.md +++ b/.changeset/gold-garlics-sing.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-jenkins-common': major +'@backstage/plugin-jenkins-common': minor --- Add a new common plugin for Jenkins which provides shared isomorphic code for the Jenkins plugin. diff --git a/.changeset/orange-cobras-shave.md b/.changeset/orange-cobras-shave.md index 9f54f6d48b..ad76443c8c 100644 --- a/.changeset/orange-cobras-shave.md +++ b/.changeset/orange-cobras-shave.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-jenkins-backend': minor +'@backstage/plugin-jenkins-backend': patch --- Jenkins plugin supports permissions now. We have added a new permission, so you can manage the permission for the users. diff --git a/plugins/jenkins-backend/api-report.md b/plugins/jenkins-backend/api-report.md index 7af3968897..23ee1d11e9 100644 --- a/plugins/jenkins-backend/api-report.md +++ b/plugins/jenkins-backend/api-report.md @@ -7,8 +7,8 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { EntityName } from '@backstage/catalog-model'; import express from 'express'; -import type { Logger as Logger_2 } from 'winston'; -import type { PermissionAuthorizer } from '@backstage/plugin-permission-common'; +import { Logger as Logger_2 } from 'winston'; +import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index 64278618af..14005656ab 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -31,7 +31,7 @@ "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", "@backstage/plugin-auth-node": "^0.1.3", - "@backstage/plugin-jenkins-common": "^0.1.0", + "@backstage/plugin-jenkins-common": "^0.0.0", "@backstage/plugin-permission-common": "^0.5.1", "@types/express": "^4.17.6", "express": "^4.17.1", diff --git a/plugins/jenkins-backend/src/service/router.ts b/plugins/jenkins-backend/src/service/router.ts index dcef2c2e0a..08712215e4 100644 --- a/plugins/jenkins-backend/src/service/router.ts +++ b/plugins/jenkins-backend/src/service/router.ts @@ -17,10 +17,10 @@ import { errorHandler } from '@backstage/backend-common'; import express from 'express'; import Router from 'express-promise-router'; -import type { Logger } from 'winston'; -import type { JenkinsInfoProvider } from './jenkinsInfoProvider'; +import { Logger } from 'winston'; +import { JenkinsInfoProvider } from './jenkinsInfoProvider'; import { JenkinsApiImpl } from './jenkinsApi'; -import type { PermissionAuthorizer } from '@backstage/plugin-permission-common'; +import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; import { stringifyEntityRef } from '@backstage/catalog-model'; diff --git a/plugins/jenkins-common/package.json b/plugins/jenkins-common/package.json index 08f12960bc..41a63298ef 100644 --- a/plugins/jenkins-common/package.json +++ b/plugins/jenkins-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins-common", - "version": "0.1.0", + "version": "0.0.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index e9ea4e5b5c..74e839f7d1 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -40,7 +40,7 @@ "@backstage/core-plugin-api": "^0.7.0", "@backstage/errors": "^0.2.2", "@backstage/plugin-catalog-react": "^0.7.0", - "@backstage/plugin-jenkins-common": "^0.1.0", + "@backstage/plugin-jenkins-common": "^0.0.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", From 4239b8dba6c490bc7cc97722e43c9699e3d849bf Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Thu, 3 Mar 2022 15:15:58 +0000 Subject: [PATCH 169/353] Don't check exceptions Signed-off-by: Karan Shah --- .../EntityAirbrakeWidget.test.tsx | 2 +- .../EntityAirbrakeWidget.tsx | 26 +++---------------- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx index 6211500b10..64f1f11223 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx @@ -34,7 +34,7 @@ import { errorApiRef } from '@backstage/core-plugin-api'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -describe('EntityAirbrakeContent', () => { +describe('EntityAirbrakeWidget', () => { const worker = setupServer(); setupRequestMockHandlers(worker); diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx index 327da881a3..29260427a9 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx @@ -29,7 +29,6 @@ import { ErrorApi, errorApiRef, useApi } from '@backstage/core-plugin-api'; import { airbrakeApiRef } from '../../api'; import useAsync from 'react-use/lib/useAsync'; import { AIRBRAKE_PROJECT_ID_ANNOTATION, useProjectId } from '../useProjectId'; -import { NoProjectIdError } from '../../api/AirbrakeApi'; const useStyles = makeStyles(() => ({ multilineText: { @@ -54,38 +53,21 @@ export const EntityAirbrakeWidget = ({ entity }: { entity: Entity }) => { ComponentState.Loading, ); - useEffect(() => { - if (!projectId) { - setComponentState(ComponentState.NoProjectId); - } else { - setComponentState(ComponentState.Loading); - } - }, [projectId]); - - const { loading, value, error } = useAsync(async () => { + const { loading, value } = useAsync(async () => { try { const result = await airbrakeApi.fetchGroups(projectId); setComponentState(ComponentState.Loaded); return result; } catch (e) { - if (e instanceof NoProjectIdError) { + if (!projectId) { setComponentState(ComponentState.NoProjectId); } else { setComponentState(ComponentState.Error); + errorApi.post(e); } throw e; } - }, [airbrakeApi, projectId]); - - useEffect(() => { - if ( - componentState === ComponentState.Error && - error && - !(error instanceof NoProjectIdError) - ) { - errorApi.post(error); - } - }, [componentState, error, errorApi]); + }, [airbrakeApi, errorApi, projectId]); useEffect(() => { if (loading) { From 3c8bb2854dde6e04a257185fcc7361a3f58a0528 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 3 Mar 2022 15:23:53 +0000 Subject: [PATCH 170/353] Version Packages --- .changeset/beige-snails-drum.md | 5 - .changeset/brown-dryers-serve.md | 10 - .changeset/chilled-dolls-agree.md | 10 - .changeset/chilled-items-trade.md | 13 -- .changeset/chilly-goats-suffer.md | 5 - .changeset/dependabot-8a13aed.md | 7 - .changeset/dull-months-knock.md | 5 - .changeset/eight-adults-joke.md | 5 - .changeset/fast-paws-arrive.md | 31 --- .changeset/fluffy-trees-occur.md | 5 - .changeset/gentle-icons-vanish.md | 20 -- .changeset/giant-taxis-drop.md | 5 - .changeset/gold-garlics-sing.md | 5 - .changeset/gorgeous-actors-shave.md | 12 - .changeset/gorgeous-boats-hide.md | 13 -- .changeset/grumpy-apes-repeat.md | 5 - .changeset/honest-students-clean.md | 7 - .changeset/khaki-socks-wash.md | 5 - .changeset/large-dancers-learn.md | 5 - .changeset/lemon-needles-applaud.md | 5 - .changeset/little-carpets-itch.md | 14 -- .changeset/long-weeks-thank.md | 5 - .changeset/lovely-goats-press.md | 5 - .changeset/metal-months-tie.md | 5 - .changeset/nasty-pets-join.md | 9 - .changeset/new-foxes-matter.md | 7 - .changeset/nice-dragons-collect.md | 5 - .changeset/nice-walls-reply.md | 14 -- .changeset/ninety-kids-drop.md | 8 - .changeset/old-waves-wash.md | 8 - .changeset/olive-glasses-approve.md | 5 - .changeset/olive-lobsters-rescue.md | 5 - .changeset/orange-cobras-shave.md | 12 - .changeset/orange-crews-explain.md | 5 - .changeset/patched.json | 4 +- .changeset/plenty-tables-mix.md | 5 - .changeset/polite-houses-wink.md | 5 - .changeset/polite-poems-nail.md | 5 - .changeset/poor-hounds-beam.md | 5 - .changeset/popular-items-tan.md | 7 - .changeset/quick-mugs-pay.md | 5 - .changeset/rare-insects-punch.md | 10 - .changeset/real-kids-hide.md | 5 - .changeset/red-chefs-beam.md | 6 - .changeset/rotten-windows-worry.md | 5 - .changeset/search-blankly-have-a-nice-life.md | 18 -- .changeset/search-done-me-no-favor.md | 12 - .changeset/search-just-smile-politely.md | 13 -- .changeset/search-like-a-bank-teller.md | 43 ---- .changeset/search-selfless-cold-composed.md | 12 - .changeset/search-throw-me-a-right.md | 13 -- .changeset/serious-mayflies-thank.md | 5 - .changeset/shaggy-readers-explain.md | 5 - .changeset/silver-boxes-flash.md | 9 - .changeset/silver-chairs-compete.md | 5 - .changeset/small-brooms-retire.md | 6 - .changeset/small-hornets-dress.md | 10 - .changeset/smart-items-fry.md | 5 - .changeset/sour-eggs-kick.md | 10 - .changeset/spicy-doors-relax.md | 5 - .changeset/spicy-lies-grin.md | 5 - .changeset/spicy-onions-kiss.md | 5 - .changeset/strong-suns-hope.md | 5 - .changeset/swift-roses-hug.md | 5 - .changeset/swift-rules-hunt.md | 5 - .changeset/tame-lions-know.md | 5 - .changeset/tasty-poems-raise.md | 5 - .changeset/techdocs-cats-whisper.md | 5 - .changeset/techdocs-panthers-listen.md | 5 - .changeset/techdocs-toes-lie.md | 5 - .changeset/techdocs-war-on-war.md | 5 - .changeset/ten-fireants-march.md | 8 - .changeset/tidy-hairs-sip.md | 5 - .changeset/tidy-jokes-dream.md | 5 - .changeset/tiny-jobs-hunt.md | 5 - .changeset/tiny-lobsters-exercise.md | 29 --- .changeset/tough-clocks-enjoy.md | 5 - .changeset/tough-pets-beg.md | 5 - .changeset/tricky-students-promise.md | 22 -- .changeset/two-forks-tell.md | 6 - .changeset/two-lobsters-hammer.md | 6 - .changeset/unlucky-queens-brush.md | 5 - .changeset/weak-news-reply.md | 23 -- .changeset/wild-dolphins-lick.md | 7 - .changeset/wise-foxes-confess.md | 5 - package.json | 2 +- packages/app-defaults/CHANGELOG.md | 14 ++ packages/app-defaults/package.json | 14 +- packages/app/CHANGELOG.md | 53 +++++ packages/app/package.json | 100 ++++---- packages/backend-common/CHANGELOG.md | 22 ++ packages/backend-common/package.json | 8 +- packages/backend-tasks/CHANGELOG.md | 7 + packages/backend-tasks/package.json | 8 +- packages/backend-test-utils/CHANGELOG.md | 8 + packages/backend-test-utils/package.json | 8 +- packages/backend/CHANGELOG.md | 39 ++++ packages/backend/package.json | 68 +++--- packages/catalog-client/CHANGELOG.md | 18 ++ packages/catalog-client/package.json | 6 +- packages/catalog-model/CHANGELOG.md | 33 +++ packages/catalog-model/package.json | 4 +- packages/cli/CHANGELOG.md | 10 + packages/cli/package.json | 14 +- packages/config/package.json | 2 +- packages/core-app-api/CHANGELOG.md | 14 ++ packages/core-app-api/package.json | 8 +- packages/core-components/CHANGELOG.md | 14 ++ packages/core-components/package.json | 10 +- packages/core-plugin-api/CHANGELOG.md | 8 + packages/core-plugin-api/package.json | 8 +- packages/create-app/CHANGELOG.md | 81 +++++++ packages/create-app/package.json | 2 +- packages/dev-utils/CHANGELOG.md | 14 ++ packages/dev-utils/package.json | 20 +- packages/errors/package.json | 2 +- packages/integration-react/CHANGELOG.md | 9 + packages/integration-react/package.json | 14 +- packages/integration/CHANGELOG.md | 10 + packages/integration/package.json | 6 +- packages/release-manifests/package.json | 2 +- packages/search-common/CHANGELOG.md | 23 ++ packages/search-common/package.json | 6 +- .../techdocs-cli-embedded-app/CHANGELOG.md | 16 ++ .../techdocs-cli-embedded-app/package.json | 24 +- packages/techdocs-cli/CHANGELOG.md | 10 + packages/techdocs-cli/package.json | 10 +- packages/techdocs-common/CHANGELOG.md | 12 + packages/techdocs-common/package.json | 12 +- packages/test-utils/CHANGELOG.md | 15 ++ packages/test-utils/package.json | 12 +- packages/theme/package.json | 2 +- packages/types/package.json | 2 +- packages/version-bridge/package.json | 2 +- plugins/airbrake-backend/CHANGELOG.md | 8 + plugins/airbrake-backend/package.json | 6 +- plugins/airbrake/CHANGELOG.md | 13 ++ plugins/airbrake/package.json | 22 +- plugins/allure/CHANGELOG.md | 10 + plugins/allure/package.json | 18 +- plugins/analytics-module-ga/CHANGELOG.md | 9 + plugins/analytics-module-ga/package.json | 14 +- plugins/apache-airflow/CHANGELOG.md | 8 + plugins/apache-airflow/package.json | 14 +- plugins/api-docs/CHANGELOG.md | 12 + plugins/api-docs/package.json | 20 +- plugins/app-backend/CHANGELOG.md | 7 + plugins/app-backend/package.json | 8 +- plugins/auth-backend/CHANGELOG.md | 16 ++ plugins/auth-backend/package.json | 14 +- plugins/auth-node/CHANGELOG.md | 8 + plugins/auth-node/package.json | 8 +- plugins/azure-devops-backend/CHANGELOG.md | 7 + plugins/azure-devops-backend/package.json | 6 +- plugins/azure-devops-common/package.json | 2 +- plugins/azure-devops/CHANGELOG.md | 10 + plugins/azure-devops/package.json | 18 +- plugins/badges-backend/CHANGELOG.md | 10 + plugins/badges-backend/package.json | 10 +- plugins/badges/CHANGELOG.md | 10 + plugins/badges/package.json | 18 +- plugins/bazaar-backend/CHANGELOG.md | 8 + plugins/bazaar-backend/package.json | 8 +- plugins/bazaar/CHANGELOG.md | 13 ++ plugins/bazaar/package.json | 20 +- plugins/bitrise/CHANGELOG.md | 10 + plugins/bitrise/package.json | 18 +- .../catalog-backend-module-aws/CHANGELOG.md | 9 + .../catalog-backend-module-aws/package.json | 8 +- .../catalog-backend-module-ldap/CHANGELOG.md | 10 + .../catalog-backend-module-ldap/package.json | 8 +- .../CHANGELOG.md | 12 + .../package.json | 12 +- plugins/catalog-backend/CHANGELOG.md | 52 +++++ plugins/catalog-backend/package.json | 30 +-- plugins/catalog-common/CHANGELOG.md | 28 +++ plugins/catalog-common/package.json | 8 +- plugins/catalog-graph/CHANGELOG.md | 14 ++ plugins/catalog-graph/package.json | 22 +- plugins/catalog-graphql/CHANGELOG.md | 7 + plugins/catalog-graphql/package.json | 8 +- plugins/catalog-import/CHANGELOG.md | 16 ++ plugins/catalog-import/package.json | 24 +- plugins/catalog-react/CHANGELOG.md | 42 ++++ plugins/catalog-react/package.json | 26 +-- plugins/catalog/CHANGELOG.md | 26 +++ plugins/catalog/package.json | 28 +-- plugins/cicd-statistics/CHANGELOG.md | 9 + plugins/cicd-statistics/package.json | 8 +- plugins/circleci/CHANGELOG.md | 10 + plugins/circleci/package.json | 18 +- plugins/cloudbuild/CHANGELOG.md | 10 + plugins/cloudbuild/package.json | 18 +- plugins/code-climate/CHANGELOG.md | 10 + plugins/code-climate/package.json | 16 +- plugins/code-coverage-backend/CHANGELOG.md | 12 + plugins/code-coverage-backend/package.json | 12 +- plugins/code-coverage/CHANGELOG.md | 11 + plugins/code-coverage/package.json | 18 +- plugins/config-schema/CHANGELOG.md | 8 + plugins/config-schema/package.json | 14 +- plugins/cost-insights/CHANGELOG.md | 9 + plugins/cost-insights/package.json | 16 +- plugins/explore-react/CHANGELOG.md | 7 + plugins/explore-react/package.json | 10 +- plugins/explore/CHANGELOG.md | 13 ++ plugins/explore/package.json | 20 +- plugins/firehydrant/CHANGELOG.md | 9 + plugins/firehydrant/package.json | 16 +- plugins/fossa/CHANGELOG.md | 13 ++ plugins/fossa/package.json | 18 +- plugins/gcp-projects/CHANGELOG.md | 8 + plugins/gcp-projects/package.json | 14 +- plugins/git-release-manager/CHANGELOG.md | 9 + plugins/git-release-manager/package.json | 16 +- plugins/github-actions/CHANGELOG.md | 11 + plugins/github-actions/package.json | 20 +- plugins/github-deployments/CHANGELOG.md | 12 + plugins/github-deployments/package.json | 22 +- plugins/gitops-profiles/CHANGELOG.md | 8 + plugins/gitops-profiles/package.json | 14 +- plugins/gocd/CHANGELOG.md | 10 + plugins/gocd/package.json | 18 +- plugins/graphiql/CHANGELOG.md | 8 + plugins/graphiql/package.json | 14 +- plugins/graphql-backend/CHANGELOG.md | 8 + plugins/graphql-backend/package.json | 8 +- plugins/home/CHANGELOG.md | 11 + plugins/home/package.json | 20 +- plugins/ilert/CHANGELOG.md | 10 + plugins/ilert/package.json | 18 +- plugins/jenkins-backend/CHANGELOG.md | 23 ++ plugins/jenkins-backend/package.json | 16 +- plugins/jenkins-common/CHANGELOG.md | 13 ++ plugins/jenkins-common/package.json | 8 +- plugins/jenkins/CHANGELOG.md | 19 ++ plugins/jenkins/package.json | 20 +- plugins/kafka-backend/CHANGELOG.md | 8 + plugins/kafka-backend/package.json | 8 +- plugins/kafka/CHANGELOG.md | 10 + plugins/kafka/package.json | 18 +- plugins/kubernetes-backend/CHANGELOG.md | 9 + plugins/kubernetes-backend/package.json | 10 +- plugins/kubernetes-common/CHANGELOG.md | 7 + plugins/kubernetes-common/package.json | 6 +- plugins/kubernetes/CHANGELOG.md | 11 + plugins/kubernetes/package.json | 20 +- plugins/lighthouse/CHANGELOG.md | 10 + plugins/lighthouse/package.json | 18 +- plugins/newrelic-dashboard/CHANGELOG.md | 10 + plugins/newrelic-dashboard/package.json | 14 +- plugins/newrelic/CHANGELOG.md | 8 + plugins/newrelic/package.json | 14 +- plugins/org/CHANGELOG.md | 12 + plugins/org/package.json | 20 +- plugins/pagerduty/CHANGELOG.md | 10 + plugins/pagerduty/package.json | 18 +- plugins/permission-backend/CHANGELOG.md | 10 + plugins/permission-backend/package.json | 12 +- plugins/permission-common/CHANGELOG.md | 6 + plugins/permission-common/package.json | 4 +- plugins/permission-node/CHANGELOG.md | 13 ++ plugins/permission-node/package.json | 10 +- plugins/permission-react/CHANGELOG.md | 8 + plugins/permission-react/package.json | 10 +- plugins/proxy-backend/CHANGELOG.md | 7 + plugins/proxy-backend/package.json | 6 +- plugins/rollbar-backend/CHANGELOG.md | 7 + plugins/rollbar-backend/package.json | 8 +- plugins/rollbar/CHANGELOG.md | 11 + plugins/rollbar/package.json | 18 +- .../CHANGELOG.md | 10 + .../package.json | 10 +- .../CHANGELOG.md | 9 + .../package.json | 10 +- .../package.json | 4 +- plugins/scaffolder-backend/CHANGELOG.md | 22 ++ plugins/scaffolder-backend/package.json | 20 +- plugins/scaffolder-common/CHANGELOG.md | 7 + plugins/scaffolder-common/package.json | 6 +- plugins/scaffolder/CHANGELOG.md | 33 +++ plugins/scaffolder/package.json | 32 +-- .../CHANGELOG.md | 19 ++ .../package.json | 10 +- plugins/search-backend-module-pg/CHANGELOG.md | 20 ++ plugins/search-backend-module-pg/package.json | 12 +- plugins/search-backend-node/CHANGELOG.md | 23 ++ plugins/search-backend-node/package.json | 8 +- plugins/search-backend/CHANGELOG.md | 12 + plugins/search-backend/package.json | 16 +- plugins/search/CHANGELOG.md | 12 + plugins/search/package.json | 20 +- plugins/sentry/CHANGELOG.md | 10 + plugins/sentry/package.json | 18 +- plugins/shortcuts/CHANGELOG.md | 8 + plugins/shortcuts/package.json | 14 +- plugins/sonarqube/CHANGELOG.md | 10 + plugins/sonarqube/package.json | 18 +- plugins/splunk-on-call/CHANGELOG.md | 10 + plugins/splunk-on-call/package.json | 18 +- .../CHANGELOG.md | 8 + .../package.json | 8 +- plugins/tech-insights-backend/CHANGELOG.md | 11 + plugins/tech-insights-backend/package.json | 14 +- plugins/tech-insights-common/package.json | 2 +- plugins/tech-insights-node/CHANGELOG.md | 7 + plugins/tech-insights-node/package.json | 6 +- plugins/tech-insights/CHANGELOG.md | 11 + plugins/tech-insights/package.json | 18 +- plugins/tech-radar/CHANGELOG.md | 8 + plugins/tech-radar/package.json | 14 +- plugins/techdocs-backend/CHANGELOG.md | 26 +++ plugins/techdocs-backend/package.json | 22 +- plugins/techdocs/CHANGELOG.md | 51 ++++ plugins/techdocs/package.json | 26 +-- plugins/todo-backend/CHANGELOG.md | 12 + plugins/todo-backend/package.json | 12 +- plugins/todo/CHANGELOG.md | 10 + plugins/todo/package.json | 18 +- plugins/user-settings/CHANGELOG.md | 12 + plugins/user-settings/package.json | 14 +- plugins/xcmetrics/CHANGELOG.md | 8 + plugins/xcmetrics/package.json | 14 +- yarn.lock | 218 +++++++++++++----- 324 files changed, 2644 insertions(+), 1649 deletions(-) delete mode 100644 .changeset/beige-snails-drum.md delete mode 100644 .changeset/brown-dryers-serve.md delete mode 100644 .changeset/chilled-dolls-agree.md delete mode 100644 .changeset/chilled-items-trade.md delete mode 100644 .changeset/chilly-goats-suffer.md delete mode 100644 .changeset/dependabot-8a13aed.md delete mode 100644 .changeset/dull-months-knock.md delete mode 100644 .changeset/eight-adults-joke.md delete mode 100644 .changeset/fast-paws-arrive.md delete mode 100644 .changeset/fluffy-trees-occur.md delete mode 100644 .changeset/gentle-icons-vanish.md delete mode 100644 .changeset/giant-taxis-drop.md delete mode 100644 .changeset/gold-garlics-sing.md delete mode 100644 .changeset/gorgeous-actors-shave.md delete mode 100644 .changeset/gorgeous-boats-hide.md delete mode 100644 .changeset/grumpy-apes-repeat.md delete mode 100644 .changeset/honest-students-clean.md delete mode 100644 .changeset/khaki-socks-wash.md delete mode 100644 .changeset/large-dancers-learn.md delete mode 100644 .changeset/lemon-needles-applaud.md delete mode 100644 .changeset/little-carpets-itch.md delete mode 100644 .changeset/long-weeks-thank.md delete mode 100644 .changeset/lovely-goats-press.md delete mode 100644 .changeset/metal-months-tie.md delete mode 100644 .changeset/nasty-pets-join.md delete mode 100644 .changeset/new-foxes-matter.md delete mode 100644 .changeset/nice-dragons-collect.md delete mode 100644 .changeset/nice-walls-reply.md delete mode 100644 .changeset/ninety-kids-drop.md delete mode 100644 .changeset/old-waves-wash.md delete mode 100644 .changeset/olive-glasses-approve.md delete mode 100644 .changeset/olive-lobsters-rescue.md delete mode 100644 .changeset/orange-cobras-shave.md delete mode 100644 .changeset/orange-crews-explain.md delete mode 100644 .changeset/plenty-tables-mix.md delete mode 100644 .changeset/polite-houses-wink.md delete mode 100644 .changeset/polite-poems-nail.md delete mode 100644 .changeset/poor-hounds-beam.md delete mode 100644 .changeset/popular-items-tan.md delete mode 100644 .changeset/quick-mugs-pay.md delete mode 100644 .changeset/rare-insects-punch.md delete mode 100644 .changeset/real-kids-hide.md delete mode 100644 .changeset/red-chefs-beam.md delete mode 100644 .changeset/rotten-windows-worry.md delete mode 100644 .changeset/search-blankly-have-a-nice-life.md delete mode 100644 .changeset/search-done-me-no-favor.md delete mode 100644 .changeset/search-just-smile-politely.md delete mode 100644 .changeset/search-like-a-bank-teller.md delete mode 100644 .changeset/search-selfless-cold-composed.md delete mode 100644 .changeset/search-throw-me-a-right.md delete mode 100644 .changeset/serious-mayflies-thank.md delete mode 100644 .changeset/shaggy-readers-explain.md delete mode 100644 .changeset/silver-boxes-flash.md delete mode 100644 .changeset/silver-chairs-compete.md delete mode 100644 .changeset/small-brooms-retire.md delete mode 100644 .changeset/small-hornets-dress.md delete mode 100644 .changeset/smart-items-fry.md delete mode 100644 .changeset/sour-eggs-kick.md delete mode 100644 .changeset/spicy-doors-relax.md delete mode 100644 .changeset/spicy-lies-grin.md delete mode 100644 .changeset/spicy-onions-kiss.md delete mode 100644 .changeset/strong-suns-hope.md delete mode 100644 .changeset/swift-roses-hug.md delete mode 100644 .changeset/swift-rules-hunt.md delete mode 100644 .changeset/tame-lions-know.md delete mode 100644 .changeset/tasty-poems-raise.md delete mode 100644 .changeset/techdocs-cats-whisper.md delete mode 100644 .changeset/techdocs-panthers-listen.md delete mode 100644 .changeset/techdocs-toes-lie.md delete mode 100644 .changeset/techdocs-war-on-war.md delete mode 100644 .changeset/ten-fireants-march.md delete mode 100644 .changeset/tidy-hairs-sip.md delete mode 100644 .changeset/tidy-jokes-dream.md delete mode 100644 .changeset/tiny-jobs-hunt.md delete mode 100644 .changeset/tiny-lobsters-exercise.md delete mode 100644 .changeset/tough-clocks-enjoy.md delete mode 100644 .changeset/tough-pets-beg.md delete mode 100644 .changeset/tricky-students-promise.md delete mode 100644 .changeset/two-forks-tell.md delete mode 100644 .changeset/two-lobsters-hammer.md delete mode 100644 .changeset/unlucky-queens-brush.md delete mode 100644 .changeset/weak-news-reply.md delete mode 100644 .changeset/wild-dolphins-lick.md delete mode 100644 .changeset/wise-foxes-confess.md create mode 100644 plugins/jenkins-common/CHANGELOG.md diff --git a/.changeset/beige-snails-drum.md b/.changeset/beige-snails-drum.md deleted file mode 100644 index 3476c9383b..0000000000 --- a/.changeset/beige-snails-drum.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/catalog-model': patch ---- - -Move `@types/json-schema` to be a dev dependency diff --git a/.changeset/brown-dryers-serve.md b/.changeset/brown-dryers-serve.md deleted file mode 100644 index 40c02538b8..0000000000 --- a/.changeset/brown-dryers-serve.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@backstage/catalog-client': minor ---- - -**BREAKING**: Removed the old deprecated request/response types: - -- `CatalogEntitiesRequest` - please use `GetEntitiesRequest` instead -- `CatalogEntityAncestorsRequest` - please use `GetEntityAncestorsRequest` instead -- `CatalogEntityAncestorsResponse` - please use `GetEntityAncestorsResponse` instead -- `CatalogListResponse` - please use `GetEntitiesResponse` instead diff --git a/.changeset/chilled-dolls-agree.md b/.changeset/chilled-dolls-agree.md deleted file mode 100644 index 750307bf53..0000000000 --- a/.changeset/chilled-dolls-agree.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': minor ---- - -**Breaking**: Mark permission-related exports as alpha. This means that the exports below should now be imported from `@backstage/plugin-catalog-backend/alpha` instead of `@backstage/plugin-catalog-backend`. - -- `catalogConditions` -- `createCatalogPolicyDecision` -- `permissionRules` -- `createCatalogPermissionRule` diff --git a/.changeset/chilled-items-trade.md b/.changeset/chilled-items-trade.md deleted file mode 100644 index b069885498..0000000000 --- a/.changeset/chilled-items-trade.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@backstage/backend-common': minor ---- - -**BREAKING**: The connection string for `redis` cache store now requires a protocol prefix. - -```diff -backend: - cache: - store: redis -- connection: user:pass@cache.example.com:6379 -+ connection: redis://user:pass@cache.example.com:6379 -``` diff --git a/.changeset/chilly-goats-suffer.md b/.changeset/chilly-goats-suffer.md deleted file mode 100644 index da98b4019b..0000000000 --- a/.changeset/chilly-goats-suffer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Applied fix from version 0.17.2 of this package, which is part of the v0.69.2 release of Backstage. diff --git a/.changeset/dependabot-8a13aed.md b/.changeset/dependabot-8a13aed.md deleted file mode 100644 index 13075ccde2..0000000000 --- a/.changeset/dependabot-8a13aed.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/core-components': patch -'@backstage/plugin-search': patch -'@backstage/plugin-techdocs': patch ---- - -chore(deps): bump `react-text-truncate` from 0.17.0 to 0.18.0 diff --git a/.changeset/dull-months-knock.md b/.changeset/dull-months-knock.md deleted file mode 100644 index 3b957518c2..0000000000 --- a/.changeset/dull-months-knock.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs-backend': patch ---- - -Added a new interface that allows for customization of when to build techdocs diff --git a/.changeset/eight-adults-joke.md b/.changeset/eight-adults-joke.md deleted file mode 100644 index ffda738213..0000000000 --- a/.changeset/eight-adults-joke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-analytics-module-ga': patch ---- - -Added CSP instructions to README diff --git a/.changeset/fast-paws-arrive.md b/.changeset/fast-paws-arrive.md deleted file mode 100644 index 9c5f4f444f..0000000000 --- a/.changeset/fast-paws-arrive.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Update the template to reflect the renaming of `DocsResultListItem` to `TechDocsSearchResultListItem` from `@backstage/plugin-techdocs`. - -To apply this change to an existing app, make the following change to `packages/app/src/components/search/SearchPage.tsx`: - -```diff --import { DocsResultListItem } from '@backstage/plugin-techdocs'; -+import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; -``` - -```diff - case 'techdocs': - return ( -- -``` - -The `TechDocsIndexPage` now uses `DefaultTechDocsHome` as fall back if no children is provided as `LegacyTechDocsHome` is marked as deprecated. If you do not use a custom techdocs homepage, you can therefore update your app to the following: - -```diff -- }> -- -- -+ } /> -``` diff --git a/.changeset/fluffy-trees-occur.md b/.changeset/fluffy-trees-occur.md deleted file mode 100644 index 6e41a61069..0000000000 --- a/.changeset/fluffy-trees-occur.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-msgraph': patch ---- - -add config option `groupExpand` to allow expanding a single relationship diff --git a/.changeset/gentle-icons-vanish.md b/.changeset/gentle-icons-vanish.md deleted file mode 100644 index 27ea78863b..0000000000 --- a/.changeset/gentle-icons-vanish.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@backstage/plugin-api-docs': patch -'@backstage/plugin-auth-backend': patch -'@backstage/plugin-badges-backend': patch -'@backstage/plugin-catalog': patch -'@backstage/plugin-catalog-graph': patch -'@backstage/plugin-catalog-import': patch -'@backstage/plugin-catalog-react': patch -'@backstage/plugin-code-coverage-backend': patch -'@backstage/plugin-explore': patch -'@backstage/plugin-fossa': patch -'@backstage/plugin-jenkins-backend': patch -'@backstage/plugin-rollbar': patch -'@backstage/plugin-scaffolder-backend': patch -'@backstage/plugin-techdocs': patch -'@backstage/plugin-techdocs-backend': patch -'@backstage/plugin-todo-backend': patch ---- - -Use `getEntityByRef` instead of `getEntityByName` in the catalog client diff --git a/.changeset/giant-taxis-drop.md b/.changeset/giant-taxis-drop.md deleted file mode 100644 index 41f8a91d40..0000000000 --- a/.changeset/giant-taxis-drop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-common': minor ---- - -Remove resourceType property from catalogEntityCreatePermission. Resource type refers to the type of resources whose resourceRefs should be passed along with authorize requests, to allow conditional responses for that resource type. Since creation does not correspond to an entity (as the entity does not exist at the time of authorization), the resourceRef should not be included on the permission. diff --git a/.changeset/gold-garlics-sing.md b/.changeset/gold-garlics-sing.md deleted file mode 100644 index 1162cca2d4..0000000000 --- a/.changeset/gold-garlics-sing.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-jenkins-common': minor ---- - -Add a new common plugin for Jenkins which provides shared isomorphic code for the Jenkins plugin. diff --git a/.changeset/gorgeous-actors-shave.md b/.changeset/gorgeous-actors-shave.md deleted file mode 100644 index bcf5f43018..0000000000 --- a/.changeset/gorgeous-actors-shave.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Update import location of catalogEntityCreatePermission. - -To apply this change to an existing app, make the following change to `packages/app/src/App.tsx`: - -```diff --import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; -+import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; -``` diff --git a/.changeset/gorgeous-boats-hide.md b/.changeset/gorgeous-boats-hide.md deleted file mode 100644 index 5c60240433..0000000000 --- a/.changeset/gorgeous-boats-hide.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@backstage/plugin-catalog': patch -'@backstage/plugin-catalog-graph': patch -'@backstage/plugin-catalog-import': patch -'@backstage/plugin-catalog-react': patch -'@backstage/plugin-explore': patch -'@backstage/plugin-fossa': patch -'@backstage/plugin-org': patch -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-techdocs': patch ---- - -- **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead. diff --git a/.changeset/grumpy-apes-repeat.md b/.changeset/grumpy-apes-repeat.md deleted file mode 100644 index bc47b0d422..0000000000 --- a/.changeset/grumpy-apes-repeat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -DockerContainerRunner.runContainer now automatically removes the container when its execution terminates diff --git a/.changeset/honest-students-clean.md b/.changeset/honest-students-clean.md deleted file mode 100644 index ca8db28343..0000000000 --- a/.changeset/honest-students-clean.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/integration': minor -'@backstage/backend-common': patch -'@backstage/plugin-scaffolder-backend': patch ---- - -ensure `apiBaseUrl` being set for Bitbucket integrations, replace hardcoded defaults diff --git a/.changeset/khaki-socks-wash.md b/.changeset/khaki-socks-wash.md deleted file mode 100644 index 241ac75be3..0000000000 --- a/.changeset/khaki-socks-wash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Set timeout for scaffolder octokit client diff --git a/.changeset/large-dancers-learn.md b/.changeset/large-dancers-learn.md deleted file mode 100644 index 9f25990924..0000000000 --- a/.changeset/large-dancers-learn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': minor ---- - -- **BREAKING**: Removed the `FavouriteTemplate` export in favor of the `FavoriteEntity` from `@backstage/plugin-catalog-react`. Please migrate any usages to that component instead if you are creating your own `TemplateCard` page. diff --git a/.changeset/lemon-needles-applaud.md b/.changeset/lemon-needles-applaud.md deleted file mode 100644 index a77d3fd33c..0000000000 --- a/.changeset/lemon-needles-applaud.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -**DEPRECATED**: The `results` export, and instead adding `processingResult` with the same shape and purpose. diff --git a/.changeset/little-carpets-itch.md b/.changeset/little-carpets-itch.md deleted file mode 100644 index 6a4d95c140..0000000000 --- a/.changeset/little-carpets-itch.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Added some deprecations as follows: - -- **DEPRECATED**: `TemplateCardComponent` and `TaskPageComponent` props have been deprecated, and moved to a `components` prop instead. You can pass them in through there instead. -- **DEPRECATED**: `TemplateList` and `TemplateListProps` has been deprecated. Please use the `TemplateCard` to create your own list component instead. -- **DEPRECATED**: `setSecret` has been deprecated in favour of `setSecrets` when calling `useTemplateSecrets` - -Other notable changes: - -- `scaffolderApi.scaffold()` `values` type has been narrowed from `Record` to `Record` instead. -- Moved all navigation internally over to using `routeRefs` and `subRouteRefs` diff --git a/.changeset/long-weeks-thank.md b/.changeset/long-weeks-thank.md deleted file mode 100644 index a6c3a0719f..0000000000 --- a/.changeset/long-weeks-thank.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Internal restructuring to collect the various provider files in a `modules` folder while waiting to be externalized diff --git a/.changeset/lovely-goats-press.md b/.changeset/lovely-goats-press.md deleted file mode 100644 index fc3c0b4763..0000000000 --- a/.changeset/lovely-goats-press.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Collapse techdocs sidebar on small devices diff --git a/.changeset/metal-months-tie.md b/.changeset/metal-months-tie.md deleted file mode 100644 index 890c7e8b5d..0000000000 --- a/.changeset/metal-months-tie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Deprecated `favoriteEntityTooltip` and `favoriteEntityIcon` since the utility value is very low. diff --git a/.changeset/nasty-pets-join.md b/.changeset/nasty-pets-join.md deleted file mode 100644 index 1ff07274cf..0000000000 --- a/.changeset/nasty-pets-join.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/catalog-model': patch ---- - -**DEPRECATION**: Deprecated the `EntityName` type, and added the better-named `CompoundEntityRef` to replace it. - -**DEPRECATION**: Deprecated the `getEntityName` function, and added the better-named `getCompoundEntityRef` to replace it. - -Please switch over to using the new symbols, as the old ones may be removed in a future release. diff --git a/.changeset/new-foxes-matter.md b/.changeset/new-foxes-matter.md deleted file mode 100644 index 234dd78451..0000000000 --- a/.changeset/new-foxes-matter.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/core-app-api': minor -'@backstage/core-plugin-api': minor -'@backstage/test-utils': minor ---- - -**BREAKING**: Removed the deprecated `get` method from `StorageAPI` and its implementations, this method has been replaced by the `snapshot` method. The return value from snapshot no longer includes `newValue` which has been replaced by `value`. For getting notified when a value changes, use `observe$`. diff --git a/.changeset/nice-dragons-collect.md b/.changeset/nice-dragons-collect.md deleted file mode 100644 index b3d9efefd9..0000000000 --- a/.changeset/nice-dragons-collect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-permission-common': patch ---- - -Add api doc comments to `Permission` type properties. diff --git a/.changeset/nice-walls-reply.md b/.changeset/nice-walls-reply.md deleted file mode 100644 index f6d1fb7562..0000000000 --- a/.changeset/nice-walls-reply.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/plugin-catalog-common': minor ---- - -**Breaking**: Mark permission-related exports as alpha. This means that the exports below should now be imported from `@backstage/plugin-catalog-common/alpha` instead of `@backstage/plugin-catalog-common`. - -- `RESOURCE_TYPE_CATALOG_ENTITY` -- `catalogEntityReadPermission` -- `catalogEntityCreatePermission` -- `catalogEntityDeletePermission` -- `catalogEntityRefreshPermission` -- `catalogLocationReadPermission` -- `catalogLocationCreatePermission` -- `catalogLocationDeletePermission` diff --git a/.changeset/ninety-kids-drop.md b/.changeset/ninety-kids-drop.md deleted file mode 100644 index ed5cf6330b..0000000000 --- a/.changeset/ninety-kids-drop.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-catalog-react': minor ---- - -Removed some previously deprecated `routeRefs` as follows: - -- **BREAKING**: Removed `entityRoute` in favor of `entityRouteRef`. -- **BREAKING**: Removed the previously deprecated `rootRoute` and `catalogRouteRef`. If you want to refer to the catalog index page from a public plugin you now need to use an `ExternalRouteRef` instead. For private plugins it is possible to take the shortcut of referring directly to `catalogPlugin.routes.indexPage` instead. diff --git a/.changeset/old-waves-wash.md b/.changeset/old-waves-wash.md deleted file mode 100644 index 34ba0760b5..0000000000 --- a/.changeset/old-waves-wash.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-aws': patch -'@backstage/plugin-catalog-backend-module-ldap': patch -'@backstage/plugin-catalog-backend-module-msgraph': patch -'@backstage/plugin-scaffolder-backend': patch ---- - -Use the new `processingResult` export from the catalog backend diff --git a/.changeset/olive-glasses-approve.md b/.changeset/olive-glasses-approve.md deleted file mode 100644 index e74b7ef485..0000000000 --- a/.changeset/olive-glasses-approve.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Export FetchUrlReader to facilitate more flexible configuration of the backend. diff --git a/.changeset/olive-lobsters-rescue.md b/.changeset/olive-lobsters-rescue.md deleted file mode 100644 index 37a7712782..0000000000 --- a/.changeset/olive-lobsters-rescue.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-app-api': minor ---- - -**BREAKING**: Removed export of `GithubSession` and `SamlSession` which are only used internally. diff --git a/.changeset/orange-cobras-shave.md b/.changeset/orange-cobras-shave.md deleted file mode 100644 index ad76443c8c..0000000000 --- a/.changeset/orange-cobras-shave.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@backstage/plugin-jenkins-backend': patch ---- - -Jenkins plugin supports permissions now. We have added a new permission, so you can manage the permission for the users. -A new permission `jenkinsExecutePermission` is provided in `jenkins-common` package. This permission rule will be applied to check rebuild actions -if user is allowed to execute this action. - -> We use 'catalog-entity' as a resource type, so you need to integrate a policy to handle catalog-entity resources - -> You need to use this permission in your permission policy to check the user role/rights and return -> `AuthorizeResult.ALLOW` to allow rebuild action to logged user. (e.g: you can check if user or related group owns the entity) diff --git a/.changeset/orange-crews-explain.md b/.changeset/orange-crews-explain.md deleted file mode 100644 index 4b831392eb..0000000000 --- a/.changeset/orange-crews-explain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': minor ---- - -**BREAKING**: Removed the deprecated `app.` template variables from the `index.html` templating. These should be replaced by using `config.getString("app.")` instead. diff --git a/.changeset/patched.json b/.changeset/patched.json index 486cfe13a5..1029c8f106 100644 --- a/.changeset/patched.json +++ b/.changeset/patched.json @@ -1,5 +1,3 @@ { - "currentReleaseVersion": { - "@backstage/plugin-scaffolder-backend": "0.17.2" - } + "currentReleaseVersion": {} } diff --git a/.changeset/plenty-tables-mix.md b/.changeset/plenty-tables-mix.md deleted file mode 100644 index 5dffdf6946..0000000000 --- a/.changeset/plenty-tables-mix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Deprecated the `BitbucketRepositoryParser` type. diff --git a/.changeset/polite-houses-wink.md b/.changeset/polite-houses-wink.md deleted file mode 100644 index 3e177f5383..0000000000 --- a/.changeset/polite-houses-wink.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -- **BREAKING**: The `isOwnerOf` function has been marked as `@alpha` and is now only available via the `@backstage/plugin-catalog-react/alpha` import. The limitations of this function with regards to only supporting direct relations have also been documented. diff --git a/.changeset/polite-poems-nail.md b/.changeset/polite-poems-nail.md deleted file mode 100644 index c3eb97fce0..0000000000 --- a/.changeset/polite-poems-nail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': minor ---- - -Marked `useEntityPermission` as alpha since the underlying permission framework is under active development. diff --git a/.changeset/poor-hounds-beam.md b/.changeset/poor-hounds-beam.md deleted file mode 100644 index 925370c1b0..0000000000 --- a/.changeset/poor-hounds-beam.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -The CLI now bundles both version 16 and 17 of the patched `@hot-loader/react-dom` dependency, and selects the appropriate one based on what version of `react-dom` is installed within the app. diff --git a/.changeset/popular-items-tan.md b/.changeset/popular-items-tan.md deleted file mode 100644 index b91773a5d8..0000000000 --- a/.changeset/popular-items-tan.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-permission-node': patch ---- - -Export some utility functions for parsing PermissionCriteria - -`isAndCriteria`, `isOrCriteria`, `isNotCriteria` are now exported. diff --git a/.changeset/quick-mugs-pay.md b/.changeset/quick-mugs-pay.md deleted file mode 100644 index f2d3a00eb8..0000000000 --- a/.changeset/quick-mugs-pay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-msgraph': patch ---- - -add documentation for config options `userGroupMemberSearch` and `groupSearch` diff --git a/.changeset/rare-insects-punch.md b/.changeset/rare-insects-punch.md deleted file mode 100644 index d22b851abd..0000000000 --- a/.changeset/rare-insects-punch.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@backstage/app-defaults': minor -'@backstage/core-app-api': minor -'@backstage/core-components': minor -'@backstage/core-plugin-api': minor -'@backstage/test-utils': minor -'@backstage/plugin-user-settings': minor ---- - -**BREAKING**: Removed deprecated `auth0AuthApiRef`, `oauth2ApiRef`, `samlAuthApiRef` and `oidcAuthApiRef` as these APIs are too generic to be useful. Instructions for how to migrate can be found at [https://backstage.io/docs/api/deprecations#generic-auth-api-refs](https://backstage.io/docs/api/deprecations#generic-auth-api-refs). diff --git a/.changeset/real-kids-hide.md b/.changeset/real-kids-hide.md deleted file mode 100644 index 188a4061c9..0000000000 --- a/.changeset/real-kids-hide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': minor ---- - -**Breaking**: Removed `entityRef` from `CatalogProcessorRelationResult`. The field is not used by the catalog and relation information is already available inside the `reation` property. diff --git a/.changeset/red-chefs-beam.md b/.changeset/red-chefs-beam.md deleted file mode 100644 index 5cadccb0aa..0000000000 --- a/.changeset/red-chefs-beam.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-airbrake': patch -'@backstage/plugin-airbrake-backend': patch ---- - -The Airbrake plugin installation instructions have been updated to work better and conform to how the frontend and backend plugins are supposed to be integrated into a Backstage instance. diff --git a/.changeset/rotten-windows-worry.md b/.changeset/rotten-windows-worry.md deleted file mode 100644 index a1ab353ce3..0000000000 --- a/.changeset/rotten-windows-worry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/catalog-client': patch ---- - -**DEPRECATION**: Deprecated `getEntityByName` from `CatalogApi` and added `getEntityByRef` instead, which accepts both string and compound ref forms. diff --git a/.changeset/search-blankly-have-a-nice-life.md b/.changeset/search-blankly-have-a-nice-life.md deleted file mode 100644 index 63dc6051bd..0000000000 --- a/.changeset/search-blankly-have-a-nice-life.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -'@backstage/plugin-search-backend-node': minor -'@backstage/search-common': minor ---- - -**BREAKING** - -The Backstage Search Platform's indexing process has been rewritten as a stream -pipeline in order to improve efficiency and performance on large document sets. - -The concepts of `Collator` and `Decorator` have been replaced with readable and -transform object streams (respectively), as well as factory classes to -instantiate them. Accordingly, the `SearchEngine.index()` method has also been -replaced with a `getIndexer()` factory method that resolves to a writable -object stream. - -Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) -for further details. diff --git a/.changeset/search-done-me-no-favor.md b/.changeset/search-done-me-no-favor.md deleted file mode 100644 index 0b4e24c4a8..0000000000 --- a/.changeset/search-done-me-no-favor.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@backstage/plugin-search-backend-module-pg': minor ---- - -**BREAKING** - -The `PgSearchEngine` implements the new stream-based indexing process expected -by the latest `@backstage/search-backend-node`. - -When updating to this version, you must also update to the latest version of -`@backstage/search-backend-node`. Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) -for further details. diff --git a/.changeset/search-just-smile-politely.md b/.changeset/search-just-smile-politely.md deleted file mode 100644 index a7831155da..0000000000 --- a/.changeset/search-just-smile-politely.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@backstage/plugin-techdocs-backend': patch ---- - -A `DefaultTechDocsCollatorFactory`, which works with the new stream-based -search indexing subsystem, is now available. The `DefaultTechDocsCollator` will -continue to be available for those unable to upgrade to the stream-based -`@backstage/search-backend-node` (and related packages), however it is now -marked as deprecated and will be removed in a future version. - -To upgrade this plugin and the search indexing subsystem in one go, check -[this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) -for necessary changes to your search backend plugin configuration. diff --git a/.changeset/search-like-a-bank-teller.md b/.changeset/search-like-a-bank-teller.md deleted file mode 100644 index a9b0330eb2..0000000000 --- a/.changeset/search-like-a-bank-teller.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -The Backstage Search Platform's indexing process has been rewritten as a stream -pipeline in order to improve efficiency and performance on large document sets. - -To take advantage of this, upgrade to the latest version of -`@backstage/plugin-search-backend-node`, as well as any backend plugins whose -collators you are using. Then, make the following changes to your -`/packages/backend/src/plugins/search.ts` file: - -```diff --import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; --import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend'; -+import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; -+import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; - -// ... - - const indexBuilder = new IndexBuilder({ logger, searchEngine }); - - indexBuilder.addCollator({ - defaultRefreshIntervalSeconds: 600, -- collator: DefaultCatalogCollator.fromConfig(config, { discovery }), -+ factory: DefaultCatalogCollatorFactory.fromConfig(config, { discovery }), - }); - - indexBuilder.addCollator({ - defaultRefreshIntervalSeconds: 600, -- collator: DefaultTechDocsCollator.fromConfig(config, { -+ factory: DefaultTechDocsCollatorFactory.fromConfig(config, { - discovery, - logger, - }), - }); -``` - -If you've written custom collators, decorators, or search engines in your -Backstage backend instance, you will need to re-implement them as readable, -transform, and writable streams respectively (including factory classes for -instantiating them). [A how-to guide for refactoring](https://backstage.io/docs/features/search/how-to-guides#rewriting-alpha-style-collators-for-beta) -existing implementations is available. diff --git a/.changeset/search-selfless-cold-composed.md b/.changeset/search-selfless-cold-composed.md deleted file mode 100644 index 7e7dafd421..0000000000 --- a/.changeset/search-selfless-cold-composed.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'@backstage/plugin-search-backend-module-elasticsearch': minor ---- - -**BREAKING** - -The `ElasticSearchSearchEngine` implements the new stream-based indexing -process expected by the latest `@backstage/search-backend-node`. - -When updating to this version, you must also update to the latest version of -`@backstage/search-backend-node`. Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) -for further details. diff --git a/.changeset/search-throw-me-a-right.md b/.changeset/search-throw-me-a-right.md deleted file mode 100644 index 32f4e82fe4..0000000000 --- a/.changeset/search-throw-me-a-right.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -A `DefaultCatalogCollatorFactory`, which works with the new stream-based -search indexing subsystem, is now available. The `DefaultCatalogCollator` will -continue to be available for those unable to upgrade to the stream-based -`@backstage/search-backend-node` (and related packages), however it is now -marked as deprecated and will be removed in a future version. - -To upgrade this plugin and the search indexing subsystem in one go, check -[this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) -for necessary changes to your search backend plugin configuration. diff --git a/.changeset/serious-mayflies-thank.md b/.changeset/serious-mayflies-thank.md deleted file mode 100644 index b3c5e31527..0000000000 --- a/.changeset/serious-mayflies-thank.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': minor ---- - -**Breaking**: Removed optional `handleError()` from `CatalogProcessor`. This optional method is never called by the catalog processing engine and can therefore be removed. diff --git a/.changeset/shaggy-readers-explain.md b/.changeset/shaggy-readers-explain.md deleted file mode 100644 index 29266e5a32..0000000000 --- a/.changeset/shaggy-readers-explain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Cleanup API report diff --git a/.changeset/silver-boxes-flash.md b/.changeset/silver-boxes-flash.md deleted file mode 100644 index 9f30c1a34b..0000000000 --- a/.changeset/silver-boxes-flash.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -**BREAKING**: Moved **DefaultStarredEntitiesApi** to `@backstage/plugin-catalog`. If you were using this in tests, you can use the new `MockStarredEntitiesApi` from `@backstage/plugin-catalog-react` instead. - -Fixed a risky behavior where `DefaultStarredEntitiesApi` forwarded values to observers that were later mutated. - -Removed the `isStarred` method from `DefaultStarredEntitiesApi`, as it is not part of the `StarredEntitiesApi`. diff --git a/.changeset/silver-chairs-compete.md b/.changeset/silver-chairs-compete.md deleted file mode 100644 index 356cd2043b..0000000000 --- a/.changeset/silver-chairs-compete.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-plugin-api': minor ---- - -**BREAKING**: OAuth provider id is now required when passing a provider to `createAuthRequester`. diff --git a/.changeset/small-brooms-retire.md b/.changeset/small-brooms-retire.md deleted file mode 100644 index 2aa722a666..0000000000 --- a/.changeset/small-brooms-retire.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-catalog': patch -'@backstage/plugin-techdocs': patch ---- - -Removed usage of deprecated favorite utility methods. diff --git a/.changeset/small-hornets-dress.md b/.changeset/small-hornets-dress.md deleted file mode 100644 index 474c3011bd..0000000000 --- a/.changeset/small-hornets-dress.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-catalog-common': patch ---- - -**DEPRECATION**: Moved the `CatalogEntityDocument` to `@backstage/plugin-catalog-common` and deprecated the export from `@backstage/plugin-catalog-backend`. - -A new `type` field has also been added to `CatalogEntityDocument` as a replacement for `componentType`, which is now deprecated. Both fields are still present and should be set to the same value in order to avoid issues with indexing. - -Any search customizations need to be updated to use this new `type` field instead, including any custom frontend filters, custom frontend result components, custom search decorators, or non-default Catalog collator implementations. diff --git a/.changeset/smart-items-fry.md b/.changeset/smart-items-fry.md deleted file mode 100644 index 625db82ce7..0000000000 --- a/.changeset/smart-items-fry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Applied fix from `v0.17.1` of this package which is part of the `v0.69.1` release of Backstage. diff --git a/.changeset/sour-eggs-kick.md b/.changeset/sour-eggs-kick.md deleted file mode 100644 index f796b173fb..0000000000 --- a/.changeset/sour-eggs-kick.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@backstage/plugin-catalog': patch -'@backstage/plugin-catalog-react': patch ---- - -Added the following deprecations to the `catalog-react` package: - -- **DEPRECATION**: `useEntity` will now warn if the entity has not yet been loaded, and will soon throw errors instead. If you're using the default implementation of `EntityLayout` and `EntitySwitch` then these components will ensure that there is an entity loaded before rendering children. If you're implementing your own `EntityLayout` or `EntitySwitch` or something that operates outside or adjacent to them, then use `useAsyncEntity`. - -- **DEPRECATION**: the `loading`, `error` and `refresh` properties that are returned from `useEntity` have been deprecated, and are available on `useAsyncEntity` instead. diff --git a/.changeset/spicy-doors-relax.md b/.changeset/spicy-doors-relax.md deleted file mode 100644 index 3007f50d0e..0000000000 --- a/.changeset/spicy-doors-relax.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Deprecated `getEntityMetadataEditUrl` and `getEntityMetadataViewUrl` as these just return one annotation from the entity passed in. diff --git a/.changeset/spicy-lies-grin.md b/.changeset/spicy-lies-grin.md deleted file mode 100644 index b7e020f1db..0000000000 --- a/.changeset/spicy-lies-grin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-ldap': patch ---- - -Fixed bug in Catalog LDAP module to acknowledge page events to continue receiving entries if pagePause=true diff --git a/.changeset/spicy-onions-kiss.md b/.changeset/spicy-onions-kiss.md deleted file mode 100644 index 777e233b88..0000000000 --- a/.changeset/spicy-onions-kiss.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -- Replaced usage of the deprecated and now removed `rootRoute` and `catalogRouteRef`s from the `catalog-react` package diff --git a/.changeset/strong-suns-hope.md b/.changeset/strong-suns-hope.md deleted file mode 100644 index 876f5f1618..0000000000 --- a/.changeset/strong-suns-hope.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': minor ---- - -Removing the `EntityName` path for the `useEntityOwnership` as it has never worked correctly. Please pass in an entire `Entity` instead. diff --git a/.changeset/swift-roses-hug.md b/.changeset/swift-roses-hug.md deleted file mode 100644 index 0e6d76ed87..0000000000 --- a/.changeset/swift-roses-hug.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': minor ---- - -**BREAKING**: Removing the exports of the raw components that back the `CustomFieldExtensions`. diff --git a/.changeset/swift-rules-hunt.md b/.changeset/swift-rules-hunt.md deleted file mode 100644 index 15707bbf90..0000000000 --- a/.changeset/swift-rules-hunt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/techdocs-common': patch ---- - -adds passing projectID to the Storage client diff --git a/.changeset/tame-lions-know.md b/.changeset/tame-lions-know.md deleted file mode 100644 index 7f304b22ef..0000000000 --- a/.changeset/tame-lions-know.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-app-api': minor ---- - -**BREAKING**: Removed the deprecated `GithubAuth.normalizeScopes` method. diff --git a/.changeset/tasty-poems-raise.md b/.changeset/tasty-poems-raise.md deleted file mode 100644 index fbd81423b6..0000000000 --- a/.changeset/tasty-poems-raise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': minor ---- - -**BREAKING**: The `TokenFactory.issueToken` used by custom sign-in resolvers now ensures that the sub claim given is a full entity reference of the format `:/`. Any existing custom sign-in resolver functions that do not supply a full entity reference must be updated. diff --git a/.changeset/techdocs-cats-whisper.md b/.changeset/techdocs-cats-whisper.md deleted file mode 100644 index 290f8a1d7a..0000000000 --- a/.changeset/techdocs-cats-whisper.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Remove copyright from old footer in documentation generated with previous version of `mkdocs-techdocs-plugin` (`v0.2.2`). diff --git a/.changeset/techdocs-panthers-listen.md b/.changeset/techdocs-panthers-listen.md deleted file mode 100644 index 0ef28f6281..0000000000 --- a/.changeset/techdocs-panthers-listen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Show feedback when copying code snippet to clipboard. diff --git a/.changeset/techdocs-toes-lie.md b/.changeset/techdocs-toes-lie.md deleted file mode 100644 index 14ca2567e2..0000000000 --- a/.changeset/techdocs-toes-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@techdocs/cli': patch ---- - -Bump `@backstage/techdocs-common` to `0.11.10` to use `spotify/techdocs:v0.3.7` which upgrades `mkdocs-theme` as a dependency of `mkdocs-techdocs-core`. diff --git a/.changeset/techdocs-war-on-war.md b/.changeset/techdocs-war-on-war.md deleted file mode 100644 index 28722feed3..0000000000 --- a/.changeset/techdocs-war-on-war.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Fixed a bug that could cause searches in the in-context TechDocs search bar to show results from a different TechDocs site. diff --git a/.changeset/ten-fireants-march.md b/.changeset/ten-fireants-march.md deleted file mode 100644 index 8283eef5d6..0000000000 --- a/.changeset/ten-fireants-march.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-jenkins': minor ---- - -Jenkins plugin supports permissions now. We have added a new permission, so you can manage the permission for the users. See relates notes for `jenkins-plugin` for more details. - -Rebuild action will be disabled if the user does not have necessary rights to execute rebuild action. A permission policy (defined in backend) must handle and check the identity rights -and return `AuthorizeResult.ALLOW` if user is allowed to execute rebuild action. diff --git a/.changeset/tidy-hairs-sip.md b/.changeset/tidy-hairs-sip.md deleted file mode 100644 index 512447236d..0000000000 --- a/.changeset/tidy-hairs-sip.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch ---- - -Fixed bug where existing cookiecutter.json file is not used. diff --git a/.changeset/tidy-jokes-dream.md b/.changeset/tidy-jokes-dream.md deleted file mode 100644 index aa9c84fc4b..0000000000 --- a/.changeset/tidy-jokes-dream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-msgraph': patch ---- - -add `userExpand` config option to allow expanding a single relationship diff --git a/.changeset/tiny-jobs-hunt.md b/.changeset/tiny-jobs-hunt.md deleted file mode 100644 index 8cf9db20b8..0000000000 --- a/.changeset/tiny-jobs-hunt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/techdocs-common': patch ---- - -Modify techdocs builder to automatically append techdocs-core plugin to mkdocs.yaml file if it is missing. Adds an optional configuration item if this plugin needs to be omitted. diff --git a/.changeset/tiny-lobsters-exercise.md b/.changeset/tiny-lobsters-exercise.md deleted file mode 100644 index 78cc753bab..0000000000 --- a/.changeset/tiny-lobsters-exercise.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@backstage/plugin-techdocs': minor ---- - -**BREAKING:** -Table column utilities `createNameColumn`, `createOwnerColumn`, `createTypeColumn` as well as actions utilities `createCopyDocsUrlAction` and `createStarEntityAction` are no longer directly exported. Instead accessible through DocsTable and EntityListDocsTable. - -Use as following: - -```tsx -DocsTable.columns.createNameColumn(); -DocsTable.columns.createOwnerColumn(); -DocsTable.columns.createTypeColumn(); - -DocsTable.actions.createCopyDocsUrlAction(); -DocsTable.actions.createStarEntityAction(); -``` - -- Renamed `DocsResultListItem` to `TechDocsSearchResultListItem`, leaving the old name in place as a deprecations. - -- Renamed `TechDocsPage` to `TechDocsReaderPage`, leaving the old name in place as a deprecations. - -- Renamed `TechDocsPageRenderFunction` to `TechDocsPageRenderFunction`, leaving the old name in place as a deprecations. - -- Renamed `TechDocsPageHeader` to `TechDocsReaderPageHeader`, leaving the old name in place as a deprecations. - -- `LegacyTechDocsHome` marked as deprecated and will be deleted in next release, use `TechDocsCustomHome` instead. - -- `LegacyTechDocsPage` marked as deprecated and will be deleted in next release, use `TechDocsReaderPage` instead. diff --git a/.changeset/tough-clocks-enjoy.md b/.changeset/tough-clocks-enjoy.md deleted file mode 100644 index 90e00382df..0000000000 --- a/.changeset/tough-clocks-enjoy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Tweaked the wording of the "does not have a location" errors to include the actual missing annotation name, to help users better in fixing their inputs. diff --git a/.changeset/tough-pets-beg.md b/.changeset/tough-pets-beg.md deleted file mode 100644 index 7fdcc6dc01..0000000000 --- a/.changeset/tough-pets-beg.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/integration': patch ---- - -Fix Bitbucket Cloud and Bitbucket Server line number reference. diff --git a/.changeset/tricky-students-promise.md b/.changeset/tricky-students-promise.md deleted file mode 100644 index b9c6150a4a..0000000000 --- a/.changeset/tricky-students-promise.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@backstage/catalog-client': patch -'@backstage/plugin-auth-backend': patch -'@backstage/plugin-catalog': patch -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-catalog-graph': patch -'@backstage/plugin-catalog-import': patch -'@backstage/plugin-catalog-react': patch -'@backstage/plugin-code-coverage': patch -'@backstage/plugin-code-coverage-backend': patch -'@backstage/plugin-fossa': patch -'@backstage/plugin-jenkins': patch -'@backstage/plugin-jenkins-backend': patch -'@backstage/plugin-scaffolder-backend': patch -'@backstage/plugin-tech-insights': patch -'@backstage/plugin-tech-insights-backend': patch -'@backstage/plugin-techdocs': patch -'@backstage/plugin-techdocs-backend': patch -'@backstage/plugin-todo-backend': patch ---- - -Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. diff --git a/.changeset/two-forks-tell.md b/.changeset/two-forks-tell.md deleted file mode 100644 index dbb17a516c..0000000000 --- a/.changeset/two-forks-tell.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-catalog': patch -'@backstage/plugin-org': patch ---- - -Removed usage of deprecated `getEntityMetadataViewUrl` and `getEntityMetadataEditUrl` helpers. diff --git a/.changeset/two-lobsters-hammer.md b/.changeset/two-lobsters-hammer.md deleted file mode 100644 index 412901fb41..0000000000 --- a/.changeset/two-lobsters-hammer.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/core-components': patch -'@backstage/plugin-catalog-react': patch ---- - -Updated usage of `StorageApi` to use `snapshot` method instead of `get` diff --git a/.changeset/unlucky-queens-brush.md b/.changeset/unlucky-queens-brush.md deleted file mode 100644 index c080856246..0000000000 --- a/.changeset/unlucky-queens-brush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Added ability for SidebarSubmenuItem to handle external links correctly via the "to" prop diff --git a/.changeset/weak-news-reply.md b/.changeset/weak-news-reply.md deleted file mode 100644 index 4a7a38cef5..0000000000 --- a/.changeset/weak-news-reply.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@backstage/catalog-model': minor ---- - -**BREAKING**: The following changes are all breaking changes. - -Removed `EDIT_URL_ANNOTATION` and `VIEW_URL_ANNOTATION`, `LOCATION_ANNOTATION`, `ORIGIN_LOCATION_ANNOTATION`, `LOCATION_ANNOTATION`, `SOURCE_LOCATION_ANNOTATION`. All of these constants have been prefixed with ANNOTATION to be easier to find meaning `SOURCE_LOCATION_ANNOTATION` is available as `ANNOTATION_SOURCE_LOCATION`. - -Removed `parseLocationReference`, replaced by `parseLocationRef`. - -Removed `stringifyLocationReference`, replaced by `stringifyLocationRef`. - -Removed `Location` type which has been moved to `catalog-client`. - -Removed `ENTITY_DEFAULT_NAMESPACE`, replaced by `DEFAULT_NAMESPACE`. - -Removed `compareEntityToRef` compare using `stringifyEntityRef` instead. - -Removed `JSONSchema` type which should be imported from `json-schema` package instead. - -Removed utility methods: `entityHasChanges`, `generateEntityEtag`, `generateEntityUid`, `generateUpdatedEntity`. - -Removed `ENTITY_META_GENERATED_FIELDS` and `EntityRefContext`. diff --git a/.changeset/wild-dolphins-lick.md b/.changeset/wild-dolphins-lick.md deleted file mode 100644 index cdd5827b00..0000000000 --- a/.changeset/wild-dolphins-lick.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -**DEPRECATION**: Deprecated the `RefreshIntervalFunction` and `createRandomRefreshInterval` in favour of the `ProcessingIntervalFunction` and `createRandomProcessingInterval` type and method respectively. Please migrate to use the new names. - -**DEPRECATION**: Deprecated the `setRefreshInterval` and `setRefreshIntervalSeconds` methods on the `CatalogBuilder` for the new `setProcessingInterval` and `setProcessingIntervalSeconds` methods. Please migrate to use the new names. diff --git a/.changeset/wise-foxes-confess.md b/.changeset/wise-foxes-confess.md deleted file mode 100644 index 8322b66555..0000000000 --- a/.changeset/wise-foxes-confess.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Fix the support for custom defaultBranch values for Bitbucket Cloud at the `publish:bitbucket` scaffolder action. diff --git a/package.json b/package.json index 1882d400b4..e9101c2280 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "resolutions": { "**/@graphql-codegen/cli/**/ws": "^7.4.6" }, - "version": "0.69.0", + "version": "0.70.0", "dependencies": { "@manypkg/get-packages": "^1.1.3", "@microsoft/api-documenter": "^7.15.0", diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index 6fb6c46b76..33e6c09e2b 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/app-defaults +## 0.2.0 + +### Minor Changes + +- af5eaa87f4: **BREAKING**: Removed deprecated `auth0AuthApiRef`, `oauth2ApiRef`, `samlAuthApiRef` and `oidcAuthApiRef` as these APIs are too generic to be useful. Instructions for how to migrate can be found at [https://backstage.io/docs/api/deprecations#generic-auth-api-refs](https://backstage.io/docs/api/deprecations#generic-auth-api-refs). + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/core-app-api@0.6.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/plugin-permission-react@0.3.3 + ## 0.1.9 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index e65d8f53ac..78accb414f 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/app-defaults", "description": "Provides the default wiring of a Backstage App", - "version": "0.1.9", + "version": "0.2.0", "private": false, "publishConfig": { "access": "public", @@ -33,10 +33,10 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/core-components": "^0.8.10", - "@backstage/core-app-api": "^0.5.4", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-permission-react": "^0.3.2", + "@backstage/core-components": "^0.9.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-permission-react": "^0.3.3", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -46,8 +46,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@types/jest": "^26.0.7", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 029b97ec30..29f77de0b3 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,58 @@ # example-app +## 0.2.67 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-search@0.7.2 + - @backstage/plugin-techdocs@0.15.0 + - @backstage/plugin-api-docs@0.8.1 + - @backstage/plugin-catalog@0.9.1 + - @backstage/plugin-catalog-graph@0.2.13 + - @backstage/plugin-catalog-import@0.8.4 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/plugin-explore@0.3.32 + - @backstage/plugin-rollbar@0.4.1 + - @backstage/plugin-catalog-common@0.2.0 + - @backstage/plugin-org@0.5.1 + - @backstage/plugin-scaffolder@0.14.0 + - @backstage/core-app-api@0.6.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/cli@0.15.0 + - @backstage/app-defaults@0.2.0 + - @backstage/plugin-user-settings@0.4.0 + - @backstage/plugin-airbrake@0.3.1 + - @backstage/search-common@0.3.0 + - @backstage/plugin-jenkins@0.7.0 + - @backstage/plugin-code-coverage@0.1.28 + - @backstage/plugin-tech-insights@0.1.11 + - @backstage/plugin-azure-devops@0.1.17 + - @backstage/plugin-badges@0.2.25 + - @backstage/plugin-circleci@0.3.1 + - @backstage/plugin-cloudbuild@0.3.1 + - @backstage/plugin-cost-insights@0.11.23 + - @backstage/plugin-github-actions@0.5.1 + - @backstage/plugin-gocd@0.1.7 + - @backstage/plugin-home@0.4.17 + - @backstage/plugin-kafka@0.3.1 + - @backstage/plugin-kubernetes@0.6.1 + - @backstage/plugin-lighthouse@0.3.1 + - @backstage/plugin-newrelic-dashboard@0.1.9 + - @backstage/plugin-pagerduty@0.3.28 + - @backstage/plugin-sentry@0.3.39 + - @backstage/plugin-todo@0.2.3 + - @backstage/integration-react@0.1.24 + - @backstage/plugin-apache-airflow@0.1.9 + - @backstage/plugin-gcp-projects@0.3.20 + - @backstage/plugin-graphiql@0.2.33 + - @backstage/plugin-newrelic@0.3.19 + - @backstage/plugin-shortcuts@0.2.2 + - @backstage/plugin-tech-radar@0.5.8 + - @backstage/plugin-permission-react@0.3.3 + ## 0.2.66 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 25dcbbd945..d272b88e52 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,59 +1,59 @@ { "name": "example-app", - "version": "0.2.66", + "version": "0.2.67", "private": true, "backstage": { "role": "frontend" }, "bundled": true, "dependencies": { - "@backstage/app-defaults": "^0.1.9", - "@backstage/catalog-model": "^0.11.0", - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/integration-react": "^0.1.23", - "@backstage/plugin-airbrake": "^0.3.0", - "@backstage/plugin-api-docs": "^0.8.0", - "@backstage/plugin-azure-devops": "^0.1.16", - "@backstage/plugin-apache-airflow": "^0.1.8", - "@backstage/plugin-badges": "^0.2.24", - "@backstage/plugin-catalog": "^0.9.0", - "@backstage/plugin-catalog-common": "^0.1.4", - "@backstage/plugin-catalog-graph": "^0.2.12", - "@backstage/plugin-catalog-import": "^0.8.3", - "@backstage/plugin-catalog-react": "^0.7.0", - "@backstage/plugin-circleci": "^0.3.0", - "@backstage/plugin-cloudbuild": "^0.3.0", - "@backstage/plugin-code-coverage": "^0.1.27", - "@backstage/plugin-cost-insights": "^0.11.22", - "@backstage/plugin-explore": "^0.3.31", - "@backstage/plugin-gcp-projects": "^0.3.19", - "@backstage/plugin-github-actions": "^0.5.0", - "@backstage/plugin-gocd": "^0.1.6", - "@backstage/plugin-graphiql": "^0.2.32", - "@backstage/plugin-home": "^0.4.16", - "@backstage/plugin-jenkins": "^0.6.0", - "@backstage/plugin-kafka": "^0.3.0", - "@backstage/plugin-kubernetes": "^0.6.0", - "@backstage/plugin-lighthouse": "^0.3.0", - "@backstage/plugin-newrelic": "^0.3.18", - "@backstage/plugin-newrelic-dashboard": "^0.1.8", - "@backstage/plugin-org": "^0.5.0", - "@backstage/plugin-pagerduty": "0.3.27", - "@backstage/plugin-permission-react": "^0.3.2", - "@backstage/plugin-rollbar": "^0.4.0", - "@backstage/plugin-scaffolder": "^0.13.0", - "@backstage/plugin-search": "^0.7.1", - "@backstage/plugin-sentry": "^0.3.38", - "@backstage/plugin-shortcuts": "^0.2.1", - "@backstage/plugin-tech-radar": "^0.5.7", - "@backstage/plugin-techdocs": "^0.14.0", - "@backstage/plugin-todo": "^0.2.2", - "@backstage/plugin-user-settings": "^0.3.21", - "@backstage/search-common": "^0.2.4", - "@backstage/plugin-tech-insights": "^0.1.10", + "@backstage/app-defaults": "^0.2.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/integration-react": "^0.1.24", + "@backstage/plugin-airbrake": "^0.3.1", + "@backstage/plugin-api-docs": "^0.8.1", + "@backstage/plugin-azure-devops": "^0.1.17", + "@backstage/plugin-apache-airflow": "^0.1.9", + "@backstage/plugin-badges": "^0.2.25", + "@backstage/plugin-catalog": "^0.9.1", + "@backstage/plugin-catalog-common": "^0.2.0", + "@backstage/plugin-catalog-graph": "^0.2.13", + "@backstage/plugin-catalog-import": "^0.8.4", + "@backstage/plugin-catalog-react": "^0.8.0", + "@backstage/plugin-circleci": "^0.3.1", + "@backstage/plugin-cloudbuild": "^0.3.1", + "@backstage/plugin-code-coverage": "^0.1.28", + "@backstage/plugin-cost-insights": "^0.11.23", + "@backstage/plugin-explore": "^0.3.32", + "@backstage/plugin-gcp-projects": "^0.3.20", + "@backstage/plugin-github-actions": "^0.5.1", + "@backstage/plugin-gocd": "^0.1.7", + "@backstage/plugin-graphiql": "^0.2.33", + "@backstage/plugin-home": "^0.4.17", + "@backstage/plugin-jenkins": "^0.7.0", + "@backstage/plugin-kafka": "^0.3.1", + "@backstage/plugin-kubernetes": "^0.6.1", + "@backstage/plugin-lighthouse": "^0.3.1", + "@backstage/plugin-newrelic": "^0.3.19", + "@backstage/plugin-newrelic-dashboard": "^0.1.9", + "@backstage/plugin-org": "^0.5.1", + "@backstage/plugin-pagerduty": "0.3.28", + "@backstage/plugin-permission-react": "^0.3.3", + "@backstage/plugin-rollbar": "^0.4.1", + "@backstage/plugin-scaffolder": "^0.14.0", + "@backstage/plugin-search": "^0.7.2", + "@backstage/plugin-sentry": "^0.3.39", + "@backstage/plugin-shortcuts": "^0.2.2", + "@backstage/plugin-tech-radar": "^0.5.8", + "@backstage/plugin-techdocs": "^0.15.0", + "@backstage/plugin-todo": "^0.2.3", + "@backstage/plugin-user-settings": "^0.4.0", + "@backstage/search-common": "^0.3.0", + "@backstage/plugin-tech-insights": "^0.1.11", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -74,8 +74,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/plugin-permission-react": "^0.3.2", - "@backstage/test-utils": "^0.2.6", + "@backstage/plugin-permission-react": "^0.3.3", + "@backstage/test-utils": "^0.3.0", "@rjsf/core": "^3.2.1", "@testing-library/cypress": "^8.0.2", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index ce5c2f0894..8a7b5ef528 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/backend-common +## 0.12.0 + +### Minor Changes + +- 9a0510144f: **BREAKING**: The connection string for `redis` cache store now requires a protocol prefix. + + ```diff + backend: + cache: + store: redis + - connection: user:pass@cache.example.com:6379 + + connection: redis://user:pass@cache.example.com:6379 + ``` + +### Patch Changes + +- 0df6077ab5: DockerContainerRunner.runContainer now automatically removes the container when its execution terminates +- 34af86517c: ensure `apiBaseUrl` being set for Bitbucket integrations, replace hardcoded defaults +- b838717e92: Export FetchUrlReader to facilitate more flexible configuration of the backend. +- Updated dependencies + - @backstage/integration@0.8.0 + ## 0.11.0 ### Minor Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index fd5ea443ab..93ad63150b 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.11.0", + "version": "0.12.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -38,7 +38,7 @@ "@backstage/config": "^0.1.15", "@backstage/config-loader": "^0.9.6", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", + "@backstage/integration": "^0.8.0", "@backstage/types": "^0.1.3", "@google-cloud/storage": "^5.8.0", "@manypkg/get-packages": "^1.1.3", @@ -89,8 +89,8 @@ } }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/test-utils": "^0.3.0", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", "@types/concat-stream": "^2.0.0", diff --git a/packages/backend-tasks/CHANGELOG.md b/packages/backend-tasks/CHANGELOG.md index 546d4a8187..506efa1226 100644 --- a/packages/backend-tasks/CHANGELOG.md +++ b/packages/backend-tasks/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/backend-tasks +## 0.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + ## 0.1.9 ### Patch Changes diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index 215fafe970..f4df77da04 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-tasks", "description": "Common distributed task management library for Backstage backends", - "version": "0.1.9", + "version": "0.1.10", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -33,7 +33,7 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", "@backstage/types": "^0.1.3", @@ -47,8 +47,8 @@ "zod": "^3.9.5" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.19", - "@backstage/cli": "^0.14.1", + "@backstage/backend-test-utils": "^0.1.20", + "@backstage/cli": "^0.15.0", "jest": "^26.0.1", "wait-for-expect": "^3.0.2" }, diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 03f564074b..6f1e921875 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/backend-test-utils +## 0.1.20 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + - @backstage/cli@0.15.0 + ## 0.1.19 ### Patch Changes diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 2cc3b8a07b..a12cc67ab4 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-test-utils", "description": "Test helpers library for Backstage backends", - "version": "0.1.19", + "version": "0.1.20", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -34,8 +34,8 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/cli": "^0.14.1", + "@backstage/backend-common": "^0.12.0", + "@backstage/cli": "^0.15.0", "@backstage/config": "^0.1.15", "@vscode/sqlite3": "^5.0.7", "knex": "^1.0.2", @@ -45,7 +45,7 @@ "uuid": "^8.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "jest": "^26.0.1" }, "files": [ diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 227b8ee231..2e483882b9 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,44 @@ # example-backend +## 0.2.67 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/plugin-catalog-backend@0.23.0 + - @backstage/backend-common@0.12.0 + - @backstage/plugin-scaffolder-backend@0.17.3 + - @backstage/plugin-techdocs-backend@0.14.1 + - @backstage/plugin-auth-backend@0.12.0 + - @backstage/plugin-badges-backend@0.1.22 + - @backstage/plugin-code-coverage-backend@0.1.26 + - @backstage/plugin-jenkins-backend@0.1.17 + - @backstage/plugin-todo-backend@0.1.25 + - @backstage/integration@0.8.0 + - @backstage/plugin-permission-common@0.5.2 + - @backstage/plugin-permission-node@0.5.3 + - @backstage/plugin-search-backend-node@0.5.0 + - @backstage/plugin-search-backend-module-pg@0.3.0 + - @backstage/plugin-search-backend-module-elasticsearch@0.1.0 + - @backstage/plugin-tech-insights-backend@0.2.8 + - example-app@0.2.67 + - @backstage/plugin-auth-node@0.1.4 + - @backstage/plugin-kafka-backend@0.2.21 + - @backstage/plugin-kubernetes-backend@0.4.11 + - @backstage/backend-tasks@0.1.10 + - @backstage/plugin-app-backend@0.3.28 + - @backstage/plugin-azure-devops-backend@0.3.7 + - @backstage/plugin-graphql-backend@0.1.18 + - @backstage/plugin-permission-backend@0.5.3 + - @backstage/plugin-proxy-backend@0.2.22 + - @backstage/plugin-rollbar-backend@0.1.25 + - @backstage/plugin-scaffolder-backend-module-rails@0.3.3 + - @backstage/plugin-search-backend@0.4.6 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.12 + - @backstage/plugin-tech-insights-node@0.2.6 + ## 0.2.66 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index cc960e3c24..b49882b837 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.66", + "version": "0.2.67", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,39 +27,39 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/backend-tasks": "^0.1.9", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.12.0", + "@backstage/backend-tasks": "^0.1.10", + "@backstage/catalog-client": "^0.8.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", - "@backstage/integration": "^0.7.5", - "@backstage/plugin-app-backend": "^0.3.27", - "@backstage/plugin-auth-backend": "^0.11.0", - "@backstage/plugin-auth-node": "^0.1.3", - "@backstage/plugin-azure-devops-backend": "^0.3.6", - "@backstage/plugin-badges-backend": "^0.1.21", - "@backstage/plugin-catalog-backend": "^0.22.0", - "@backstage/plugin-code-coverage-backend": "^0.1.25", - "@backstage/plugin-graphql-backend": "^0.1.17", - "@backstage/plugin-jenkins-backend": "^0.1.16", - "@backstage/plugin-kubernetes-backend": "^0.4.10", - "@backstage/plugin-kafka-backend": "^0.2.20", - "@backstage/plugin-permission-backend": "^0.5.2", - "@backstage/plugin-permission-common": "^0.5.1", - "@backstage/plugin-permission-node": "^0.5.2", - "@backstage/plugin-proxy-backend": "^0.2.21", - "@backstage/plugin-rollbar-backend": "^0.1.24", - "@backstage/plugin-scaffolder-backend": "^0.17.0", - "@backstage/plugin-scaffolder-backend-module-rails": "^0.3.2", - "@backstage/plugin-search-backend": "^0.4.5", - "@backstage/plugin-search-backend-node": "^0.4.7", - "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.10", - "@backstage/plugin-search-backend-module-pg": "^0.2.9", - "@backstage/plugin-techdocs-backend": "^0.14.0", - "@backstage/plugin-tech-insights-backend": "^0.2.7", - "@backstage/plugin-tech-insights-node": "^0.2.5", - "@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.11", - "@backstage/plugin-todo-backend": "^0.1.24", + "@backstage/integration": "^0.8.0", + "@backstage/plugin-app-backend": "^0.3.28", + "@backstage/plugin-auth-backend": "^0.12.0", + "@backstage/plugin-auth-node": "^0.1.4", + "@backstage/plugin-azure-devops-backend": "^0.3.7", + "@backstage/plugin-badges-backend": "^0.1.22", + "@backstage/plugin-catalog-backend": "^0.23.0", + "@backstage/plugin-code-coverage-backend": "^0.1.26", + "@backstage/plugin-graphql-backend": "^0.1.18", + "@backstage/plugin-jenkins-backend": "^0.1.17", + "@backstage/plugin-kubernetes-backend": "^0.4.11", + "@backstage/plugin-kafka-backend": "^0.2.21", + "@backstage/plugin-permission-backend": "^0.5.3", + "@backstage/plugin-permission-common": "^0.5.2", + "@backstage/plugin-permission-node": "^0.5.3", + "@backstage/plugin-proxy-backend": "^0.2.22", + "@backstage/plugin-rollbar-backend": "^0.1.25", + "@backstage/plugin-scaffolder-backend": "^0.17.3", + "@backstage/plugin-scaffolder-backend-module-rails": "^0.3.3", + "@backstage/plugin-search-backend": "^0.4.6", + "@backstage/plugin-search-backend-node": "^0.5.0", + "@backstage/plugin-search-backend-module-elasticsearch": "^0.1.0", + "@backstage/plugin-search-backend-module-pg": "^0.3.0", + "@backstage/plugin-techdocs-backend": "^0.14.1", + "@backstage/plugin-tech-insights-backend": "^0.2.8", + "@backstage/plugin-tech-insights-node": "^0.2.6", + "@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.12", + "@backstage/plugin-todo-backend": "^0.1.25", "@gitbeaker/node": "^35.1.0", "@octokit/rest": "^18.5.3", "@vscode/sqlite3": "^5.0.7", @@ -76,7 +76,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/dockerode": "^3.3.0", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5" diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 93f248ff0e..6e1cc280a8 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/catalog-client +## 0.8.0 + +### Minor Changes + +- bb2ba5f10d: **BREAKING**: Removed the old deprecated request/response types: + + - `CatalogEntitiesRequest` - please use `GetEntitiesRequest` instead + - `CatalogEntityAncestorsRequest` - please use `GetEntityAncestorsRequest` instead + - `CatalogEntityAncestorsResponse` - please use `GetEntityAncestorsResponse` instead + - `CatalogListResponse` - please use `GetEntitiesResponse` instead + +### Patch Changes + +- a52f69987a: **DEPRECATION**: Deprecated `getEntityByName` from `CatalogApi` and added `getEntityByRef` instead, which accepts both string and compound ref forms. +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + ## 0.7.2 ### Patch Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index ffcfbc16b7..2eca878e9f 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-client", "description": "An isomorphic client for the catalog backend", - "version": "0.7.2", + "version": "0.8.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,12 +33,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/errors": "^0.2.2", "cross-fetch": "^3.1.5" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/jest": "^26.0.7", "msw": "^0.35.0" }, diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index cbb2e681bf..72d4f8b04a 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/catalog-model +## 0.12.0 + +### Minor Changes + +- ac7b1161a6: **BREAKING**: The following changes are all breaking changes. + + Removed `EDIT_URL_ANNOTATION` and `VIEW_URL_ANNOTATION`, `LOCATION_ANNOTATION`, `ORIGIN_LOCATION_ANNOTATION`, `LOCATION_ANNOTATION`, `SOURCE_LOCATION_ANNOTATION`. All of these constants have been prefixed with ANNOTATION to be easier to find meaning `SOURCE_LOCATION_ANNOTATION` is available as `ANNOTATION_SOURCE_LOCATION`. + + Removed `parseLocationReference`, replaced by `parseLocationRef`. + + Removed `stringifyLocationReference`, replaced by `stringifyLocationRef`. + + Removed `Location` type which has been moved to `catalog-client`. + + Removed `ENTITY_DEFAULT_NAMESPACE`, replaced by `DEFAULT_NAMESPACE`. + + Removed `compareEntityToRef` compare using `stringifyEntityRef` instead. + + Removed `JSONSchema` type which should be imported from `json-schema` package instead. + + Removed utility methods: `entityHasChanges`, `generateEntityEtag`, `generateEntityUid`, `generateUpdatedEntity`. + + Removed `ENTITY_META_GENERATED_FIELDS` and `EntityRefContext`. + +### Patch Changes + +- debfcd9515: Move `@types/json-schema` to be a dev dependency +- 36aa63022b: **DEPRECATION**: Deprecated the `EntityName` type, and added the better-named `CompoundEntityRef` to replace it. + + **DEPRECATION**: Deprecated the `getEntityName` function, and added the better-named `getCompoundEntityRef` to replace it. + + Please switch over to using the new symbols, as the old ones may be removed in a future release. + ## 0.11.0 ### Minor Changes diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index f9781b14a2..32416f7bb0 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-model", "description": "Types and validators that help describe the model of a Backstage Catalog", - "version": "0.11.0", + "version": "0.12.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -43,7 +43,7 @@ "uuid": "^8.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/jest": "^26.0.7", "@types/json-schema": "^7.0.5", "@types/lodash": "^4.14.151", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 46b67615c4..cfae3cab06 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/cli +## 0.15.0 + +### Minor Changes + +- 8c3f30cb28: **BREAKING**: Removed the deprecated `app.` template variables from the `index.html` templating. These should be replaced by using `config.getString("app.")` instead. + +### Patch Changes + +- 46a19c599f: The CLI now bundles both version 16 and 17 of the patched `@hot-loader/react-dom` dependency, and selects the appropriate one based on what version of `react-dom` is installed within the app. + ## 0.14.1 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index e5551ce29e..26b941c699 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.14.1", + "version": "0.15.0", "private": false, "publishConfig": { "access": "public" @@ -122,13 +122,13 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.12.0", "@backstage/config": "^0.1.15", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@backstage/theme": "^0.2.15", "@types/diff": "^5.0.0", "@types/express": "^4.17.6", diff --git a/packages/config/package.json b/packages/config/package.json index 63edfe0360..6b6bac4d3c 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -37,7 +37,7 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/test-utils": "^0.2.5", + "@backstage/test-utils": "^0.3.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index f40ea23f73..f286c3d787 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/core-app-api +## 0.6.0 + +### Minor Changes + +- bb2bb36651: **BREAKING**: Removed the deprecated `get` method from `StorageAPI` and its implementations, this method has been replaced by the `snapshot` method. The return value from snapshot no longer includes `newValue` which has been replaced by `value`. For getting notified when a value changes, use `observe# @backstage/core-app-api. +- f3cce3dcf7: **BREAKING**: Removed export of `GithubSession` and `SamlSession` which are only used internally. +- af5eaa87f4: **BREAKING**: Removed deprecated `auth0AuthApiRef`, `oauth2ApiRef`, `samlAuthApiRef` and `oidcAuthApiRef` as these APIs are too generic to be useful. Instructions for how to migrate can be found at [https://backstage.io/docs/api/deprecations#generic-auth-api-refs](https://backstage.io/docs/api/deprecations#generic-auth-api-refs). +- dbf84eee55: **BREAKING**: Removed the deprecated `GithubAuth.normalizeScopes` method. + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@0.8.0 + ## 0.5.4 ### Patch Changes diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 6f74bf95d6..57ee14e1d0 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "0.5.4", + "version": "0.6.0", "private": false, "publishConfig": { "access": "public", @@ -34,7 +34,7 @@ }, "dependencies": { "@backstage/config": "^0.1.15", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/types": "^0.1.3", "@backstage/version-bridge": "^0.1.2", "@types/prop-types": "^15.7.3", @@ -49,8 +49,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 5d8211d303..65407daef6 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/core-components +## 0.9.0 + +### Minor Changes + +- af5eaa87f4: **BREAKING**: Removed deprecated `auth0AuthApiRef`, `oauth2ApiRef`, `samlAuthApiRef` and `oidcAuthApiRef` as these APIs are too generic to be useful. Instructions for how to migrate can be found at [https://backstage.io/docs/api/deprecations#generic-auth-api-refs](https://backstage.io/docs/api/deprecations#generic-auth-api-refs). + +### Patch Changes + +- 64b430f80d: chore(deps): bump `react-text-truncate` from 0.17.0 to 0.18.0 +- bb2bb36651: Updated usage of `StorageApi` to use `snapshot` method instead of `get` +- 689840dcbe: Added ability for SidebarSubmenuItem to handle external links correctly via the "to" prop +- Updated dependencies + - @backstage/core-plugin-api@0.8.0 + ## 0.8.10 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 87d9f78fcc..93fa015b89 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.8.10", + "version": "0.9.0", "private": false, "publishConfig": { "access": "public", @@ -34,7 +34,7 @@ }, "dependencies": { "@backstage/config": "^0.1.15", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", "@backstage/theme": "^0.2.15", "@material-table/core": "^3.1.0", @@ -78,9 +78,9 @@ "react-dom": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/core-app-api": "^0.5.4", - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/core-app-api": "^0.6.0", + "@backstage/cli": "^0.15.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index bba39bf2cf..3702de3474 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/core-plugin-api +## 0.8.0 + +### Minor Changes + +- bb2bb36651: **BREAKING**: Removed the deprecated `get` method from `StorageAPI` and its implementations, this method has been replaced by the `snapshot` method. The return value from snapshot no longer includes `newValue` which has been replaced by `value`. For getting notified when a value changes, use `observe# @backstage/core-plugin-api. +- af5eaa87f4: **BREAKING**: Removed deprecated `auth0AuthApiRef`, `oauth2ApiRef`, `samlAuthApiRef` and `oidcAuthApiRef` as these APIs are too generic to be useful. Instructions for how to migrate can be found at [https://backstage.io/docs/api/deprecations#generic-auth-api-refs](https://backstage.io/docs/api/deprecations#generic-auth-api-refs). +- a480f670c7: **BREAKING**: OAuth provider id is now required when passing a provider to `createAuthRequester`. + ## 0.7.0 ### Minor Changes diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 4e9198557b..50a87056f6 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-plugin-api", "description": "Core API used by Backstage plugins", - "version": "0.7.0", + "version": "0.8.0", "private": false, "publishConfig": { "access": "public", @@ -46,9 +46,9 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 52dad1bfc9..cd4fde3851 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,86 @@ # @backstage/create-app +## 0.4.22 + +### Patch Changes + +- ee3d6c6f10: Update the template to reflect the renaming of `DocsResultListItem` to `TechDocsSearchResultListItem` from `@backstage/plugin-techdocs`. + + To apply this change to an existing app, make the following change to `packages/app/src/components/search/SearchPage.tsx`: + + ```diff + -import { DocsResultListItem } from '@backstage/plugin-techdocs'; + +import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; + ``` + + ```diff + case 'techdocs': + return ( + - + ``` + + The `TechDocsIndexPage` now uses `DefaultTechDocsHome` as fall back if no children is provided as `LegacyTechDocsHome` is marked as deprecated. If you do not use a custom techdocs homepage, you can therefore update your app to the following: + + ```diff + - }> + - + - + + } /> + ``` + +- 617a132871: Update import location of catalogEntityCreatePermission. + + To apply this change to an existing app, make the following change to `packages/app/src/App.tsx`: + + ```diff + -import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; + +import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; + ``` + +- 022507c860: The Backstage Search Platform's indexing process has been rewritten as a stream + pipeline in order to improve efficiency and performance on large document sets. + + To take advantage of this, upgrade to the latest version of + `@backstage/plugin-search-backend-node`, as well as any backend plugins whose + collators you are using. Then, make the following changes to your + `/packages/backend/src/plugins/search.ts` file: + + ```diff + -import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend'; + -import { DefaultTechDocsCollator } from '@backstage/plugin-techdocs-backend'; + +import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; + +import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; + + // ... + + const indexBuilder = new IndexBuilder({ logger, searchEngine }); + + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + - collator: DefaultCatalogCollator.fromConfig(config, { discovery }), + + factory: DefaultCatalogCollatorFactory.fromConfig(config, { discovery }), + }); + + indexBuilder.addCollator({ + defaultRefreshIntervalSeconds: 600, + - collator: DefaultTechDocsCollator.fromConfig(config, { + + factory: DefaultTechDocsCollatorFactory.fromConfig(config, { + discovery, + logger, + }), + }); + ``` + + If you've written custom collators, decorators, or search engines in your + Backstage backend instance, you will need to re-implement them as readable, + transform, and writable streams respectively (including factory classes for + instantiating them). [A how-to guide for refactoring](https://backstage.io/docs/features/search/how-to-guides#rewriting-alpha-style-collators-for-beta) + existing implementations is available. + ## 0.4.21 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index a240e95cac..975d2cee7e 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.4.21", + "version": "0.4.22", "private": false, "publishConfig": { "access": "public" diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 4d2354297e..72717e3891 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/dev-utils +## 0.2.24 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-app-api@0.6.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/test-utils@0.3.0 + - @backstage/app-defaults@0.2.0 + - @backstage/integration-react@0.1.24 + ## 0.2.23 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 5c29c7dd0f..60876d47e0 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.2.23", + "version": "0.2.24", "private": false, "publishConfig": { "access": "public", @@ -33,14 +33,14 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/app-defaults": "^0.1.9", - "@backstage/core-app-api": "^0.5.4", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/catalog-model": "^0.11.0", - "@backstage/integration-react": "^0.1.23", - "@backstage/plugin-catalog-react": "^0.7.0", - "@backstage/test-utils": "^0.2.6", + "@backstage/app-defaults": "^0.2.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/integration-react": "^0.1.24", + "@backstage/plugin-catalog-react": "^0.8.0", + "@backstage/test-utils": "^0.3.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -59,7 +59,7 @@ "react-dom": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/errors/package.json b/packages/errors/package.json index b1c90f0371..e612ade21b 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -38,7 +38,7 @@ "serialize-error": "^8.0.1" }, "devDependencies": { - "@backstage/cli": "^0.14.0", + "@backstage/cli": "^0.15.0", "@types/jest": "^26.0.7" }, "files": [ diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index 1b8f22bcfc..4ea916b8bd 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/integration-react +## 0.1.24 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/integration@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.23 ### Patch Changes diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 27e4492e65..c929b2799d 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration-react", "description": "Frontend package for managing integrations towards external systems", - "version": "0.1.23", + "version": "0.1.24", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,9 +25,9 @@ }, "dependencies": { "@backstage/config": "^0.1.15", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/integration": "^0.7.5", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/integration": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -38,9 +38,9 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index af764c3b69..faac399f63 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/integration +## 0.8.0 + +### Minor Changes + +- 34af86517c: ensure `apiBaseUrl` being set for Bitbucket integrations, replace hardcoded defaults + +### Patch Changes + +- 33d5e79822: Fix Bitbucket Cloud and Bitbucket Server line number reference. + ## 0.7.5 ### Patch Changes diff --git a/packages/integration/package.json b/packages/integration/package.json index bf7c167bfa..de232307ad 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration", "description": "Helpers for managing integrations towards external systems", - "version": "0.7.5", + "version": "0.8.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -42,9 +42,9 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@backstage/config-loader": "^0.9.6", - "@backstage/test-utils": "^0.2.6", + "@backstage/test-utils": "^0.3.0", "@types/jest": "^26.0.7", "@types/luxon": "^2.0.4", "msw": "^0.35.0" diff --git a/packages/release-manifests/package.json b/packages/release-manifests/package.json index 7ec69a35e9..7959a17d0c 100644 --- a/packages/release-manifests/package.json +++ b/packages/release-manifests/package.json @@ -36,7 +36,7 @@ "cross-fetch": "^3.1.5" }, "devDependencies": { - "@backstage/test-utils": "^0.2.5", + "@backstage/test-utils": "^0.3.0", "msw": "^0.35.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" diff --git a/packages/search-common/CHANGELOG.md b/packages/search-common/CHANGELOG.md index 71f2952d90..112d3d583a 100644 --- a/packages/search-common/CHANGELOG.md +++ b/packages/search-common/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/search-common +## 0.3.0 + +### Minor Changes + +- 022507c860: **BREAKING** + + The Backstage Search Platform's indexing process has been rewritten as a stream + pipeline in order to improve efficiency and performance on large document sets. + + The concepts of `Collator` and `Decorator` have been replaced with readable and + transform object streams (respectively), as well as factory classes to + instantiate them. Accordingly, the `SearchEngine.index()` method has also been + replaced with a `getIndexer()` factory method that resolves to a writable + object stream. + + Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) + for further details. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.5.2 + ## 0.2.4 ### Patch Changes diff --git a/packages/search-common/package.json b/packages/search-common/package.json index ef56017405..0e4482e377 100644 --- a/packages/search-common/package.json +++ b/packages/search-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/search-common", "description": "Common functionalities for Search, to be shared between various search-enabled plugins", - "version": "0.2.4", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -40,10 +40,10 @@ }, "dependencies": { "@backstage/types": "^0.1.3", - "@backstage/plugin-permission-common": "^0.5.1" + "@backstage/plugin-permission-common": "^0.5.2" }, "devDependencies": { - "@backstage/cli": "^0.14.0" + "@backstage/cli": "^0.15.0" }, "jest": { "roots": [ diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index 1302d1ca9d..50b2c11542 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,21 @@ # techdocs-cli-embedded-app +## 0.2.66 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-techdocs@0.15.0 + - @backstage/plugin-catalog@0.9.1 + - @backstage/core-app-api@0.6.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/test-utils@0.3.0 + - @backstage/cli@0.15.0 + - @backstage/app-defaults@0.2.0 + - @backstage/integration-react@0.1.24 + ## 0.2.65 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index 6719701378..81ad2a1bc4 100644 --- a/packages/techdocs-cli-embedded-app/package.json +++ b/packages/techdocs-cli-embedded-app/package.json @@ -1,23 +1,23 @@ { "name": "techdocs-cli-embedded-app", - "version": "0.2.65", + "version": "0.2.66", "private": true, "backstage": { "role": "frontend" }, "bundled": true, "dependencies": { - "@backstage/app-defaults": "^0.1.9", - "@backstage/catalog-model": "^0.11.0", - "@backstage/cli": "^0.14.1", + "@backstage/app-defaults": "^0.2.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/cli": "^0.15.0", "@backstage/config": "^0.1.15", - "@backstage/core-app-api": "^0.5.4", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/integration-react": "^0.1.23", - "@backstage/plugin-catalog": "^0.9.0", - "@backstage/plugin-techdocs": "^0.14.0", - "@backstage/test-utils": "^0.2.6", + "@backstage/core-app-api": "^0.6.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/integration-react": "^0.1.24", + "@backstage/plugin-catalog": "^0.9.1", + "@backstage/plugin-techdocs": "^0.15.0", + "@backstage/test-utils": "^0.3.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -29,7 +29,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index bc23499662..aa360b1781 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,15 @@ # @techdocs/cli +## 0.8.16 + +### Patch Changes + +- 853efd42bd: Bump `@backstage/techdocs-common` to `0.11.10` to use `spotify/techdocs:v0.3.7` which upgrades `mkdocs-theme` as a dependency of `mkdocs-techdocs-core`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/backend-common@0.12.0 + - @backstage/techdocs-common@0.11.11 + ## 0.8.15 ### Patch Changes diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 929564e01c..32eb70602c 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,7 +1,7 @@ { "name": "@techdocs/cli", "description": "Utility CLI for managing TechDocs sites in Backstage.", - "version": "0.8.15", + "version": "0.8.16", "private": false, "publishConfig": { "access": "public" @@ -37,7 +37,7 @@ "techdocs-cli": "bin/techdocs-cli" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/commander": "^2.12.2", "@types/fs-extra": "^9.0.6", "@types/http-proxy": "^1.17.4", @@ -62,11 +62,11 @@ "ext": "ts" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.12.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/cli-common": "^0.1.8", "@backstage/config": "^0.1.15", - "@backstage/techdocs-common": "^0.11.10", + "@backstage/techdocs-common": "^0.11.11", "@types/dockerode": "^3.3.0", "commander": "^6.1.0", "dockerode": "^3.3.1", diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index c99295e8db..f41c944a89 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/techdocs-common +## 0.11.11 + +### Patch Changes + +- 955be6bc7d: adds passing projectID to the Storage client +- ff0a16fb1a: Modify techdocs builder to automatically append techdocs-core plugin to mkdocs.yaml file if it is missing. Adds an optional configuration item if this plugin needs to be omitted. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/backend-common@0.12.0 + - @backstage/integration@0.8.0 + - @backstage/search-common@0.3.0 + ## 0.11.10 ### Patch Changes diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 731e967637..04e13065c8 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/techdocs-common", "description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "0.11.10", + "version": "0.11.11", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -42,12 +42,12 @@ "dependencies": { "@azure/identity": "^2.0.1", "@azure/storage-blob": "^12.5.0", - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.12.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/search-common": "^0.2.4", - "@backstage/integration": "^0.7.5", + "@backstage/search-common": "^0.3.0", + "@backstage/integration": "^0.8.0", "@google-cloud/storage": "^5.6.0", "@trendyol-js/openstack-swift-sdk": "^0.0.5", "@types/express": "^4.17.6", @@ -64,7 +64,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/fs-extra": "^9.0.5", "@types/js-yaml": "^4.0.0", "@types/mime-types": "^2.1.0", diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index 4de90a4aa7..2f9deae04f 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/test-utils +## 0.3.0 + +### Minor Changes + +- bb2bb36651: **BREAKING**: Removed the deprecated `get` method from `StorageAPI` and its implementations, this method has been replaced by the `snapshot` method. The return value from snapshot no longer includes `newValue` which has been replaced by `value`. For getting notified when a value changes, use `observe# @backstage/test-utils. +- af5eaa87f4: **BREAKING**: Removed deprecated `auth0AuthApiRef`, `oauth2ApiRef`, `samlAuthApiRef` and `oidcAuthApiRef` as these APIs are too generic to be useful. Instructions for how to migrate can be found at [https://backstage.io/docs/api/deprecations#generic-auth-api-refs](https://backstage.io/docs/api/deprecations#generic-auth-api-refs). + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@0.6.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/plugin-permission-common@0.5.2 + - @backstage/plugin-permission-react@0.3.3 + ## 0.2.6 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 5465f7783a..baf4e0dd37 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.2.6", + "version": "0.3.0", "private": false, "publishConfig": { "access": "public", @@ -34,10 +34,10 @@ }, "dependencies": { "@backstage/config": "^0.1.15", - "@backstage/core-app-api": "^0.5.4", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-permission-common": "^0.5.1", - "@backstage/plugin-permission-react": "^0.3.2", + "@backstage/core-app-api": "^0.6.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-permission-common": "^0.5.2", + "@backstage/plugin-permission-react": "^0.3.3", "@backstage/theme": "^0.2.15", "@backstage/types": "^0.1.3", "@material-ui/core": "^4.12.2", @@ -55,7 +55,7 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "msw": "^0.35.0" diff --git a/packages/theme/package.json b/packages/theme/package.json index 17404e8db0..db54e495e6 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -36,7 +36,7 @@ "@material-ui/core": "^4.12.2" }, "devDependencies": { - "@backstage/cli": "^0.14.0" + "@backstage/cli": "^0.15.0" }, "files": [ "dist" diff --git a/packages/types/package.json b/packages/types/package.json index ab0740d568..c71fbd9d47 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -34,7 +34,7 @@ }, "dependencies": {}, "devDependencies": { - "@backstage/cli": "^0.14.0", + "@backstage/cli": "^0.15.0", "@types/zen-observable": "^0.8.0", "zen-observable": "^0.8.15" }, diff --git a/packages/version-bridge/package.json b/packages/version-bridge/package.json index 485b0417cf..377f08621f 100644 --- a/packages/version-bridge/package.json +++ b/packages/version-bridge/package.json @@ -37,7 +37,7 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.0", + "@backstage/cli": "^0.15.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2" diff --git a/plugins/airbrake-backend/CHANGELOG.md b/plugins/airbrake-backend/CHANGELOG.md index 148dbdeb07..8726260060 100644 --- a/plugins/airbrake-backend/CHANGELOG.md +++ b/plugins/airbrake-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-airbrake-backend +## 0.2.1 + +### Patch Changes + +- 3c1d3cb07e: The Airbrake plugin installation instructions have been updated to work better and conform to how the frontend and backend plugins are supposed to be integrated into a Backstage instance. +- Updated dependencies + - @backstage/backend-common@0.12.0 + ## 0.2.0 ### Minor Changes diff --git a/plugins/airbrake-backend/package.json b/plugins/airbrake-backend/package.json index 2ee36eefb7..831578dd9e 100644 --- a/plugins/airbrake-backend/package.json +++ b/plugins/airbrake-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-airbrake-backend", - "version": "0.2.0", + "version": "0.2.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,7 +19,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.12.0", "@backstage/config": "^0.1.15", "@types/express": "*", "express": "^4.17.1", @@ -30,7 +30,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/http-proxy-middleware": "^0.19.3", "@types/supertest": "^2.0.8", "supertest": "^6.1.6", diff --git a/plugins/airbrake/CHANGELOG.md b/plugins/airbrake/CHANGELOG.md index 685ef4c0e7..0e458b9522 100644 --- a/plugins/airbrake/CHANGELOG.md +++ b/plugins/airbrake/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-airbrake +## 0.3.1 + +### Patch Changes + +- 3c1d3cb07e: The Airbrake plugin installation instructions have been updated to work better and conform to how the frontend and backend plugins are supposed to be integrated into a Backstage instance. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/test-utils@0.3.0 + - @backstage/dev-utils@0.2.24 + ## 0.3.0 ### Minor Changes diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index 7342fdd839..5ccd9033d5 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-airbrake", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,12 +23,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/dev-utils": "^0.2.23", - "@backstage/plugin-catalog-react": "^0.7.0", - "@backstage/test-utils": "^0.2.6", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/plugin-catalog-react": "^0.8.0", + "@backstage/test-utils": "^0.3.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -40,10 +40,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/app-defaults": "^0.1.9", - "@backstage/cli": "^0.14.1", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/app-defaults": "^0.2.0", + "@backstage/cli": "^0.15.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md index 43e83e2d1e..70050127a7 100644 --- a/plugins/allure/CHANGELOG.md +++ b/plugins/allure/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-allure +## 0.1.17 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.16 ### Patch Changes diff --git a/plugins/allure/package.json b/plugins/allure/package.json index eaa3013da8..f5f9f81025 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-allure", "description": "A Backstage plugin that integrates with Allure", - "version": "0.1.16", + "version": "0.1.17", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,10 +25,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -40,10 +40,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/analytics-module-ga/CHANGELOG.md b/plugins/analytics-module-ga/CHANGELOG.md index ccf5ebd98c..aed1945ca5 100644 --- a/plugins/analytics-module-ga/CHANGELOG.md +++ b/plugins/analytics-module-ga/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-analytics-module-ga +## 0.1.12 + +### Patch Changes + +- 123019673b: Added CSP instructions to README +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.11 ### Patch Changes diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index d71b66cd6f..07dbcc91ba 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-analytics-module-ga", - "version": "0.1.11", + "version": "0.1.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,8 +25,8 @@ }, "dependencies": { "@backstage/config": "^0.1.15", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -38,10 +38,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/apache-airflow/CHANGELOG.md b/plugins/apache-airflow/CHANGELOG.md index e86a6cef5d..968ecefc71 100644 --- a/plugins/apache-airflow/CHANGELOG.md +++ b/plugins/apache-airflow/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-apache-airflow +## 0.1.9 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.8 ### Patch Changes diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json index b5045551bb..4de1e87d28 100644 --- a/plugins/apache-airflow/package.json +++ b/plugins/apache-airflow/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-apache-airflow", - "version": "0.1.8", + "version": "0.1.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,8 +23,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -36,10 +36,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 6bbbe9d056..bca082d38e 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-api-docs +## 0.8.1 + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog@0.9.1 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.8.0 ### Minor Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 331845bd13..b21b5aff9f 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-api-docs", "description": "A Backstage plugin that helps represent API entities in the frontend", - "version": "0.8.0", + "version": "0.8.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,11 +34,11 @@ }, "dependencies": { "@asyncapi/react-component": "1.0.0-next.33", - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog": "^0.9.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog": "^0.9.1", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -56,10 +56,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index baf9a928d8..0763fd7995 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-app-backend +## 0.3.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + ## 0.3.27 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 79682ca9de..aaad6da545 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-app-backend", "description": "A Backstage backend plugin that serves the Backstage frontend app", - "version": "0.3.27", + "version": "0.3.28", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,7 +33,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.12.0", "@backstage/config-loader": "^0.9.6", "@backstage/config": "^0.1.15", "@backstage/types": "^0.1.3", @@ -50,8 +50,8 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.19", - "@backstage/cli": "^0.14.1", + "@backstage/backend-test-utils": "^0.1.20", + "@backstage/cli": "^0.15.0", "@backstage/types": "^0.1.3", "@types/supertest": "^2.0.8", "mock-fs": "^5.1.0", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 5f85363fcc..f9d05aecc1 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-auth-backend +## 0.12.0 + +### Minor Changes + +- 0c8ba31d72: **BREAKING**: The `TokenFactory.issueToken` used by custom sign-in resolvers now ensures that the sub claim given is a full entity reference of the format `:/`. Any existing custom sign-in resolver functions that do not supply a full entity reference must be updated. + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/backend-common@0.12.0 + - @backstage/plugin-auth-node@0.1.4 + ## 0.11.0 ### Minor Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 7a370ee8a9..49cff317e3 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend", "description": "A Backstage backend plugin that handles authentication", - "version": "0.11.0", + "version": "0.12.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,10 +33,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/plugin-auth-node": "^0.1.3", - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/plugin-auth-node": "^0.1.4", + "@backstage/backend-common": "^0.12.0", + "@backstage/catalog-client": "^0.8.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", "@backstage/types": "^0.1.3", @@ -76,8 +76,8 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/test-utils": "^0.3.0", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md index 7676ce7afa..5cf6aebac9 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-auth-node +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/backend-common@0.12.0 + ## 0.1.3 ### Patch Changes diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index 2becee849d..1c80b6ca8a 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-node", - "version": "0.1.3", + "version": "0.1.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,8 +23,8 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.12.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", "jose": "^1.27.1", @@ -32,7 +32,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "msw": "^0.35.0", "uuid": "^8.0.0" }, diff --git a/plugins/azure-devops-backend/CHANGELOG.md b/plugins/azure-devops-backend/CHANGELOG.md index 0156f92891..8d473a84c7 100644 --- a/plugins/azure-devops-backend/CHANGELOG.md +++ b/plugins/azure-devops-backend/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-azure-devops-backend +## 0.3.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + ## 0.3.6 ### Patch Changes diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index 206294c863..73a7dd57e2 100644 --- a/plugins/azure-devops-backend/package.json +++ b/plugins/azure-devops-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops-backend", - "version": "0.3.6", + "version": "0.3.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,7 +23,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/plugin-azure-devops-common": "^0.2.2", "@types/express": "^4.17.6", @@ -35,7 +35,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/supertest": "^2.0.8", "supertest": "^6.1.6", "msw": "^0.35.0" diff --git a/plugins/azure-devops-common/package.json b/plugins/azure-devops-common/package.json index 7810469bf9..b3755f40e6 100644 --- a/plugins/azure-devops-common/package.json +++ b/plugins/azure-devops-common/package.json @@ -32,7 +32,7 @@ "clean": "backstage-cli package clean" }, "devDependencies": { - "@backstage/cli": "^0.14.0" + "@backstage/cli": "^0.15.0" }, "files": [ "dist" diff --git a/plugins/azure-devops/CHANGELOG.md b/plugins/azure-devops/CHANGELOG.md index 7c0a533dd3..1d120382f1 100644 --- a/plugins/azure-devops/CHANGELOG.md +++ b/plugins/azure-devops/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-azure-devops +## 0.1.17 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.16 ### Patch Changes diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index 57c1654527..ae90a2c42d 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops", - "version": "0.1.16", + "version": "0.1.17", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,12 +30,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", "@backstage/plugin-azure-devops-common": "^0.2.2", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -49,10 +49,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md index ae93cdbe5a..46c51d8833 100644 --- a/plugins/badges-backend/CHANGELOG.md +++ b/plugins/badges-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-badges-backend +## 0.1.22 + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/backend-common@0.12.0 + ## 0.1.21 ### Patch Changes diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index 94796ba665..384c22fb5a 100644 --- a/plugins/badges-backend/package.json +++ b/plugins/badges-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges-backend", "description": "A Backstage backend plugin that generates README badges for your entities", - "version": "0.1.21", + "version": "0.1.22", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,9 +34,9 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.12.0", + "@backstage/catalog-client": "^0.8.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", "@types/express": "^4.17.6", @@ -48,7 +48,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/badges/CHANGELOG.md b/plugins/badges/CHANGELOG.md index e624dc36e9..0e257e78ac 100644 --- a/plugins/badges/CHANGELOG.md +++ b/plugins/badges/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-badges +## 0.2.25 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.2.24 ### Patch Changes diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 612b9f3b2d..e0d756d990 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges", "description": "A Backstage plugin that generates README badges for your entities", - "version": "0.2.24", + "version": "0.2.25", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,11 +30,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -46,10 +46,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/bazaar-backend/CHANGELOG.md b/plugins/bazaar-backend/CHANGELOG.md index 50dc8b2ad3..10de02dfa9 100644 --- a/plugins/bazaar-backend/CHANGELOG.md +++ b/plugins/bazaar-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-bazaar-backend +## 0.1.12 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + - @backstage/backend-test-utils@0.1.20 + ## 0.1.11 ### Patch Changes diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json index cb596770d4..c9b1af2139 100644 --- a/plugins/bazaar-backend/package.json +++ b/plugins/bazaar-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar-backend", - "version": "0.1.11", + "version": "0.1.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,8 +23,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/backend-test-utils": "^0.1.19", + "@backstage/backend-common": "^0.12.0", + "@backstage/backend-test-utils": "^0.1.20", "@backstage/config": "^0.1.15", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -34,7 +34,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1" + "@backstage/cli": "^0.15.0" }, "files": [ "dist", diff --git a/plugins/bazaar/CHANGELOG.md b/plugins/bazaar/CHANGELOG.md index df72f76d6a..779baa5650 100644 --- a/plugins/bazaar/CHANGELOG.md +++ b/plugins/bazaar/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-bazaar +## 0.1.16 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog@0.9.1 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/cli@0.15.0 + ## 0.1.15 ### Patch Changes diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index a38cd1cd16..961ce38460 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar", - "version": "0.1.15", + "version": "0.1.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,13 +24,13 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", - "@backstage/cli": "^0.14.1", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog": "^0.9.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-client": "^0.8.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/cli": "^0.15.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog": "^0.9.1", + "@backstage/plugin-catalog-react": "^0.8.0", "@date-io/luxon": "1.x", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -47,8 +47,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/dev-utils": "^0.2.23", + "@backstage/cli": "^0.15.0", + "@backstage/dev-utils": "^0.2.24", "@testing-library/jest-dom": "^5.10.1", "cross-fetch": "^3.1.5" }, diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md index 8af11aa8a4..551f04310a 100644 --- a/plugins/bitrise/CHANGELOG.md +++ b/plugins/bitrise/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-bitrise +## 0.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.27 ### Patch Changes diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 228eed3e9b..dc5c71d5d7 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-bitrise", "description": "A Backstage plugin that integrates towards Bitrise", - "version": "0.1.27", + "version": "0.1.28", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,10 +24,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -43,10 +43,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index 749ab3a230..9ef634a3d1 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.1.1 + +### Patch Changes + +- 83a83381b0: Use the new `processingResult` export from the catalog backend +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/plugin-catalog-backend@0.23.0 + ## 0.1.0 ### Minor Changes diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index ebe77263ac..2ea57652cc 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-aws", "description": "A Backstage catalog backend module that helps integrate towards AWS", - "version": "0.1.0", + "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,17 +33,17 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/plugin-catalog-backend": "^0.22.0", + "@backstage/plugin-catalog-backend": "^0.23.0", "@backstage/types": "^0.1.3", "aws-sdk": "^2.840.0", "lodash": "^4.17.21", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/lodash": "^4.14.151", "aws-sdk-mock": "^5.2.1" }, diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index e05e4936a8..94af4a78e9 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.3.15 + +### Patch Changes + +- 83a83381b0: Use the new `processingResult` export from the catalog backend +- 66aa05c23c: Fixed bug in Catalog LDAP module to acknowledge page events to continue receiving entries if pagePause=true +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/plugin-catalog-backend@0.23.0 + ## 0.3.14 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index dc2f56aad6..8f05dceb4e 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", "description": "A Backstage catalog backend module that helps integrate towards LDAP", - "version": "0.3.14", + "version": "0.3.15", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,10 +33,10 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/plugin-catalog-backend": "^0.22.0", + "@backstage/plugin-catalog-backend": "^0.23.0", "@backstage/types": "^0.1.3", "@types/ldapjs": "^2.2.0", "ldapjs": "^2.2.0", @@ -44,7 +44,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/lodash": "^4.14.151" }, "files": [ diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index d2e9facfb5..1c24d03bea 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.2.18 + +### Patch Changes + +- c820a49426: add config option `groupExpand` to allow expanding a single relationship +- 83a83381b0: Use the new `processingResult` export from the catalog backend +- 4bc61a64e2: add documentation for config options `userGroupMemberSearch` and `groupSearch` +- f9bb6aa0aa: add `userExpand` config option to allow expanding a single relationship +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/plugin-catalog-backend@0.23.0 + ## 0.2.17 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 5005a16c5b..463b0435c8 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", - "version": "0.2.17", + "version": "0.2.18", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,9 +34,9 @@ }, "dependencies": { "@azure/msal-node": "^1.1.0", - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", - "@backstage/plugin-catalog-backend": "^0.22.0", + "@backstage/plugin-catalog-backend": "^0.23.0", "@microsoft/microsoft-graph-types": "^2.6.0", "@types/node-fetch": "^2.5.12", "lodash": "^4.17.21", @@ -46,9 +46,9 @@ "qs": "^6.9.4" }, "devDependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/backend-common": "^0.12.0", + "@backstage/cli": "^0.15.0", + "@backstage/test-utils": "^0.3.0", "@types/lodash": "^4.14.151", "msw": "^0.35.0" }, diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index d80acc6d23..fb197a65f5 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,57 @@ # @backstage/plugin-catalog-backend +## 0.23.0 + +### Minor Changes + +- 0c9cf2822d: **Breaking**: Mark permission-related exports as alpha. This means that the exports below should now be imported from `@backstage/plugin-catalog-backend/alpha` instead of `@backstage/plugin-catalog-backend`. + + - `catalogConditions` + - `createCatalogPolicyDecision` + - `permissionRules` + - `createCatalogPermissionRule` + +- 862e416239: **Breaking**: Removed `entityRef` from `CatalogProcessorRelationResult`. The field is not used by the catalog and relation information is already available inside the `reation` property. +- c85292b768: **Breaking**: Removed optional `handleError()` from `CatalogProcessor`. This optional method is never called by the catalog processing engine and can therefore be removed. + +### Patch Changes + +- 83a83381b0: **DEPRECATED**: The `results` export, and instead adding `processingResult` with the same shape and purpose. +- 83a83381b0: Internal restructuring to collect the various provider files in a `modules` folder while waiting to be externalized +- fc6d31b5c3: Deprecated the `BitbucketRepositoryParser` type. +- 022507c860: A `DefaultCatalogCollatorFactory`, which works with the new stream-based + search indexing subsystem, is now available. The `DefaultCatalogCollator` will + continue to be available for those unable to upgrade to the stream-based + `@backstage/search-backend-node` (and related packages), however it is now + marked as deprecated and will be removed in a future version. + + To upgrade this plugin and the search indexing subsystem in one go, check + [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) + for necessary changes to your search backend plugin configuration. + +- ab7b6cb7b1: **DEPRECATION**: Moved the `CatalogEntityDocument` to `@backstage/plugin-catalog-common` and deprecated the export from `@backstage/plugin-catalog-backend`. + + A new `type` field has also been added to `CatalogEntityDocument` as a replacement for `componentType`, which is now deprecated. Both fields are still present and should be set to the same value in order to avoid issues with indexing. + + Any search customizations need to be updated to use this new `type` field instead, including any custom frontend filters, custom frontend result components, custom search decorators, or non-default Catalog collator implementations. + +- cb09096607: Tweaked the wording of the "does not have a location" errors to include the actual missing annotation name, to help users better in fixing their inputs. +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- b753d22a56: **DEPRECATION**: Deprecated the `RefreshIntervalFunction` and `createRandomRefreshInterval` in favour of the `ProcessingIntervalFunction` and `createRandomProcessingInterval` type and method respectively. Please migrate to use the new names. + + **DEPRECATION**: Deprecated the `setRefreshInterval` and `setRefreshIntervalSeconds` methods on the `CatalogBuilder` for the new `setProcessingInterval` and `setProcessingIntervalSeconds` methods. Please migrate to use the new names. + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/backend-common@0.12.0 + - @backstage/plugin-catalog-common@0.2.0 + - @backstage/integration@0.8.0 + - @backstage/plugin-permission-common@0.5.2 + - @backstage/plugin-permission-node@0.5.3 + - @backstage/search-common@0.3.0 + - @backstage/plugin-scaffolder-common@0.2.3 + ## 0.22.0 ### Minor Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index f7d946c531..a1404189f8 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend", "description": "The Backstage backend plugin that provides the Backstage catalog", - "version": "0.22.0", + "version": "0.23.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,17 +34,17 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.12.0", + "@backstage/catalog-client": "^0.8.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", - "@backstage/plugin-catalog-common": "^0.1.4", - "@backstage/plugin-permission-common": "^0.5.1", - "@backstage/plugin-permission-node": "^0.5.2", - "@backstage/plugin-scaffolder-common": "^0.2.2", - "@backstage/search-common": "^0.2.4", + "@backstage/integration": "^0.8.0", + "@backstage/plugin-catalog-common": "^0.2.0", + "@backstage/plugin-permission-common": "^0.5.2", + "@backstage/plugin-permission-node": "^0.5.3", + "@backstage/plugin-scaffolder-common": "^0.2.3", + "@backstage/search-common": "^0.3.0", "@backstage/types": "^0.1.3", "@octokit/graphql": "^4.5.8", "@types/express": "^4.17.6", @@ -70,11 +70,11 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.19", - "@backstage/cli": "^0.14.1", - "@backstage/plugin-permission-common": "^0.5.1", - "@backstage/plugin-search-backend-node": "0.4.7", - "@backstage/test-utils": "^0.2.6", + "@backstage/backend-test-utils": "^0.1.20", + "@backstage/cli": "^0.15.0", + "@backstage/plugin-permission-common": "^0.5.2", + "@backstage/plugin-search-backend-node": "0.5.0", + "@backstage/test-utils": "^0.3.0", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", "@types/lodash": "^4.14.151", diff --git a/plugins/catalog-common/CHANGELOG.md b/plugins/catalog-common/CHANGELOG.md index 337bd6854c..707207eda4 100644 --- a/plugins/catalog-common/CHANGELOG.md +++ b/plugins/catalog-common/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-catalog-common +## 0.2.0 + +### Minor Changes + +- e3c2bfef11: Remove resourceType property from catalogEntityCreatePermission. Resource type refers to the type of resources whose resourceRefs should be passed along with authorize requests, to allow conditional responses for that resource type. Since creation does not correspond to an entity (as the entity does not exist at the time of authorization), the resourceRef should not be included on the permission. +- 81273e95cf: **Breaking**: Mark permission-related exports as alpha. This means that the exports below should now be imported from `@backstage/plugin-catalog-common/alpha` instead of `@backstage/plugin-catalog-common`. + + - `RESOURCE_TYPE_CATALOG_ENTITY` + - `catalogEntityReadPermission` + - `catalogEntityCreatePermission` + - `catalogEntityDeletePermission` + - `catalogEntityRefreshPermission` + - `catalogLocationReadPermission` + - `catalogLocationCreatePermission` + - `catalogLocationDeletePermission` + +### Patch Changes + +- ab7b6cb7b1: **DEPRECATION**: Moved the `CatalogEntityDocument` to `@backstage/plugin-catalog-common` and deprecated the export from `@backstage/plugin-catalog-backend`. + + A new `type` field has also been added to `CatalogEntityDocument` as a replacement for `componentType`, which is now deprecated. Both fields are still present and should be set to the same value in order to avoid issues with indexing. + + Any search customizations need to be updated to use this new `type` field instead, including any custom frontend filters, custom frontend result components, custom search decorators, or non-default Catalog collator implementations. + +- Updated dependencies + - @backstage/plugin-permission-common@0.5.2 + - @backstage/search-common@0.3.0 + ## 0.1.4 ### Patch Changes diff --git a/plugins/catalog-common/package.json b/plugins/catalog-common/package.json index bc0d3750ba..471893abc4 100644 --- a/plugins/catalog-common/package.json +++ b/plugins/catalog-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-common", "description": "Common functionalities for the catalog plugin", - "version": "0.1.4", + "version": "0.2.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,11 +34,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/plugin-permission-common": "^0.5.1", - "@backstage/search-common": "^0.2.4" + "@backstage/plugin-permission-common": "^0.5.2", + "@backstage/search-common": "^0.3.0" }, "devDependencies": { - "@backstage/cli": "^0.14.0" + "@backstage/cli": "^0.15.0" }, "files": [ "dist", diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index 5ce044aa14..f319a8e23a 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-graph +## 0.2.13 + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- f41a293231: - **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead. +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.2.12 ### Patch Changes diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 0d85123646..46de988621 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.2.12", + "version": "0.2.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,11 +24,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-client": "^0.8.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -45,11 +45,11 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/plugin-catalog": "^0.9.0", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/plugin-catalog": "^0.9.1", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/catalog-graphql/CHANGELOG.md b/plugins/catalog-graphql/CHANGELOG.md index cd1f255e23..fe0e4c61e2 100644 --- a/plugins/catalog-graphql/CHANGELOG.md +++ b/plugins/catalog-graphql/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-catalog-graphql +## 0.3.5 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + ## 0.3.4 ### Patch Changes diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index 6ecf7eb512..78b0600d17 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-graphql", "description": "An experimental Backstage catalog GraphQL module", - "version": "0.3.4", + "version": "0.3.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,7 +34,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/types": "^0.1.3", "graphql-modules": "^2.0.0", @@ -46,8 +46,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/test-utils": "^0.3.0", "@graphql-codegen/cli": "^2.3.1", "@graphql-codegen/typescript": "^2.4.2", "@graphql-codegen/typescript-resolvers": "^2.4.3", diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index b55f8b34fa..4808443a14 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-import +## 0.8.4 + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- f41a293231: - **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead. +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/integration@0.8.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/integration-react@0.1.24 + ## 0.8.3 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 800b747e8a..a4d0c39a64 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-import", "description": "A Backstage plugin the helps you import entities into your catalog", - "version": "0.8.3", + "version": "0.8.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,15 +34,15 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", + "@backstage/catalog-client": "^0.8.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", "@backstage/config": "^0.1.15", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", - "@backstage/integration-react": "^0.1.23", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/integration": "^0.8.0", + "@backstage/integration-react": "^0.1.24", + "@backstage/plugin-catalog-react": "^0.8.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -60,10 +60,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index ac611032e5..89e03aa445 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,47 @@ # @backstage/plugin-catalog-react +## 0.8.0 + +### Minor Changes + +- da79aac2a6: Removed some previously deprecated `routeRefs` as follows: + + - **BREAKING**: Removed `entityRoute` in favor of `entityRouteRef`. + - **BREAKING**: Removed the previously deprecated `rootRoute` and `catalogRouteRef`. If you want to refer to the catalog index page from a public plugin you now need to use an `ExternalRouteRef` instead. For private plugins it is possible to take the shortcut of referring directly to `catalogPlugin.routes.indexPage` instead. + +- e26fd1c7ab: Marked `useEntityPermission` as alpha since the underlying permission framework is under active development. +- 2de1d82bd1: Removing the `EntityName` path for the `useEntityOwnership` as it has never worked correctly. Please pass in an entire `Entity` instead. + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- f41a293231: - **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead. +- f590d1681b: Deprecated `favoriteEntityTooltip` and `favoriteEntityIcon` since the utility value is very low. +- 72431d7bed: - **BREAKING**: The `isOwnerOf` function has been marked as `@alpha` and is now only available via the `@backstage/plugin-catalog-react/alpha` import. The limitations of this function with regards to only supporting direct relations have also been documented. +- 03ec06bf7f: **BREAKING**: Moved **DefaultStarredEntitiesApi** to `@backstage/plugin-catalog`. If you were using this in tests, you can use the new `MockStarredEntitiesApi` from `@backstage/plugin-catalog-react` instead. + + Fixed a risky behavior where `DefaultStarredEntitiesApi` forwarded values to observers that were later mutated. + + Removed the `isStarred` method from `DefaultStarredEntitiesApi`, as it is not part of the `StarredEntitiesApi`. + +- 44403296e7: Added the following deprecations to the `catalog-react` package: + + - **DEPRECATION**: `useEntity` will now warn if the entity has not yet been loaded, and will soon throw errors instead. If you're using the default implementation of `EntityLayout` and `EntitySwitch` then these components will ensure that there is an entity loaded before rendering children. If you're implementing your own `EntityLayout` or `EntitySwitch` or something that operates outside or adjacent to them, then use `useAsyncEntity`. + + - **DEPRECATION**: the `loading`, `error` and `refresh` properties that are returned from `useEntity` have been deprecated, and are available on `useAsyncEntity` instead. + +- 8f0e8e039b: Deprecated `getEntityMetadataEditUrl` and `getEntityMetadataViewUrl` as these just return one annotation from the entity passed in. +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- bb2bb36651: Updated usage of `StorageApi` to use `snapshot` method instead of `get` +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/core-components@0.9.0 + - @backstage/integration@0.8.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/plugin-permission-common@0.5.2 + - @backstage/plugin-permission-react@0.3.3 + ## 0.7.0 ### Minor Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 119c3f19a4..75639d21ee 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-react", "description": "A frontend library that helps other Backstage plugins interact with the catalog", - "version": "0.7.0", + "version": "0.8.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,14 +34,14 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-client": "^0.8.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", - "@backstage/plugin-permission-common": "^0.5.1", - "@backstage/plugin-permission-react": "^0.3.2", + "@backstage/integration": "^0.8.0", + "@backstage/plugin-permission-common": "^0.5.2", + "@backstage/plugin-permission-react": "^0.3.3", "@backstage/types": "^0.1.3", "@backstage/version-bridge": "^0.1.2", "@material-ui/core": "^4.12.2", @@ -61,11 +61,11 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/plugin-catalog-common": "^0.1.4", - "@backstage/plugin-scaffolder-common": "^0.2.2", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/plugin-catalog-common": "^0.2.0", + "@backstage/plugin-scaffolder-common": "^0.2.3", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 3c70959b78..cae922f26a 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-catalog +## 0.9.1 + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- f41a293231: - **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead. +- f590d1681b: Removed usage of deprecated favorite utility methods. +- 44403296e7: Added the following deprecations to the `catalog-react` package: + + - **DEPRECATION**: `useEntity` will now warn if the entity has not yet been loaded, and will soon throw errors instead. If you're using the default implementation of `EntityLayout` and `EntitySwitch` then these components will ensure that there is an entity loaded before rendering children. If you're implementing your own `EntityLayout` or `EntitySwitch` or something that operates outside or adjacent to them, then use `useAsyncEntity`. + + - **DEPRECATION**: the `loading`, `error` and `refresh` properties that are returned from `useEntity` have been deprecated, and are available on `useAsyncEntity` instead. + +- da79aac2a6: - Replaced usage of the deprecated and now removed `rootRoute` and `catalogRouteRef`s from the `catalog-react` package +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- 8f0e8e039b: Removed usage of deprecated `getEntityMetadataViewUrl` and `getEntityMetadataEditUrl` helpers. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/plugin-catalog-common@0.2.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/search-common@0.3.0 + - @backstage/integration-react@0.1.24 + ## 0.9.0 ### Minor Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 6179cf6c44..0a8a4af58c 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog", "description": "The Backstage plugin for browsing the Backstage catalog", - "version": "0.9.0", + "version": "0.9.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,15 +34,15 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-client": "^0.8.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/integration-react": "^0.1.23", - "@backstage/plugin-catalog-common": "^0.1.4", - "@backstage/plugin-catalog-react": "^0.7.0", - "@backstage/search-common": "^0.2.4", + "@backstage/integration-react": "^0.1.24", + "@backstage/plugin-catalog-common": "^0.2.0", + "@backstage/plugin-catalog-react": "^0.8.0", + "@backstage/search-common": "^0.3.0", "@backstage/theme": "^0.2.15", "@backstage/types": "^0.1.2", "@material-ui/core": "^4.12.2", @@ -60,11 +60,11 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/plugin-permission-react": "^0.3.2", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/plugin-permission-react": "^0.3.3", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/cicd-statistics/CHANGELOG.md b/plugins/cicd-statistics/CHANGELOG.md index 9fda781e52..b98e6f65c5 100644 --- a/plugins/cicd-statistics/CHANGELOG.md +++ b/plugins/cicd-statistics/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-cicd-statistics +## 0.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.2 ### Patch Changes diff --git a/plugins/cicd-statistics/package.json b/plugins/cicd-statistics/package.json index a9fe0b0807..d954408ed3 100644 --- a/plugins/cicd-statistics/package.json +++ b/plugins/cicd-statistics/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cicd-statistics", "description": "A frontend plugin visualizing CI/CD pipeline statistics (build time)", - "version": "0.1.2", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,9 +37,9 @@ "@types/luxon": "^2.0.5" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@date-io/luxon": "^1.3.13", "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.11.2", diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index cc16ef1f53..45ad2bff7c 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-circleci +## 0.3.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.3.0 ### Minor Changes diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 2005320b7d..212076721b 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-circleci", "description": "A Backstage plugin that integrates towards Circle CI", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,10 +35,10 @@ "postpack": "backstage-cli package postpack" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -55,10 +55,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md index f4b3c0ec6b..2e79b62657 100644 --- a/plugins/cloudbuild/CHANGELOG.md +++ b/plugins/cloudbuild/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-cloudbuild +## 0.3.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.3.0 ### Minor Changes diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index aca7f03b5d..4ef3dd60b1 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cloudbuild", "description": "A Backstage plugin that integrates towards Google Cloud Build", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,10 +34,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -52,10 +52,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/code-climate/CHANGELOG.md b/plugins/code-climate/CHANGELOG.md index 596c4343b0..b3008e52fb 100644 --- a/plugins/code-climate/CHANGELOG.md +++ b/plugins/code-climate/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-code-climate +## 0.1.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.0 ### Minor Changes diff --git a/plugins/code-climate/package.json b/plugins/code-climate/package.json index 57b6d097ed..567ec6b4b1 100644 --- a/plugins/code-climate/package.json +++ b/plugins/code-climate/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-code-climate", - "version": "0.1.0", + "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.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -37,9 +37,9 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/code-coverage-backend/CHANGELOG.md b/plugins/code-coverage-backend/CHANGELOG.md index d5442a0d6c..3c0b5cde6e 100644 --- a/plugins/code-coverage-backend/CHANGELOG.md +++ b/plugins/code-coverage-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-code-coverage-backend +## 0.1.26 + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/backend-common@0.12.0 + - @backstage/integration@0.8.0 + ## 0.1.25 ### Patch Changes diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index 7ce90f4165..43c2276ca0 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage-backend", "description": "A Backstage backend plugin that helps you keep track of your code coverage", - "version": "0.1.25", + "version": "0.1.26", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,12 +23,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.12.0", + "@backstage/catalog-client": "^0.8.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", + "@backstage/integration": "^0.8.0", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -39,7 +39,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/express-xml-bodyparser": "^0.3.2", "@types/supertest": "^2.0.8", "msw": "^0.35.0", diff --git a/plugins/code-coverage/CHANGELOG.md b/plugins/code-coverage/CHANGELOG.md index aca8a8ba26..cd84fad770 100644 --- a/plugins/code-coverage/CHANGELOG.md +++ b/plugins/code-coverage/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-code-coverage +## 0.1.28 + +### Patch Changes + +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.27 ### Patch Changes diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index ba6d792aba..f1f9bdd5ae 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage", "description": "A Backstage plugin that helps you keep track of your code coverage", - "version": "0.1.27", + "version": "0.1.28", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,12 +24,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -46,10 +46,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md index 83868bd498..9aa10e9ca4 100644 --- a/plugins/config-schema/CHANGELOG.md +++ b/plugins/config-schema/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-config-schema +## 0.1.24 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.23 ### Patch Changes diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index e52654b506..15964135df 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-config-schema", "description": "A Backstage plugin that lets you browse the configuration schema of your app", - "version": "0.1.23", + "version": "0.1.24", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,8 +25,8 @@ }, "dependencies": { "@backstage/config": "^0.1.15", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", "@backstage/theme": "^0.2.15", "@backstage/types": "^0.1.3", @@ -41,10 +41,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index 1520f476b6..549d00ad07 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-cost-insights +## 0.11.23 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.11.22 ### Patch Changes diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 64613102cc..a12fef92ab 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cost-insights", "description": "A Backstage plugin that helps you keep track of your cloud spend", - "version": "0.11.22", + "version": "0.11.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,10 +34,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -60,10 +60,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/explore-react/CHANGELOG.md b/plugins/explore-react/CHANGELOG.md index 9c8e4da88a..d8e6a50f9d 100644 --- a/plugins/explore-react/CHANGELOG.md +++ b/plugins/explore-react/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-explore-react +## 0.0.14 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@0.8.0 + ## 0.0.13 ### Patch Changes diff --git a/plugins/explore-react/package.json b/plugins/explore-react/package.json index 468989d414..8acb7efb40 100644 --- a/plugins/explore-react/package.json +++ b/plugins/explore-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-explore-react", "description": "A frontend library for Backstage plugins that want to interact with the explore plugin", - "version": "0.0.13", + "version": "0.0.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,12 +33,12 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/core-plugin-api": "^0.7.0" + "@backstage/core-plugin-api": "^0.8.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/explore/CHANGELOG.md b/plugins/explore/CHANGELOG.md index 319197565d..8116f246c8 100644 --- a/plugins/explore/CHANGELOG.md +++ b/plugins/explore/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-explore +## 0.3.32 + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- f41a293231: - **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/plugin-explore-react@0.0.14 + ## 0.3.31 ### Patch Changes diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 5eeb8e2a7c..55d2e63367 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-explore", "description": "A Backstage plugin for building an exploration page of your software ecosystem", - "version": "0.3.31", + "version": "0.3.32", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,11 +34,11 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", - "@backstage/plugin-explore-react": "^0.0.13", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", + "@backstage/plugin-explore-react": "^0.0.14", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -53,10 +53,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/firehydrant/CHANGELOG.md b/plugins/firehydrant/CHANGELOG.md index e553158bf4..17150c86a5 100644 --- a/plugins/firehydrant/CHANGELOG.md +++ b/plugins/firehydrant/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-firehydrant +## 0.1.18 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.17 ### Patch Changes diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index 07a3938e49..cec4352e19 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-firehydrant", "description": "A Backstage plugin that integrates towards FireHydrant", - "version": "0.1.17", + "version": "0.1.18", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,9 +25,9 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -39,10 +39,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/fossa/CHANGELOG.md b/plugins/fossa/CHANGELOG.md index c55ebb1aea..1fbdfdd856 100644 --- a/plugins/fossa/CHANGELOG.md +++ b/plugins/fossa/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-fossa +## 0.2.33 + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- f41a293231: - **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead. +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.2.32 ### Patch Changes diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index f6ed9df07f..6d3f364403 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-fossa", "description": "A Backstage plugin that integrates towards FOSSA", - "version": "0.2.32", + "version": "0.2.33", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,11 +35,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -53,10 +53,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/gcp-projects/CHANGELOG.md b/plugins/gcp-projects/CHANGELOG.md index b23adf1239..66827b021e 100644 --- a/plugins/gcp-projects/CHANGELOG.md +++ b/plugins/gcp-projects/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-gcp-projects +## 0.3.20 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.3.19 ### Patch Changes diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index dc1eb7d5b0..3cb3be5382 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gcp-projects", "description": "A Backstage plugin that helps you manage projects in GCP", - "version": "0.3.19", + "version": "0.3.20", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,8 +34,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -47,10 +47,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/git-release-manager/CHANGELOG.md b/plugins/git-release-manager/CHANGELOG.md index eb6f3c6460..3449e9e49b 100644 --- a/plugins/git-release-manager/CHANGELOG.md +++ b/plugins/git-release-manager/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-git-release-manager +## 0.3.14 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/integration@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.3.13 ### Patch Changes diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index 0534039d52..82e099b012 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-git-release-manager", "description": "A Backstage plugin that helps you manage releases in git", - "version": "0.3.13", + "version": "0.3.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,9 +24,9 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/integration": "^0.7.5", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/integration": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -43,10 +43,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index d48aa303d0..a700f82742 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-github-actions +## 0.5.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/integration@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.5.0 ### Minor Changes diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 2802374a61..baa8ab66fa 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-actions", "description": "A Backstage plugin that integrates towards GitHub Actions", - "version": "0.5.0", + "version": "0.5.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,11 +36,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/integration": "^0.7.5", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/integration": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -55,10 +55,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/github-deployments/CHANGELOG.md b/plugins/github-deployments/CHANGELOG.md index e2d398c7f8..c32442ff7d 100644 --- a/plugins/github-deployments/CHANGELOG.md +++ b/plugins/github-deployments/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-github-deployments +## 0.1.32 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/integration@0.8.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/integration-react@0.1.24 + ## 0.1.31 ### Patch Changes diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index 20198ee40a..9a3c2710dd 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-deployments", "description": "A Backstage plugin that integrates towards GitHub Deployments", - "version": "0.1.31", + "version": "0.1.32", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,13 +24,13 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", - "@backstage/integration-react": "^0.1.23", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/integration": "^0.8.0", + "@backstage/integration-react": "^0.1.24", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -43,10 +43,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/gitops-profiles/CHANGELOG.md b/plugins/gitops-profiles/CHANGELOG.md index c2fba95783..55e7b92699 100644 --- a/plugins/gitops-profiles/CHANGELOG.md +++ b/plugins/gitops-profiles/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-gitops-profiles +## 0.3.19 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.3.18 ### Patch Changes diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index d96e6a670d..1668fe58ab 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gitops-profiles", "description": "A Backstage plugin that helps you manage GitOps profiles", - "version": "0.3.18", + "version": "0.3.19", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,8 +35,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -48,10 +48,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/gocd/CHANGELOG.md b/plugins/gocd/CHANGELOG.md index 296d7cc928..92444c38f2 100644 --- a/plugins/gocd/CHANGELOG.md +++ b/plugins/gocd/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-gocd +## 0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.6 ### Patch Changes diff --git a/plugins/gocd/package.json b/plugins/gocd/package.json index afd6c63a91..b66cfe44b0 100644 --- a/plugins/gocd/package.json +++ b/plugins/gocd/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gocd", "description": "A Backstage plugin that integrates towards GoCD", - "version": "0.1.6", + "version": "0.1.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,11 +31,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -49,10 +49,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/graphiql/CHANGELOG.md b/plugins/graphiql/CHANGELOG.md index 522c67d32c..2f899b1c4c 100644 --- a/plugins/graphiql/CHANGELOG.md +++ b/plugins/graphiql/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-graphiql +## 0.2.33 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.2.32 ### Patch Changes diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 517d9c097d..ed7550e48e 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.2.32", + "version": "0.2.33", "private": false, "publishConfig": { "access": "public", @@ -34,8 +34,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -48,10 +48,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/graphql-backend/CHANGELOG.md b/plugins/graphql-backend/CHANGELOG.md index 5fa09c1432..a0ed81d2d7 100644 --- a/plugins/graphql-backend/CHANGELOG.md +++ b/plugins/graphql-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-graphql-backend +## 0.1.18 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + - @backstage/plugin-catalog-graphql@0.3.5 + ## 0.1.17 ### Patch Changes diff --git a/plugins/graphql-backend/package.json b/plugins/graphql-backend/package.json index d1d0f5a153..4e31544b96 100644 --- a/plugins/graphql-backend/package.json +++ b/plugins/graphql-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphql-backend", "description": "An experimental Backstage backend plugin for GraphQL", - "version": "0.1.17", + "version": "0.1.18", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,9 +34,9 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.12.0", "@backstage/config": "^0.1.15", - "@backstage/plugin-catalog-graphql": "^0.3.4", + "@backstage/plugin-catalog-graphql": "^0.3.5", "@graphql-tools/schema": "^8.3.1", "graphql-modules": "^2.0.0", "@types/express": "^4.17.6", @@ -51,7 +51,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/supertest": "^2.0.8", "eslint-plugin-graphql": "^4.0.0", "msw": "^0.35.0", diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index 93fe89340d..c0040de031 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-home +## 0.4.17 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-search@0.7.2 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.4.16 ### Patch Changes diff --git a/plugins/home/package.json b/plugins/home/package.json index 3f265e136c..64cb995577 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-home", "description": "A Backstage plugin that helps you build a home page", - "version": "0.4.16", + "version": "0.4.17", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,11 +34,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", - "@backstage/plugin-search": "^0.7.1", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", + "@backstage/plugin-search": "^0.7.2", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -52,10 +52,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/ilert/CHANGELOG.md b/plugins/ilert/CHANGELOG.md index 7e3296691f..7eb99c6618 100644 --- a/plugins/ilert/CHANGELOG.md +++ b/plugins/ilert/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-ilert +## 0.1.27 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.26 ### Patch Changes diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index d2250287b0..1f64492a2d 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-ilert", "description": "A Backstage plugin that integrates towards iLert", - "version": "0.1.26", + "version": "0.1.27", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,11 +24,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@date-io/luxon": "1.x", "@material-ui/core": "^4.12.2", @@ -43,10 +43,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/jenkins-backend/CHANGELOG.md b/plugins/jenkins-backend/CHANGELOG.md index fe53f21004..1f7e1be399 100644 --- a/plugins/jenkins-backend/CHANGELOG.md +++ b/plugins/jenkins-backend/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-jenkins-backend +## 0.1.17 + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- 23e1c17bba: Jenkins plugin supports permissions now. We have added a new permission, so you can manage the permission for the users. + A new permission `jenkinsExecutePermission` is provided in `jenkins-common` package. This permission rule will be applied to check rebuild actions + if user is allowed to execute this action. + + > We use 'catalog-entity' as a resource type, so you need to integrate a policy to handle catalog-entity resources + + > You need to use this permission in your permission policy to check the user role/rights and return + > `AuthorizeResult.ALLOW` to allow rebuild action to logged user. (e.g: you can check if user or related group owns the entity) + +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/backend-common@0.12.0 + - @backstage/plugin-jenkins-common@0.1.0 + - @backstage/plugin-permission-common@0.5.2 + - @backstage/plugin-auth-node@0.1.4 + ## 0.1.16 ### Patch Changes diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index 14005656ab..dab89b7d35 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins-backend", "description": "A Backstage backend plugin that integrates towards Jenkins", - "version": "0.1.16", + "version": "0.1.17", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,14 +25,14 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.12.0", + "@backstage/catalog-client": "^0.8.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/plugin-auth-node": "^0.1.3", - "@backstage/plugin-jenkins-common": "^0.0.0", - "@backstage/plugin-permission-common": "^0.5.1", + "@backstage/plugin-auth-node": "^0.1.4", + "@backstage/plugin-jenkins-common": "^0.1.0", + "@backstage/plugin-permission-common": "^0.5.2", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -41,7 +41,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/jenkins": "^0.23.1", "@types/supertest": "^2.0.8", "msw": "^0.35.0", diff --git a/plugins/jenkins-common/CHANGELOG.md b/plugins/jenkins-common/CHANGELOG.md new file mode 100644 index 0000000000..9a9f8889ee --- /dev/null +++ b/plugins/jenkins-common/CHANGELOG.md @@ -0,0 +1,13 @@ +# @backstage/plugin-jenkins-common + +## 0.1.0 + +### Minor Changes + +- 23e1c17bba: Add a new common plugin for Jenkins which provides shared isomorphic code for the Jenkins plugin. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-common@0.2.0 + - @backstage/plugin-permission-common@0.5.2 diff --git a/plugins/jenkins-common/package.json b/plugins/jenkins-common/package.json index 41a63298ef..c3a9ec204e 100644 --- a/plugins/jenkins-common/package.json +++ b/plugins/jenkins-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins-common", - "version": "0.0.0", + "version": "0.1.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,11 +22,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/plugin-catalog-common": "^0.1.4", - "@backstage/plugin-permission-common": "^0.5.1" + "@backstage/plugin-catalog-common": "^0.2.0", + "@backstage/plugin-permission-common": "^0.5.2" }, "devDependencies": { - "@backstage/cli": "^0.14.1" + "@backstage/cli": "^0.15.0" }, "files": [ "dist" diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index 33d1fa2508..11e397a595 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-jenkins +## 0.7.0 + +### Minor Changes + +- 23e1c17bba: Jenkins plugin supports permissions now. We have added a new permission, so you can manage the permission for the users. See relates notes for `jenkins-plugin` for more details. + + Rebuild action will be disabled if the user does not have necessary rights to execute rebuild action. A permission policy (defined in backend) must handle and check the identity rights + and return `AuthorizeResult.ALLOW` if user is allowed to execute rebuild action. + +### Patch Changes + +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/plugin-jenkins-common@0.1.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.6.0 ### Minor Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 74e839f7d1..a710b6cae0 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins", "description": "A Backstage plugin that integrates towards Jenkins", - "version": "0.6.0", + "version": "0.7.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,12 +35,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/plugin-catalog-react": "^0.7.0", - "@backstage/plugin-jenkins-common": "^0.0.0", + "@backstage/plugin-catalog-react": "^0.8.0", + "@backstage/plugin-jenkins-common": "^0.1.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -54,10 +54,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/kafka-backend/CHANGELOG.md b/plugins/kafka-backend/CHANGELOG.md index 766f6e9dfb..90e031b064 100644 --- a/plugins/kafka-backend/CHANGELOG.md +++ b/plugins/kafka-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-kafka-backend +## 0.2.21 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/backend-common@0.12.0 + ## 0.2.20 ### Patch Changes diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index f10020b6d6..cf511a5c45 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka-backend", "description": "A Backstage backend plugin that integrates towards Kafka", - "version": "0.2.20", + "version": "0.2.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,8 +35,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.12.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", "@types/express": "^4.17.6", @@ -47,7 +47,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/jest-when": "^2.7.2", "@types/lodash": "^4.14.151", "jest-when": "^3.1.0", diff --git a/plugins/kafka/CHANGELOG.md b/plugins/kafka/CHANGELOG.md index 578e05fe33..b3129a8911 100644 --- a/plugins/kafka/CHANGELOG.md +++ b/plugins/kafka/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-kafka +## 0.3.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.3.0 ### Minor Changes diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index ea979e37f9..1bc311ed7d 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka", "description": "A Backstage plugin that integrates towards Kafka", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,10 +24,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -39,10 +39,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index d6473619cb..eebbf045ca 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-kubernetes-backend +## 0.4.11 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/backend-common@0.12.0 + - @backstage/plugin-kubernetes-common@0.2.6 + ## 0.4.10 ### Patch Changes diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index fa387a9191..e858da79ba 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-backend", "description": "A Backstage backend plugin that integrates towards Kubernetes", - "version": "0.4.10", + "version": "0.4.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,11 +35,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.12.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/plugin-kubernetes-common": "^0.2.5", + "@backstage/plugin-kubernetes-common": "^0.2.6", "@google-cloud/container": "^2.2.0", "@kubernetes/client-node": "^0.16.0", "@types/express": "^4.17.6", @@ -58,7 +58,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/aws4": "^1.5.1", "supertest": "^6.1.3", "aws-sdk-mock": "^5.2.1", diff --git a/plugins/kubernetes-common/CHANGELOG.md b/plugins/kubernetes-common/CHANGELOG.md index 4016be8e1b..566804b2ea 100644 --- a/plugins/kubernetes-common/CHANGELOG.md +++ b/plugins/kubernetes-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-kubernetes-common +## 0.2.6 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + ## 0.2.5 ### Patch Changes diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json index 229cf01d40..9730c3ca39 100644 --- a/plugins/kubernetes-common/package.json +++ b/plugins/kubernetes-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-common", "description": "Common functionalities for kubernetes, to be shared between kubernetes and kubernetes-backend plugin", - "version": "0.2.5", + "version": "0.2.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -38,11 +38,11 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-model": "^0.12.0", "@kubernetes/client-node": "^0.16.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1" + "@backstage/cli": "^0.15.0" }, "jest": { "roots": [ diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index c729333b2a..f754157a83 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-kubernetes +## 0.6.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/plugin-kubernetes-common@0.2.6 + ## 0.6.0 ### Minor Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 6b3c90e396..fb4d1d048b 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes", "description": "A Backstage plugin that integrates towards Kubernetes", - "version": "0.6.0", + "version": "0.6.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,12 +34,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", - "@backstage/plugin-kubernetes-common": "^0.2.5", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", + "@backstage/plugin-kubernetes-common": "^0.2.6", "@kubernetes/client-node": "^0.16.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", @@ -56,10 +56,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index 18dc47bb44..76aeb988cd 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-lighthouse +## 0.3.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.3.0 ### Minor Changes diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 219f6bf8d9..a654ccee65 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-lighthouse", "description": "A Backstage plugin that integrates towards Lighthouse", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,11 +35,11 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -51,10 +51,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/newrelic-dashboard/CHANGELOG.md b/plugins/newrelic-dashboard/CHANGELOG.md index 98f751fb92..ae337e62e9 100644 --- a/plugins/newrelic-dashboard/CHANGELOG.md +++ b/plugins/newrelic-dashboard/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-newrelic-dashboard +## 0.1.9 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.8 ### Patch Changes diff --git a/plugins/newrelic-dashboard/package.json b/plugins/newrelic-dashboard/package.json index d75a96a30e..d7340764b8 100644 --- a/plugins/newrelic-dashboard/package.json +++ b/plugins/newrelic-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-newrelic-dashboard", - "version": "0.1.8", + "version": "0.1.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,19 +23,19 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/dev-utils": "^0.2.23", + "@backstage/cli": "^0.15.0", + "@backstage/dev-utils": "^0.2.24", "@testing-library/jest-dom": "^5.10.1", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5" diff --git a/plugins/newrelic/CHANGELOG.md b/plugins/newrelic/CHANGELOG.md index f25d105a3c..d20c99cc65 100644 --- a/plugins/newrelic/CHANGELOG.md +++ b/plugins/newrelic/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-newrelic +## 0.3.19 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.3.18 ### Patch Changes diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 1f7b1a8fae..9039290a33 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-newrelic", "description": "A Backstage plugin that integrates towards New Relic", - "version": "0.3.18", + "version": "0.3.19", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,8 +35,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -47,10 +47,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index c9cc96bbe6..63d00ce305 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-org +## 0.5.1 + +### Patch Changes + +- f41a293231: - **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead. +- 8f0e8e039b: Removed usage of deprecated `getEntityMetadataViewUrl` and `getEntityMetadataEditUrl` helpers. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.5.0 ### Minor Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index 93c8a2660a..bdb0b248a7 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-org", "description": "A Backstage plugin that helps you create entity pages for your organization", - "version": "0.5.0", + "version": "0.5.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,10 +24,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -42,11 +42,11 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/catalog-client": "^0.7.2", - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/catalog-client": "^0.8.0", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index c5a4e87923..eaf65a1d1c 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-pagerduty +## 0.3.28 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.3.27 ### Patch Changes diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 8a7fcee37e..0f0afda6b2 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-pagerduty", "description": "A Backstage plugin that integrates towards PagerDuty", - "version": "0.3.27", + "version": "0.3.28", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,10 +34,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -52,10 +52,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/permission-backend/CHANGELOG.md b/plugins/permission-backend/CHANGELOG.md index 7ec949ff06..d8f569a58c 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-permission-backend +## 0.5.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + - @backstage/plugin-permission-common@0.5.2 + - @backstage/plugin-permission-node@0.5.3 + - @backstage/plugin-auth-node@0.1.4 + ## 0.5.2 ### Patch Changes diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index 30f8f45dd8..bf9742d56f 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend", - "version": "0.5.2", + "version": "0.5.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,12 +22,12 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/plugin-auth-node": "^0.1.3", - "@backstage/plugin-permission-common": "^0.5.1", - "@backstage/plugin-permission-node": "^0.5.2", + "@backstage/plugin-auth-node": "^0.1.4", + "@backstage/plugin-permission-common": "^0.5.2", + "@backstage/plugin-permission-node": "^0.5.3", "@types/express": "*", "dataloader": "^2.0.0", "express": "^4.17.1", @@ -39,7 +39,7 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/lodash": "^4.14.151", "@types/supertest": "^2.0.8", "supertest": "^6.1.6", diff --git a/plugins/permission-common/CHANGELOG.md b/plugins/permission-common/CHANGELOG.md index b7162ecb06..bdeb000bd4 100644 --- a/plugins/permission-common/CHANGELOG.md +++ b/plugins/permission-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-permission-common +## 0.5.2 + +### Patch Changes + +- 79b9d8a861: Add api doc comments to `Permission` type properties. + ## 0.5.1 ### Patch Changes diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json index 122106da8f..51e6ef7030 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-permission-common", "description": "Isomorphic types and client for Backstage permissions and authorization", - "version": "0.5.1", + "version": "0.5.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { @@ -48,7 +48,7 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.14.0", + "@backstage/cli": "^0.15.0", "@types/jest": "^26.0.7", "msw": "^0.35.0" } diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index 74ea9b927b..72a34005f8 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-permission-node +## 0.5.3 + +### Patch Changes + +- 580f4e1df8: Export some utility functions for parsing PermissionCriteria + + `isAndCriteria`, `isOrCriteria`, `isNotCriteria` are now exported. + +- Updated dependencies + - @backstage/backend-common@0.12.0 + - @backstage/plugin-permission-common@0.5.2 + - @backstage/plugin-auth-node@0.1.4 + ## 0.5.2 ### Patch Changes diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index 7fb5542c27..cc1109c820 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-permission-node", "description": "Common permission and authorization utilities for backend plugins", - "version": "0.5.2", + "version": "0.5.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,18 +33,18 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/plugin-auth-node": "^0.1.3", - "@backstage/plugin-permission-common": "^0.5.1", + "@backstage/plugin-auth-node": "^0.1.4", + "@backstage/plugin-permission-common": "^0.5.2", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/supertest": "^2.0.8", "msw": "^0.35.0", "supertest": "^6.1.3" diff --git a/plugins/permission-react/CHANGELOG.md b/plugins/permission-react/CHANGELOG.md index f83856d958..fa1e950899 100644 --- a/plugins/permission-react/CHANGELOG.md +++ b/plugins/permission-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-permission-react +## 0.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@0.8.0 + - @backstage/plugin-permission-common@0.5.2 + ## 0.3.2 ### Patch Changes diff --git a/plugins/permission-react/package.json b/plugins/permission-react/package.json index 9b338df822..b1764e1373 100644 --- a/plugins/permission-react/package.json +++ b/plugins/permission-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-react", - "version": "0.3.2", + "version": "0.3.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/config": "^0.1.15", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-permission-common": "^0.5.1", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-permission-common": "^0.5.2", "cross-fetch": "^3.1.5", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4", @@ -44,8 +44,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@types/jest": "^26.0.7" diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index 84699fb11f..e64a19af24 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-proxy-backend +## 0.2.22 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + ## 0.2.21 ### Patch Changes diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 8bdb7b72bd..616c72e998 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-proxy-backend", "description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend", - "version": "0.2.21", + "version": "0.2.22", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,7 +32,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.12.0", "@backstage/config": "^0.1.15", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -46,7 +46,7 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/http-proxy-middleware": "^0.19.3", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", diff --git a/plugins/rollbar-backend/CHANGELOG.md b/plugins/rollbar-backend/CHANGELOG.md index 0292160304..dde555ad6d 100644 --- a/plugins/rollbar-backend/CHANGELOG.md +++ b/plugins/rollbar-backend/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-rollbar-backend +## 0.1.25 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + ## 0.1.24 ### Patch Changes diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index 3f477e46d1..8331c547a3 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar-backend", "description": "A Backstage backend plugin that integrates towards Rollbar", - "version": "0.1.24", + "version": "0.1.25", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,7 +34,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.12.0", "@backstage/config": "^0.1.15", "@types/express": "^4.17.6", "camelcase-keys": "^7.0.1", @@ -50,8 +50,8 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/test-utils": "^0.3.0", "@types/supertest": "^2.0.8", "msw": "^0.36.3", "supertest": "^6.1.3" diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index d3dc72fc57..f9fc708273 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-rollbar +## 0.4.1 + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.4.0 ### Minor Changes diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index acd07071e5..671c9b7383 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar", "description": "A Backstage plugin that integrates towards Rollbar", - "version": "0.4.0", + "version": "0.4.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,10 +35,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -53,10 +53,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index 00e5f08ba4..2630e879e3 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.2.3 + +### Patch Changes + +- 6c7a879660: Fixed bug where existing cookiecutter.json file is not used. +- Updated dependencies + - @backstage/backend-common@0.12.0 + - @backstage/plugin-scaffolder-backend@0.17.3 + - @backstage/integration@0.8.0 + ## 0.2.2 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 2e878fc882..838a8641cf 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,10 +23,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.12.0", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", - "@backstage/plugin-scaffolder-backend": "^0.17.0", + "@backstage/integration": "^0.8.0", + "@backstage/plugin-scaffolder-backend": "^0.17.3", "@backstage/config": "^0.1.15", "@backstage/types": "^0.1.3", "command-exists": "^1.2.9", @@ -35,7 +35,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/fs-extra": "^9.0.1", "@types/mock-fs": "^4.13.0", "@types/jest": "^26.0.7", diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index 5a71d8d8ec..56ba37d09d 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + - @backstage/plugin-scaffolder-backend@0.17.3 + - @backstage/integration@0.8.0 + ## 0.3.2 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index f3dc8cd8e5..7b9c877800 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", "description": "A module for the scaffolder backend that lets you template projects using Rails", - "version": "0.3.2", + "version": "0.3.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,17 +24,17 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/plugin-scaffolder-backend": "^0.17.0", + "@backstage/backend-common": "^0.12.0", + "@backstage/plugin-scaffolder-backend": "^0.17.3", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", + "@backstage/integration": "^0.8.0", "@backstage/types": "^0.1.3", "command-exists": "^1.2.9", "fs-extra": "^9.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/command-exists": "^1.2.0", diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index 609e8abab7..ebe8320338 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -24,13 +24,13 @@ }, "dependencies": { "@backstage/config": "^0.1.15", - "@backstage/plugin-scaffolder-backend": "^0.17.0", + "@backstage/plugin-scaffolder-backend": "^0.17.3", "@backstage/types": "^0.1.3", "winston": "^3.2.1", "yeoman-environment": "^3.9.1" }, "devDependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.12.0", "@types/jest": "^26.0.7" }, "files": [ diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 25beb6c644..a44bb923fa 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-scaffolder-backend +## 0.17.3 + +### Patch Changes + +- 5c7f2343ea: Applied fix from version 0.17.2 of this package, which is part of the v0.69.2 release of Backstage. +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- 34af86517c: ensure `apiBaseUrl` being set for Bitbucket integrations, replace hardcoded defaults +- d6deb5e440: Set timeout for scaffolder octokit client +- 83a83381b0: Use the new `processingResult` export from the catalog backend +- 7372f29473: Cleanup API report +- c7f6424a26: Applied fix from `v0.17.1` of this package which is part of the `v0.69.1` release of Backstage. +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- 8119a9e011: Fix the support for custom defaultBranch values for Bitbucket Cloud at the `publish:bitbucket` scaffolder action. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/plugin-catalog-backend@0.23.0 + - @backstage/backend-common@0.12.0 + - @backstage/integration@0.8.0 + - @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.3 + - @backstage/plugin-scaffolder-common@0.2.3 + ## 0.17.2 ### Patch Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 51ff17c1e2..09df1818b4 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend", "description": "The Backstage backend plugin that helps you create new things", - "version": "0.17.0", + "version": "0.17.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,15 +34,15 @@ "build:assets": "node scripts/build-nunjucks.js" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.12.0", + "@backstage/catalog-client": "^0.8.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", - "@backstage/plugin-catalog-backend": "^0.22.0", - "@backstage/plugin-scaffolder-common": "^0.2.2", - "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.2.2", + "@backstage/integration": "^0.8.0", + "@backstage/plugin-catalog-backend": "^0.23.0", + "@backstage/plugin-scaffolder-common": "^0.2.3", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.2.3", "@backstage/types": "^0.1.3", "@gitbeaker/core": "^34.6.0", "@gitbeaker/node": "^35.1.0", @@ -76,8 +76,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/test-utils": "^0.3.0", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md index db702973fe..6d662a1e0e 100644 --- a/plugins/scaffolder-common/CHANGELOG.md +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-scaffolder-common +## 0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + ## 0.2.2 ### Patch Changes diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index be48edd743..67d8f0f8cb 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-common", "description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -39,10 +39,10 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/types": "^0.1.3" }, "devDependencies": { - "@backstage/cli": "^0.14.1" + "@backstage/cli": "^0.15.0" } } diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 1d1cddfa31..b893424ee3 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-scaffolder +## 0.14.0 + +### Minor Changes + +- 1c2755991d: - **BREAKING**: Removed the `FavouriteTemplate` export in favor of the `FavoriteEntity` from `@backstage/plugin-catalog-react`. Please migrate any usages to that component instead if you are creating your own `TemplateCard` page. +- 86da51cec5: **BREAKING**: Removing the exports of the raw components that back the `CustomFieldExtensions`. + +### Patch Changes + +- f41a293231: - **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead. +- 55361f3f7b: Added some deprecations as follows: + + - **DEPRECATED**: `TemplateCardComponent` and `TaskPageComponent` props have been deprecated, and moved to a `components` prop instead. You can pass them in through there instead. + - **DEPRECATED**: `TemplateList` and `TemplateListProps` has been deprecated. Please use the `TemplateCard` to create your own list component instead. + - **DEPRECATED**: `setSecret` has been deprecated in favour of `setSecrets` when calling `useTemplateSecrets` + + Other notable changes: + + - `scaffolderApi.scaffold()` `values` type has been narrowed from `Record` to `Record` instead. + - Moved all navigation internally over to using `routeRefs` and `subRouteRefs` + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/plugin-catalog-common@0.2.0 + - @backstage/integration@0.8.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/plugin-scaffolder-common@0.2.3 + - @backstage/integration-react@0.1.24 + - @backstage/plugin-permission-react@0.3.3 + ## 0.13.0 ### Minor Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index e4eada7a51..4ec86d9b61 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder", "description": "The Backstage plugin that helps you create new things", - "version": "0.13.0", + "version": "0.14.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,18 +35,18 @@ }, "dependencies": { "@types/json-schema": "^7.0.9", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-client": "^0.8.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", - "@backstage/integration-react": "^0.1.23", - "@backstage/plugin-catalog-common": "^0.1.4", - "@backstage/plugin-catalog-react": "^0.7.0", - "@backstage/plugin-permission-react": "^0.3.2", - "@backstage/plugin-scaffolder-common": "^0.2.2", + "@backstage/integration": "^0.8.0", + "@backstage/integration-react": "^0.1.24", + "@backstage/plugin-catalog-common": "^0.2.0", + "@backstage/plugin-catalog-react": "^0.8.0", + "@backstage/plugin-permission-react": "^0.3.3", + "@backstage/plugin-scaffolder-common": "^0.2.3", "@backstage/theme": "^0.2.15", "@backstage/types": "^0.1.3", "@material-ui/core": "^4.12.2", @@ -73,11 +73,11 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/plugin-catalog": "^0.9.0", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/plugin-catalog": "^0.9.1", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md index b68824932e..5cca5a452d 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 0.1.0 + +### Minor Changes + +- 022507c860: **BREAKING** + + The `ElasticSearchSearchEngine` implements the new stream-based indexing + process expected by the latest `@backstage/search-backend-node`. + + When updating to this version, you must also update to the latest version of + `@backstage/search-backend-node`. Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) + for further details. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@0.5.0 + - @backstage/search-common@0.3.0 + ## 0.0.10 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 0a279d6b4c..f49c86f41f 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-elasticsearch", "description": "A module for the search backend that implements search using ElasticSearch", - "version": "0.0.10", + "version": "0.1.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,8 +24,8 @@ }, "dependencies": { "@backstage/config": "^0.1.15", - "@backstage/search-common": "^0.2.4", - "@backstage/plugin-search-backend-node": "^0.4.7", + "@backstage/search-common": "^0.3.0", + "@backstage/plugin-search-backend-node": "^0.5.0", "@elastic/elasticsearch": "7.13.0", "@acuris/aws-es-connection": "^2.2.0", "aws-sdk": "^2.948.0", @@ -34,8 +34,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/cli": "^0.14.1", + "@backstage/backend-common": "^0.12.0", + "@backstage/cli": "^0.15.0", "@elastic/elasticsearch-mock": "^1.0.0" }, "files": [ diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index 08986f6d34..da7b4cb1c2 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-search-backend-module-pg +## 0.3.0 + +### Minor Changes + +- 022507c860: **BREAKING** + + The `PgSearchEngine` implements the new stream-based indexing process expected + by the latest `@backstage/search-backend-node`. + + When updating to this version, you must also update to the latest version of + `@backstage/search-backend-node`. Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) + for further details. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + - @backstage/plugin-search-backend-node@0.5.0 + - @backstage/search-common@0.3.0 + ## 0.2.9 ### Patch Changes diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index d006c4e675..fb95dff540 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-pg", "description": "A module for the search backend that implements search using PostgreSQL", - "version": "0.2.9", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,15 +23,15 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/search-common": "^0.2.4", - "@backstage/plugin-search-backend-node": "^0.4.7", + "@backstage/backend-common": "^0.12.0", + "@backstage/search-common": "^0.3.0", + "@backstage/plugin-search-backend-node": "^0.5.0", "lodash": "^4.17.21", "knex": "^1.0.2" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.19", - "@backstage/cli": "^0.14.1" + "@backstage/backend-test-utils": "^0.1.20", + "@backstage/cli": "^0.15.0" }, "files": [ "dist", diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index 864376a7e7..190efc72a3 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-search-backend-node +## 0.5.0 + +### Minor Changes + +- 022507c860: **BREAKING** + + The Backstage Search Platform's indexing process has been rewritten as a stream + pipeline in order to improve efficiency and performance on large document sets. + + The concepts of `Collator` and `Decorator` have been replaced with readable and + transform object streams (respectively), as well as factory classes to + instantiate them. Accordingly, the `SearchEngine.index()` method has also been + replaced with a `getIndexer()` factory method that resolves to a writable + object stream. + + Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) + for further details. + +### Patch Changes + +- Updated dependencies + - @backstage/search-common@0.3.0 + ## 0.4.7 ### Patch Changes diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index 2c644e357d..dc8d1046b1 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-node", "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", - "version": "0.4.7", + "version": "0.5.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,15 +24,15 @@ }, "dependencies": { "@backstage/errors": "^0.2.2", - "@backstage/search-common": "^0.2.4", + "@backstage/search-common": "^0.3.0", "@types/lunr": "^2.3.3", "lodash": "^4.17.21", "lunr": "^2.3.9", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/cli": "^0.14.1" + "@backstage/backend-common": "^0.12.0", + "@backstage/cli": "^0.15.0" }, "files": [ "dist" diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index a802848287..ec59c13fd3 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-search-backend +## 0.4.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + - @backstage/plugin-permission-common@0.5.2 + - @backstage/plugin-permission-node@0.5.3 + - @backstage/plugin-search-backend-node@0.5.0 + - @backstage/search-common@0.3.0 + - @backstage/plugin-auth-node@0.1.4 + ## 0.4.5 ### Patch Changes diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index cdfb1afead..2733be841e 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend", "description": "The Backstage backend plugin that provides your backstage app with search", - "version": "0.4.5", + "version": "0.4.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,14 +23,14 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/search-common": "^0.2.4", - "@backstage/plugin-auth-node": "^0.1.3", - "@backstage/plugin-permission-common": "^0.5.1", - "@backstage/plugin-permission-node": "^0.5.2", - "@backstage/plugin-search-backend-node": "^0.4.7", + "@backstage/search-common": "^0.3.0", + "@backstage/plugin-auth-node": "^0.1.4", + "@backstage/plugin-permission-common": "^0.5.2", + "@backstage/plugin-permission-node": "^0.5.3", + "@backstage/plugin-search-backend-node": "^0.5.0", "@backstage/types": "^0.1.3", "@types/express": "^4.17.6", "dataloader": "^2.0.0", @@ -43,7 +43,7 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index a0e80eaeee..caea2e7543 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-search +## 0.7.2 + +### Patch Changes + +- 64b430f80d: chore(deps): bump `react-text-truncate` from 0.17.0 to 0.18.0 +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/search-common@0.3.0 + ## 0.7.1 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index 4543aa8dcd..6499920806 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search", "description": "The Backstage plugin that provides your backstage app with search", - "version": "0.7.1", + "version": "0.7.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,13 +33,13 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/plugin-catalog-react": "^0.7.0", - "@backstage/search-common": "^0.2.4", + "@backstage/plugin-catalog-react": "^0.8.0", + "@backstage/search-common": "^0.3.0", "@backstage/theme": "^0.2.15", "@backstage/types": "^0.1.3", "@material-ui/core": "^4.12.2", @@ -56,10 +56,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 44f20e05d1..f5ed8b8989 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-sentry +## 0.3.39 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.3.38 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 1d6db3a846..ae53bfa21c 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sentry", "description": "A Backstage plugin that integrates towards Sentry", - "version": "0.3.38", + "version": "0.3.39", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,10 +35,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -52,10 +52,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/shortcuts/CHANGELOG.md b/plugins/shortcuts/CHANGELOG.md index f60d458dd0..6ace3195db 100644 --- a/plugins/shortcuts/CHANGELOG.md +++ b/plugins/shortcuts/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-shortcuts +## 0.2.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.2.1 ### Patch Changes diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 2407f4b84b..2418a877ff 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-shortcuts", "description": "A Backstage plugin that provides a shortcuts feature to the sidebar", - "version": "0.2.1", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,8 +24,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/theme": "^0.2.15", "@backstage/types": "^0.1.3", "@material-ui/core": "^4.12.2", @@ -42,10 +42,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index 53209faa93..2d13fa8c22 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-sonarqube +## 0.3.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.3.0 ### Minor Changes diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 38aada552f..c44a73f9c9 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sonarqube", "description": "", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,10 +36,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -53,10 +53,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/splunk-on-call/CHANGELOG.md b/plugins/splunk-on-call/CHANGELOG.md index 8667f46749..26b58dfc8a 100644 --- a/plugins/splunk-on-call/CHANGELOG.md +++ b/plugins/splunk-on-call/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-splunk-on-call +## 0.3.25 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.3.24 ### Patch Changes diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 7d8f6b5bdf..bcf41f4c16 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-splunk-on-call", "description": "A Backstage plugin that integrates towards Splunk On-Call", - "version": "0.3.24", + "version": "0.3.25", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,10 +34,10 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -51,10 +51,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md index b34cb336cd..6010797298 100644 --- a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md +++ b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-tech-insights-backend-module-jsonfc +## 0.1.12 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + - @backstage/plugin-tech-insights-node@0.2.6 + ## 0.1.11 ### Patch Changes diff --git a/plugins/tech-insights-backend-module-jsonfc/package.json b/plugins/tech-insights-backend-module-jsonfc/package.json index 5c618d4172..1f5239d8d3 100644 --- a/plugins/tech-insights-backend-module-jsonfc/package.json +++ b/plugins/tech-insights-backend-module-jsonfc/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-backend-module-jsonfc", - "version": "0.1.11", + "version": "0.1.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,11 +34,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", "@backstage/plugin-tech-insights-common": "^0.2.3", - "@backstage/plugin-tech-insights-node": "^0.2.5", + "@backstage/plugin-tech-insights-node": "^0.2.6", "ajv": "^7.0.3", "json-rules-engine": "^6.1.2", "lodash": "^4.17.21", @@ -46,7 +46,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/node-cron": "^3.0.1" }, "files": [ diff --git a/plugins/tech-insights-backend/CHANGELOG.md b/plugins/tech-insights-backend/CHANGELOG.md index 34dfee6ccb..960fcebc79 100644 --- a/plugins/tech-insights-backend/CHANGELOG.md +++ b/plugins/tech-insights-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-tech-insights-backend +## 0.2.8 + +### Patch Changes + +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/backend-common@0.12.0 + - @backstage/plugin-tech-insights-node@0.2.6 + ## 0.2.7 ### Patch Changes diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index c44aaa138c..871dca35d5 100644 --- a/plugins/tech-insights-backend/package.json +++ b/plugins/tech-insights-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-backend", - "version": "0.2.7", + "version": "0.2.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,13 +34,13 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.12.0", + "@backstage/catalog-client": "^0.8.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", "@backstage/plugin-tech-insights-common": "^0.2.3", - "@backstage/plugin-tech-insights-node": "^0.2.5", + "@backstage/plugin-tech-insights-node": "^0.2.6", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -54,8 +54,8 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.19", - "@backstage/cli": "^0.14.1", + "@backstage/backend-test-utils": "^0.1.20", + "@backstage/cli": "^0.15.0", "@types/supertest": "^2.0.8", "@types/node-cron": "^3.0.0", "@types/semver": "^7.3.8", diff --git a/plugins/tech-insights-common/package.json b/plugins/tech-insights-common/package.json index f20310cc0c..f367db199b 100644 --- a/plugins/tech-insights-common/package.json +++ b/plugins/tech-insights-common/package.json @@ -38,7 +38,7 @@ "@backstage/types": "^0.1.3" }, "devDependencies": { - "@backstage/cli": "^0.14.0" + "@backstage/cli": "^0.15.0" }, "files": [ "dist" diff --git a/plugins/tech-insights-node/CHANGELOG.md b/plugins/tech-insights-node/CHANGELOG.md index cb019ccb99..648aaff92d 100644 --- a/plugins/tech-insights-node/CHANGELOG.md +++ b/plugins/tech-insights-node/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-tech-insights-node +## 0.2.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.12.0 + ## 0.2.5 ### Patch Changes diff --git a/plugins/tech-insights-node/package.json b/plugins/tech-insights-node/package.json index b3b0c7d6b8..64973048f2 100644 --- a/plugins/tech-insights-node/package.json +++ b/plugins/tech-insights-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-node", - "version": "0.2.5", + "version": "0.2.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,7 +33,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", + "@backstage/backend-common": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/plugin-tech-insights-common": "^0.2.3", "@types/luxon": "^2.0.5", @@ -41,7 +41,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.14.1" + "@backstage/cli": "^0.15.0" }, "files": [ "dist" diff --git a/plugins/tech-insights/CHANGELOG.md b/plugins/tech-insights/CHANGELOG.md index 30f21e45bd..c15abed40d 100644 --- a/plugins/tech-insights/CHANGELOG.md +++ b/plugins/tech-insights/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-tech-insights +## 0.1.11 + +### Patch Changes + +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.1.10 ### Patch Changes diff --git a/plugins/tech-insights/package.json b/plugins/tech-insights/package.json index 99d4eeefad..f2ccd2da92 100644 --- a/plugins/tech-insights/package.json +++ b/plugins/tech-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights", - "version": "0.1.10", + "version": "0.1.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,11 +23,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/plugin-tech-insights-common": "^0.2.3", "@backstage/theme": "^0.2.15", "@backstage/types": "^0.1.3", @@ -42,10 +42,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/tech-radar/CHANGELOG.md b/plugins/tech-radar/CHANGELOG.md index 88913e12e4..3ec71686e0 100644 --- a/plugins/tech-radar/CHANGELOG.md +++ b/plugins/tech-radar/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-tech-radar +## 0.5.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.5.7 ### Patch Changes diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 0ea04212a5..27aa8d9fb4 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-tech-radar", "description": "A Backstage plugin that lets you display a Tech Radar for your organization", - "version": "0.5.7", + "version": "0.5.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,8 +34,8 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -49,10 +49,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 2e253f5d0c..ca6648bf84 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-techdocs-backend +## 0.14.1 + +### Patch Changes + +- 6537a601c7: Added a new interface that allows for customization of when to build techdocs +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- 022507c860: A `DefaultTechDocsCollatorFactory`, which works with the new stream-based + search indexing subsystem, is now available. The `DefaultTechDocsCollator` will + continue to be available for those unable to upgrade to the stream-based + `@backstage/search-backend-node` (and related packages), however it is now + marked as deprecated and will be removed in a future version. + + To upgrade this plugin and the search indexing subsystem in one go, check + [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) + for necessary changes to your search backend plugin configuration. + +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/backend-common@0.12.0 + - @backstage/plugin-catalog-common@0.2.0 + - @backstage/integration@0.8.0 + - @backstage/search-common@0.3.0 + - @backstage/techdocs-common@0.11.11 + ## 0.14.0 ### Minor Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 20100f4848..81aa19a937 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-backend", "description": "The Backstage backend plugin that renders technical documentation for your components", - "version": "0.14.0", + "version": "0.14.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,15 +34,15 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.12.0", + "@backstage/catalog-client": "^0.8.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", - "@backstage/plugin-catalog-common": "^0.1.4", - "@backstage/search-common": "^0.2.4", - "@backstage/techdocs-common": "^0.11.10", + "@backstage/integration": "^0.8.0", + "@backstage/plugin-catalog-common": "^0.2.0", + "@backstage/search-common": "^0.3.0", + "@backstage/techdocs-common": "^0.11.11", "@types/express": "^4.17.6", "dockerode": "^3.3.1", "express": "^4.17.1", @@ -55,9 +55,9 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/plugin-search-backend-node": "0.4.7", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/plugin-search-backend-node": "0.5.0", + "@backstage/test-utils": "^0.3.0", "@types/dockerode": "^3.3.0", "msw": "^0.35.0", "supertest": "^6.1.3" diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 97104ea5a4..525a911c21 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,56 @@ # @backstage/plugin-techdocs +## 0.15.0 + +### Minor Changes + +- ee3d6c6f10: **BREAKING:** + Table column utilities `createNameColumn`, `createOwnerColumn`, `createTypeColumn` as well as actions utilities `createCopyDocsUrlAction` and `createStarEntityAction` are no longer directly exported. Instead accessible through DocsTable and EntityListDocsTable. + + Use as following: + + ```tsx + DocsTable.columns.createNameColumn(); + DocsTable.columns.createOwnerColumn(); + DocsTable.columns.createTypeColumn(); + + DocsTable.actions.createCopyDocsUrlAction(); + DocsTable.actions.createStarEntityAction(); + ``` + + - Renamed `DocsResultListItem` to `TechDocsSearchResultListItem`, leaving the old name in place as a deprecations. + + - Renamed `TechDocsPage` to `TechDocsReaderPage`, leaving the old name in place as a deprecations. + + - Renamed `TechDocsPageRenderFunction` to `TechDocsPageRenderFunction`, leaving the old name in place as a deprecations. + + - Renamed `TechDocsPageHeader` to `TechDocsReaderPageHeader`, leaving the old name in place as a deprecations. + + - `LegacyTechDocsHome` marked as deprecated and will be deleted in next release, use `TechDocsCustomHome` instead. + + - `LegacyTechDocsPage` marked as deprecated and will be deleted in next release, use `TechDocsReaderPage` instead. + +### Patch Changes + +- 64b430f80d: chore(deps): bump `react-text-truncate` from 0.17.0 to 0.18.0 +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- f41a293231: - **DEPRECATION**: Deprecated `formatEntityRefTitle` in favor of the new `humanizeEntityRef` method instead. Please migrate to using the new method instead. +- c5fda066b1: Collapse techdocs sidebar on small devices +- f590d1681b: Removed usage of deprecated favorite utility methods. +- 5b0f9a75fa: Remove copyright from old footer in documentation generated with previous version of `mkdocs-techdocs-plugin` (`v0.2.2`). +- 0c3ba547a6: Show feedback when copying code snippet to clipboard. +- 0ca964ee0e: Fixed a bug that could cause searches in the in-context TechDocs search bar to show results from a different TechDocs site. +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-search@0.7.2 + - @backstage/plugin-catalog@0.9.1 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/integration@0.8.0 + - @backstage/core-plugin-api@0.8.0 + - @backstage/integration-react@0.1.24 + ## 0.14.0 ### Minor Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 42e57f8be8..7c87035983 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs", "description": "The Backstage plugin that renders technical documentation for your components", - "version": "0.14.0", + "version": "0.15.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,16 +35,16 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", - "@backstage/integration-react": "^0.1.23", - "@backstage/plugin-catalog": "^0.9.0", - "@backstage/plugin-catalog-react": "^0.7.0", - "@backstage/plugin-search": "^0.7.1", + "@backstage/integration": "^0.8.0", + "@backstage/integration-react": "^0.1.24", + "@backstage/plugin-catalog": "^0.9.1", + "@backstage/plugin-catalog-react": "^0.8.0", + "@backstage/plugin-search": "^0.7.2", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -65,10 +65,10 @@ "react-dom": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/todo-backend/CHANGELOG.md b/plugins/todo-backend/CHANGELOG.md index 28ed332560..07e8e279ba 100644 --- a/plugins/todo-backend/CHANGELOG.md +++ b/plugins/todo-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-todo-backend +## 0.1.25 + +### Patch Changes + +- 899f196af5: Use `getEntityByRef` instead of `getEntityByName` in the catalog client +- 36aa63022b: Use `CompoundEntityRef` instead of `EntityName`, and `getCompoundEntityRef` instead of `getEntityName`, from `@backstage/catalog-model`. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/catalog-client@0.8.0 + - @backstage/backend-common@0.12.0 + - @backstage/integration@0.8.0 + ## 0.1.24 ### Patch Changes diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 21f914c04a..a74e8ea5b2 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-todo-backend", "description": "A Backstage backend plugin that lets you browse TODO comments in your source code", - "version": "0.1.24", + "version": "0.1.25", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,12 +29,12 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/backend-common": "^0.11.0", - "@backstage/catalog-client": "^0.7.2", - "@backstage/catalog-model": "^0.11.0", + "@backstage/backend-common": "^0.12.0", + "@backstage/catalog-client": "^0.8.0", + "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/integration": "^0.7.5", + "@backstage/integration": "^0.8.0", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", @@ -43,7 +43,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", + "@backstage/cli": "^0.15.0", "@types/supertest": "^2.0.8", "msw": "^0.35.0", "supertest": "^6.1.3" diff --git a/plugins/todo/CHANGELOG.md b/plugins/todo/CHANGELOG.md index 3a5d1811fe..bfbfd6bb08 100644 --- a/plugins/todo/CHANGELOG.md +++ b/plugins/todo/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-todo +## 0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/core-components@0.9.0 + - @backstage/plugin-catalog-react@0.8.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.2.2 ### Patch Changes diff --git a/plugins/todo/package.json b/plugins/todo/package.json index 11ce818672..4817e5058e 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-todo", "description": "A Backstage plugin that lets you browse TODO comments in your source code", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,11 +30,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -45,10 +45,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index 49a5a5d51b..eb136be1f9 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-user-settings +## 0.4.0 + +### Minor Changes + +- af5eaa87f4: **BREAKING**: Removed deprecated `auth0AuthApiRef`, `oauth2ApiRef`, `samlAuthApiRef` and `oidcAuthApiRef` as these APIs are too generic to be useful. Instructions for how to migrate can be found at [https://backstage.io/docs/api/deprecations#generic-auth-api-refs](https://backstage.io/docs/api/deprecations#generic-auth-api-refs). + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.3.21 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index d9777bda83..fe78e19ce6 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-user-settings", "description": "A Backstage plugin that provides a settings page", - "version": "0.3.21", + "version": "0.4.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,8 +34,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -47,10 +47,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/xcmetrics/CHANGELOG.md b/plugins/xcmetrics/CHANGELOG.md index 4ceecc6c7a..703351b247 100644 --- a/plugins/xcmetrics/CHANGELOG.md +++ b/plugins/xcmetrics/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-xcmetrics +## 0.2.21 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.9.0 + - @backstage/core-plugin-api@0.8.0 + ## 0.2.20 ### Patch Changes diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index 623b27e4ab..b570258530 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-xcmetrics", "description": "A Backstage plugin that shows XCode build metrics for your components", - "version": "0.2.20", + "version": "0.2.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,8 +24,8 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", @@ -40,10 +40,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/yarn.lock b/yarn.lock index 2b13c60201..48784ac13c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1358,6 +1358,15 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" +"@backstage/catalog-client@^0.7.0": + version "0.7.2" + resolved "https://registry.npmjs.org/@backstage/catalog-client/-/catalog-client-0.7.2.tgz#bcfdb2c210e878fbc5833f18e59feae1e7e48330" + integrity sha512-jeJfi4ekwIffi8ozSzvgdv94DZ2kKNwFVhAP0V+63GF6wIspvGuTIwU2uKYH0v9JXA08lwmAsOGnrrwgS6ragA== + dependencies: + "@backstage/catalog-model" "^0.11.0" + "@backstage/errors" "^0.2.2" + cross-fetch "^3.1.5" + "@backstage/catalog-model@^0.10.0": version "0.10.1" resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.10.1.tgz#dcc3415eb4d4ee3d437355c477e85c7479626b3b" @@ -1372,6 +1381,20 @@ lodash "^4.17.21" uuid "^8.0.0" +"@backstage/catalog-model@^0.11.0": + version "0.11.0" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.11.0.tgz#f02f86fe74305b49fce300e9c221659f9092d1b7" + integrity sha512-DnmbzKZejvxBSQv1LjA3AZIxwmchir2A9L9MzWH9D+pVKIu4AEt2HItf6g+xhpgk+4GJppETsEgrLI+Iyr6QSA== + dependencies: + "@backstage/config" "^0.1.15" + "@backstage/errors" "^0.2.2" + "@backstage/types" "^0.1.3" + "@types/json-schema" "^7.0.5" + ajv "^7.0.3" + json-schema "^0.4.0" + lodash "^4.17.21" + uuid "^8.0.0" + "@backstage/catalog-model@^0.9.7": version "0.9.10" resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.9.10.tgz#bd5662e1ad7bd7c9604f3f45d055c99b5b2bb87f" @@ -1388,6 +1411,50 @@ uuid "^8.0.0" yup "^0.32.9" +"@backstage/core-components@^0.8.0", "@backstage/core-components@^0.8.9": + version "0.8.10" + resolved "https://registry.npmjs.org/@backstage/core-components/-/core-components-0.8.10.tgz#6f79c46cdf507fc3a0d764848a4a8aa73af7ce93" + integrity sha512-gGyCPPSdgvzYHWMKlxe/H4yFmEYDQuAtaoT7Y/3+pilcJSIQi9d3oV104BDLC94bdkyOWDsA1tT+PGZtd0p/8g== + dependencies: + "@backstage/config" "^0.1.15" + "@backstage/core-plugin-api" "^0.7.0" + "@backstage/errors" "^0.2.2" + "@backstage/theme" "^0.2.15" + "@material-table/core" "^3.1.0" + "@material-ui/core" "^4.12.2" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.57" + "@react-hookz/web" "^12.3.0" + "@types/react-sparklines" "^1.7.0" + "@types/react-text-truncate" "^0.14.0" + ansi-regex "^6.0.1" + classnames "^2.2.6" + d3-selection "^3.0.0" + d3-shape "^3.0.0" + d3-zoom "^3.0.0" + dagre "^0.8.5" + history "^5.0.0" + immer "^9.0.1" + lodash "^4.17.21" + pluralize "^8.0.0" + prop-types "^15.7.2" + qs "^6.9.4" + rc-progress "3.2.4" + react-helmet "6.1.0" + react-hook-form "^7.12.2" + react-markdown "^8.0.0" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-sparklines "^1.7.0" + react-syntax-highlighter "^15.4.5" + react-text-truncate "^0.17.0" + react-use "^17.3.2" + react-virtualized-auto-sizer "^1.0.6" + react-window "^1.8.6" + remark-gfm "^3.0.1" + zen-observable "^0.8.15" + zod "^3.11.6" + "@backstage/core-plugin-api@^0.6.0", "@backstage/core-plugin-api@^0.6.1": version "0.6.1" resolved "https://registry.npmjs.org/@backstage/core-plugin-api/-/core-plugin-api-0.6.1.tgz#a6fb8110f384ab9405990450956b2c81b88e90b2" @@ -1401,6 +1468,32 @@ react-router-dom "6.0.0-beta.0" zen-observable "^0.8.15" +"@backstage/core-plugin-api@^0.7.0": + version "0.7.0" + resolved "https://registry.npmjs.org/@backstage/core-plugin-api/-/core-plugin-api-0.7.0.tgz#e53f3aa5bbf074a70fcaf264b04402637008659c" + integrity sha512-SVzrJjvEjWzJgIqfFUjQjqW0RGCzj6zgLULpSgP53GQat70I0wje6E4n0blNU+/q7hpt6PoIT0lgZtqFOJrfYQ== + dependencies: + "@backstage/config" "^0.1.15" + "@backstage/types" "^0.1.3" + "@backstage/version-bridge" "^0.1.2" + history "^5.0.0" + prop-types "^15.7.2" + react-router-dom "6.0.0-beta.0" + zen-observable "^0.8.15" + +"@backstage/integration@^0.7.3": + version "0.7.5" + resolved "https://registry.npmjs.org/@backstage/integration/-/integration-0.7.5.tgz#c68848f35db51705b3287b6fa6a259ae4b17bfbc" + integrity sha512-KUoNQLfPaRqQsdfx04IX4d3EIHbJU3tJxoUjVCCDIAThtep6clhY4uoxXIYTUz+bhERUGGJYhMR+1ryEEtwWhw== + dependencies: + "@backstage/config" "^0.1.15" + "@octokit/auth-app" "^3.4.0" + "@octokit/rest" "^18.5.3" + cross-fetch "^3.1.5" + git-url-parse "^11.6.0" + lodash "^4.17.21" + luxon "^2.0.2" + "@backstage/plugin-catalog-react@^0.6.5": version "0.6.15" resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.6.15.tgz#04de88caa4ac2ce2ad000ee34d28b11eda3910b9" @@ -11915,55 +12008,55 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: safe-buffer "^5.1.1" "example-app@link:packages/app": - version "0.2.66" + version "0.2.67" dependencies: - "@backstage/app-defaults" "^0.1.9" - "@backstage/catalog-model" "^0.11.0" - "@backstage/cli" "^0.14.1" - "@backstage/core-app-api" "^0.5.4" - "@backstage/core-components" "^0.8.10" - "@backstage/core-plugin-api" "^0.7.0" - "@backstage/integration-react" "^0.1.23" - "@backstage/plugin-airbrake" "^0.3.0" - "@backstage/plugin-apache-airflow" "^0.1.8" - "@backstage/plugin-api-docs" "^0.8.0" - "@backstage/plugin-azure-devops" "^0.1.16" - "@backstage/plugin-badges" "^0.2.24" - "@backstage/plugin-catalog" "^0.9.0" - "@backstage/plugin-catalog-common" "^0.1.4" - "@backstage/plugin-catalog-graph" "^0.2.12" - "@backstage/plugin-catalog-import" "^0.8.3" - "@backstage/plugin-catalog-react" "^0.7.0" - "@backstage/plugin-circleci" "^0.3.0" - "@backstage/plugin-cloudbuild" "^0.3.0" - "@backstage/plugin-code-coverage" "^0.1.27" - "@backstage/plugin-cost-insights" "^0.11.22" - "@backstage/plugin-explore" "^0.3.31" - "@backstage/plugin-gcp-projects" "^0.3.19" - "@backstage/plugin-github-actions" "^0.5.0" - "@backstage/plugin-gocd" "^0.1.6" - "@backstage/plugin-graphiql" "^0.2.32" - "@backstage/plugin-home" "^0.4.16" - "@backstage/plugin-jenkins" "^0.6.0" - "@backstage/plugin-kafka" "^0.3.0" - "@backstage/plugin-kubernetes" "^0.6.0" - "@backstage/plugin-lighthouse" "^0.3.0" - "@backstage/plugin-newrelic" "^0.3.18" - "@backstage/plugin-newrelic-dashboard" "^0.1.8" - "@backstage/plugin-org" "^0.5.0" - "@backstage/plugin-pagerduty" "0.3.27" - "@backstage/plugin-permission-react" "^0.3.2" - "@backstage/plugin-rollbar" "^0.4.0" - "@backstage/plugin-scaffolder" "^0.13.0" - "@backstage/plugin-search" "^0.7.1" - "@backstage/plugin-sentry" "^0.3.38" - "@backstage/plugin-shortcuts" "^0.2.1" - "@backstage/plugin-tech-insights" "^0.1.10" - "@backstage/plugin-tech-radar" "^0.5.7" - "@backstage/plugin-techdocs" "^0.14.0" - "@backstage/plugin-todo" "^0.2.2" - "@backstage/plugin-user-settings" "^0.3.21" - "@backstage/search-common" "^0.2.4" + "@backstage/app-defaults" "^0.2.0" + "@backstage/catalog-model" "^0.12.0" + "@backstage/cli" "^0.15.0" + "@backstage/core-app-api" "^0.6.0" + "@backstage/core-components" "^0.9.0" + "@backstage/core-plugin-api" "^0.8.0" + "@backstage/integration-react" "^0.1.24" + "@backstage/plugin-airbrake" "^0.3.1" + "@backstage/plugin-apache-airflow" "^0.1.9" + "@backstage/plugin-api-docs" "^0.8.1" + "@backstage/plugin-azure-devops" "^0.1.17" + "@backstage/plugin-badges" "^0.2.25" + "@backstage/plugin-catalog" "^0.9.1" + "@backstage/plugin-catalog-common" "^0.2.0" + "@backstage/plugin-catalog-graph" "^0.2.13" + "@backstage/plugin-catalog-import" "^0.8.4" + "@backstage/plugin-catalog-react" "^0.8.0" + "@backstage/plugin-circleci" "^0.3.1" + "@backstage/plugin-cloudbuild" "^0.3.1" + "@backstage/plugin-code-coverage" "^0.1.28" + "@backstage/plugin-cost-insights" "^0.11.23" + "@backstage/plugin-explore" "^0.3.32" + "@backstage/plugin-gcp-projects" "^0.3.20" + "@backstage/plugin-github-actions" "^0.5.1" + "@backstage/plugin-gocd" "^0.1.7" + "@backstage/plugin-graphiql" "^0.2.33" + "@backstage/plugin-home" "^0.4.17" + "@backstage/plugin-jenkins" "^0.7.0" + "@backstage/plugin-kafka" "^0.3.1" + "@backstage/plugin-kubernetes" "^0.6.1" + "@backstage/plugin-lighthouse" "^0.3.1" + "@backstage/plugin-newrelic" "^0.3.19" + "@backstage/plugin-newrelic-dashboard" "^0.1.9" + "@backstage/plugin-org" "^0.5.1" + "@backstage/plugin-pagerduty" "0.3.28" + "@backstage/plugin-permission-react" "^0.3.3" + "@backstage/plugin-rollbar" "^0.4.1" + "@backstage/plugin-scaffolder" "^0.14.0" + "@backstage/plugin-search" "^0.7.2" + "@backstage/plugin-sentry" "^0.3.39" + "@backstage/plugin-shortcuts" "^0.2.2" + "@backstage/plugin-tech-insights" "^0.1.11" + "@backstage/plugin-tech-radar" "^0.5.8" + "@backstage/plugin-techdocs" "^0.15.0" + "@backstage/plugin-todo" "^0.2.3" + "@backstage/plugin-user-settings" "^0.4.0" + "@backstage/search-common" "^0.3.0" "@backstage/theme" "^0.2.15" "@material-ui/core" "^4.12.2" "@material-ui/icons" "^4.9.1" @@ -20988,6 +21081,13 @@ react-test-renderer@^16.13.1: react-is "^16.8.6" scheduler "^0.19.1" +react-text-truncate@^0.17.0: + version "0.17.0" + resolved "https://registry.npmjs.org/react-text-truncate/-/react-text-truncate-0.17.0.tgz#a820bfd9d084caf85d900a011fe2ab4216fc3821" + integrity sha512-EUL7s47XApOgbR//t/9X+fXg1feS47RcTywNXEQZAlNL0vrCIYGye1C+mpUgGIIXKTkabweid6z7s16AkTo5sA== + dependencies: + prop-types "^15.5.7" + react-text-truncate@^0.18.0: version "0.18.0" resolved "https://registry.npmjs.org/react-text-truncate/-/react-text-truncate-0.18.0.tgz#c65f4be660d24734badb903a4832467eddcf8058" @@ -23667,19 +23767,19 @@ tdigest@^0.1.1: bintrees "1.0.1" "techdocs-cli-embedded-app@link:packages/techdocs-cli-embedded-app": - version "0.2.65" + version "0.2.66" dependencies: - "@backstage/app-defaults" "^0.1.9" - "@backstage/catalog-model" "^0.11.0" - "@backstage/cli" "^0.14.1" + "@backstage/app-defaults" "^0.2.0" + "@backstage/catalog-model" "^0.12.0" + "@backstage/cli" "^0.15.0" "@backstage/config" "^0.1.15" - "@backstage/core-app-api" "^0.5.4" - "@backstage/core-components" "^0.8.10" - "@backstage/core-plugin-api" "^0.7.0" - "@backstage/integration-react" "^0.1.23" - "@backstage/plugin-catalog" "^0.9.0" - "@backstage/plugin-techdocs" "^0.14.0" - "@backstage/test-utils" "^0.2.6" + "@backstage/core-app-api" "^0.6.0" + "@backstage/core-components" "^0.9.0" + "@backstage/core-plugin-api" "^0.8.0" + "@backstage/integration-react" "^0.1.24" + "@backstage/plugin-catalog" "^0.9.1" + "@backstage/plugin-techdocs" "^0.15.0" + "@backstage/test-utils" "^0.3.0" "@backstage/theme" "^0.2.15" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" From 751418ff8a81f4454934f01b13daa962abe22e33 Mon Sep 17 00:00:00 2001 From: Peiman Jafari Date: Thu, 3 Mar 2022 07:42:27 -0800 Subject: [PATCH 171/353] update mockContext Signed-off-by: Peiman Jafari --- .../src/scaffolder/actions/builtin/publish/github.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index ec564e93dc..e997c30a32 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -54,6 +54,10 @@ describe('publish:github', () => { description: 'description', repoVisibility: 'private' as const, access: 'owner/blam', + deleteBranchOnMerge: false, + allowMergeCommit: true, + allowSquashMerge: true, + allowRebaseMerge: true, }, workspacePath: 'lol', logger: getVoidLogger(), From c108a04dd116a016147ff7a0733cf03682b81d96 Mon Sep 17 00:00:00 2001 From: Peiman Jafari Date: Thu, 3 Mar 2022 08:22:02 -0800 Subject: [PATCH 172/353] update api-report Signed-off-by: Peiman Jafari --- plugins/scaffolder-backend/api-report.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 3bc03c4840..3328a11343 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -233,6 +233,10 @@ export function createPublishGithubAction(options: { description?: string | undefined; access?: string | undefined; defaultBranch?: string | undefined; + deleteBranchOnMerge: boolean; + allowRebaseMerge: boolean; + allowSquashMerge: boolean; + allowMergeCommit: boolean; sourcePath?: string | undefined; requireCodeOwnerReviews?: boolean | undefined; repoVisibility?: 'internal' | 'private' | 'public' | undefined; From 84a8788794d0d9cf223f5e3572e1852297a79807 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 3 Mar 2022 13:20:36 +0100 Subject: [PATCH 173/353] Move packages/techdocs-common -> plugins/techdocs-node Signed-off-by: Eric Peterson --- {packages/techdocs-common => plugins/techdocs-node}/.eslintrc.js | 0 {packages/techdocs-common => plugins/techdocs-node}/CHANGELOG.md | 0 {packages/techdocs-common => plugins/techdocs-node}/README.md | 0 .../techdocs-node}/__mocks__/@azure/identity.ts | 0 .../techdocs-node}/__mocks__/@azure/storage-blob.ts | 0 .../techdocs-node}/__mocks__/@google-cloud/storage.ts | 0 .../techdocs-node}/__mocks__/@trendyol-js/openstack-swift-sdk.ts | 0 .../techdocs-node}/__mocks__/aws-sdk.ts | 0 {packages/techdocs-common => plugins/techdocs-node}/api-report.md | 0 {packages/techdocs-common => plugins/techdocs-node}/package.json | 0 .../techdocs-common => plugins/techdocs-node}/src/helpers.test.ts | 0 .../techdocs-common => plugins/techdocs-node}/src/helpers.ts | 0 {packages/techdocs-common => plugins/techdocs-node}/src/index.ts | 0 .../techdocs-common => plugins/techdocs-node}/src/setupTests.ts | 0 .../techdocs-node}/src/stages/generate/__fixtures__/mkdocs.yml | 0 .../src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir.yml | 0 .../src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir2.yml | 0 .../src/stages/generate/__fixtures__/mkdocs_valid_doc_dir.yml | 0 .../generate/__fixtures__/mkdocs_with_additional_plugins.yml | 0 .../src/stages/generate/__fixtures__/mkdocs_with_comments.yml | 0 .../src/stages/generate/__fixtures__/mkdocs_with_edit_uri.yml | 0 .../src/stages/generate/__fixtures__/mkdocs_with_extensions.yml | 0 .../src/stages/generate/__fixtures__/mkdocs_with_repo_url.yml | 0 .../stages/generate/__fixtures__/mkdocs_with_techdocs_plugin.yml | 0 .../src/stages/generate/__fixtures__/mkdocs_without_plugins.yml | 0 .../techdocs-node}/src/stages/generate/generators.test.ts | 0 .../techdocs-node}/src/stages/generate/generators.ts | 0 .../techdocs-node}/src/stages/generate/helpers.test.ts | 0 .../techdocs-node}/src/stages/generate/helpers.ts | 0 .../techdocs-node}/src/stages/generate/index.ts | 0 .../techdocs-node}/src/stages/generate/mkDocsPatchers.ts | 0 .../techdocs-node}/src/stages/generate/techdocs.test.ts | 0 .../techdocs-node}/src/stages/generate/techdocs.ts | 0 .../techdocs-node}/src/stages/generate/types.ts | 0 .../techdocs-common => plugins/techdocs-node}/src/stages/index.ts | 0 .../techdocs-node}/src/stages/prepare/dir.test.ts | 0 .../techdocs-node}/src/stages/prepare/dir.ts | 0 .../techdocs-node}/src/stages/prepare/index.ts | 0 .../techdocs-node}/src/stages/prepare/preparers.ts | 0 .../techdocs-node}/src/stages/prepare/types.ts | 0 .../techdocs-node}/src/stages/prepare/url.ts | 0 .../techdocs-node}/src/stages/publish/awsS3.test.ts | 0 .../techdocs-node}/src/stages/publish/awsS3.ts | 0 .../techdocs-node}/src/stages/publish/azureBlobStorage.test.ts | 0 .../techdocs-node}/src/stages/publish/azureBlobStorage.ts | 0 .../techdocs-node}/src/stages/publish/googleStorage.test.ts | 0 .../techdocs-node}/src/stages/publish/googleStorage.ts | 0 .../techdocs-node}/src/stages/publish/helpers.test.ts | 0 .../techdocs-node}/src/stages/publish/helpers.ts | 0 .../techdocs-node}/src/stages/publish/index.ts | 0 .../techdocs-node}/src/stages/publish/local.test.ts | 0 .../techdocs-node}/src/stages/publish/local.ts | 0 .../src/stages/publish/migrations/GoogleMigration.ts | 0 .../techdocs-node}/src/stages/publish/migrations/index.ts | 0 .../techdocs-node}/src/stages/publish/openStackSwift.test.ts | 0 .../techdocs-node}/src/stages/publish/openStackSwift.ts | 0 .../techdocs-node}/src/stages/publish/publish.test.ts | 0 .../techdocs-node}/src/stages/publish/publish.ts | 0 .../techdocs-node}/src/stages/publish/types.ts | 0 .../techdocs-node}/src/techdocsTypes.ts | 0 .../techdocs-node}/src/testUtils/StorageFilesMock.ts | 0 .../techdocs-node}/src/testUtils/types.ts | 0 62 files changed, 0 insertions(+), 0 deletions(-) rename {packages/techdocs-common => plugins/techdocs-node}/.eslintrc.js (100%) rename {packages/techdocs-common => plugins/techdocs-node}/CHANGELOG.md (100%) rename {packages/techdocs-common => plugins/techdocs-node}/README.md (100%) rename {packages/techdocs-common => plugins/techdocs-node}/__mocks__/@azure/identity.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/__mocks__/@azure/storage-blob.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/__mocks__/@google-cloud/storage.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/__mocks__/@trendyol-js/openstack-swift-sdk.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/__mocks__/aws-sdk.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/api-report.md (100%) rename {packages/techdocs-common => plugins/techdocs-node}/package.json (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/helpers.test.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/helpers.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/index.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/setupTests.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/__fixtures__/mkdocs.yml (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir.yml (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir2.yml (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/__fixtures__/mkdocs_valid_doc_dir.yml (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/__fixtures__/mkdocs_with_additional_plugins.yml (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/__fixtures__/mkdocs_with_comments.yml (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/__fixtures__/mkdocs_with_edit_uri.yml (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/__fixtures__/mkdocs_with_extensions.yml (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/__fixtures__/mkdocs_with_repo_url.yml (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/__fixtures__/mkdocs_with_techdocs_plugin.yml (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/__fixtures__/mkdocs_without_plugins.yml (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/generators.test.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/generators.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/helpers.test.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/helpers.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/index.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/mkDocsPatchers.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/techdocs.test.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/techdocs.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/generate/types.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/index.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/prepare/dir.test.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/prepare/dir.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/prepare/index.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/prepare/preparers.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/prepare/types.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/prepare/url.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/publish/awsS3.test.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/publish/awsS3.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/publish/azureBlobStorage.test.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/publish/azureBlobStorage.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/publish/googleStorage.test.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/publish/googleStorage.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/publish/helpers.test.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/publish/helpers.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/publish/index.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/publish/local.test.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/publish/local.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/publish/migrations/GoogleMigration.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/publish/migrations/index.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/publish/openStackSwift.test.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/publish/openStackSwift.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/publish/publish.test.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/publish/publish.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/stages/publish/types.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/techdocsTypes.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/testUtils/StorageFilesMock.ts (100%) rename {packages/techdocs-common => plugins/techdocs-node}/src/testUtils/types.ts (100%) diff --git a/packages/techdocs-common/.eslintrc.js b/plugins/techdocs-node/.eslintrc.js similarity index 100% rename from packages/techdocs-common/.eslintrc.js rename to plugins/techdocs-node/.eslintrc.js diff --git a/packages/techdocs-common/CHANGELOG.md b/plugins/techdocs-node/CHANGELOG.md similarity index 100% rename from packages/techdocs-common/CHANGELOG.md rename to plugins/techdocs-node/CHANGELOG.md diff --git a/packages/techdocs-common/README.md b/plugins/techdocs-node/README.md similarity index 100% rename from packages/techdocs-common/README.md rename to plugins/techdocs-node/README.md diff --git a/packages/techdocs-common/__mocks__/@azure/identity.ts b/plugins/techdocs-node/__mocks__/@azure/identity.ts similarity index 100% rename from packages/techdocs-common/__mocks__/@azure/identity.ts rename to plugins/techdocs-node/__mocks__/@azure/identity.ts diff --git a/packages/techdocs-common/__mocks__/@azure/storage-blob.ts b/plugins/techdocs-node/__mocks__/@azure/storage-blob.ts similarity index 100% rename from packages/techdocs-common/__mocks__/@azure/storage-blob.ts rename to plugins/techdocs-node/__mocks__/@azure/storage-blob.ts diff --git a/packages/techdocs-common/__mocks__/@google-cloud/storage.ts b/plugins/techdocs-node/__mocks__/@google-cloud/storage.ts similarity index 100% rename from packages/techdocs-common/__mocks__/@google-cloud/storage.ts rename to plugins/techdocs-node/__mocks__/@google-cloud/storage.ts diff --git a/packages/techdocs-common/__mocks__/@trendyol-js/openstack-swift-sdk.ts b/plugins/techdocs-node/__mocks__/@trendyol-js/openstack-swift-sdk.ts similarity index 100% rename from packages/techdocs-common/__mocks__/@trendyol-js/openstack-swift-sdk.ts rename to plugins/techdocs-node/__mocks__/@trendyol-js/openstack-swift-sdk.ts diff --git a/packages/techdocs-common/__mocks__/aws-sdk.ts b/plugins/techdocs-node/__mocks__/aws-sdk.ts similarity index 100% rename from packages/techdocs-common/__mocks__/aws-sdk.ts rename to plugins/techdocs-node/__mocks__/aws-sdk.ts diff --git a/packages/techdocs-common/api-report.md b/plugins/techdocs-node/api-report.md similarity index 100% rename from packages/techdocs-common/api-report.md rename to plugins/techdocs-node/api-report.md diff --git a/packages/techdocs-common/package.json b/plugins/techdocs-node/package.json similarity index 100% rename from packages/techdocs-common/package.json rename to plugins/techdocs-node/package.json diff --git a/packages/techdocs-common/src/helpers.test.ts b/plugins/techdocs-node/src/helpers.test.ts similarity index 100% rename from packages/techdocs-common/src/helpers.test.ts rename to plugins/techdocs-node/src/helpers.test.ts diff --git a/packages/techdocs-common/src/helpers.ts b/plugins/techdocs-node/src/helpers.ts similarity index 100% rename from packages/techdocs-common/src/helpers.ts rename to plugins/techdocs-node/src/helpers.ts diff --git a/packages/techdocs-common/src/index.ts b/plugins/techdocs-node/src/index.ts similarity index 100% rename from packages/techdocs-common/src/index.ts rename to plugins/techdocs-node/src/index.ts diff --git a/packages/techdocs-common/src/setupTests.ts b/plugins/techdocs-node/src/setupTests.ts similarity index 100% rename from packages/techdocs-common/src/setupTests.ts rename to plugins/techdocs-node/src/setupTests.ts diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs.yml similarity index 100% rename from packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs.yml rename to plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs.yml diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir.yml similarity index 100% rename from packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir.yml rename to plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir.yml diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir2.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir2.yml similarity index 100% rename from packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir2.yml rename to plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir2.yml diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_valid_doc_dir.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_valid_doc_dir.yml similarity index 100% rename from packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_valid_doc_dir.yml rename to plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_valid_doc_dir.yml diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_additional_plugins.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_additional_plugins.yml similarity index 100% rename from packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_additional_plugins.yml rename to plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_additional_plugins.yml diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_comments.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_comments.yml similarity index 100% rename from packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_comments.yml rename to plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_comments.yml diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_edit_uri.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_edit_uri.yml similarity index 100% rename from packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_edit_uri.yml rename to plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_edit_uri.yml diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_extensions.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_extensions.yml similarity index 100% rename from packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_extensions.yml rename to plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_extensions.yml diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_repo_url.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_repo_url.yml similarity index 100% rename from packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_repo_url.yml rename to plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_repo_url.yml diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_techdocs_plugin.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_techdocs_plugin.yml similarity index 100% rename from packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_with_techdocs_plugin.yml rename to plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_with_techdocs_plugin.yml diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_without_plugins.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_without_plugins.yml similarity index 100% rename from packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_without_plugins.yml rename to plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_without_plugins.yml diff --git a/packages/techdocs-common/src/stages/generate/generators.test.ts b/plugins/techdocs-node/src/stages/generate/generators.test.ts similarity index 100% rename from packages/techdocs-common/src/stages/generate/generators.test.ts rename to plugins/techdocs-node/src/stages/generate/generators.test.ts diff --git a/packages/techdocs-common/src/stages/generate/generators.ts b/plugins/techdocs-node/src/stages/generate/generators.ts similarity index 100% rename from packages/techdocs-common/src/stages/generate/generators.ts rename to plugins/techdocs-node/src/stages/generate/generators.ts diff --git a/packages/techdocs-common/src/stages/generate/helpers.test.ts b/plugins/techdocs-node/src/stages/generate/helpers.test.ts similarity index 100% rename from packages/techdocs-common/src/stages/generate/helpers.test.ts rename to plugins/techdocs-node/src/stages/generate/helpers.test.ts diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/plugins/techdocs-node/src/stages/generate/helpers.ts similarity index 100% rename from packages/techdocs-common/src/stages/generate/helpers.ts rename to plugins/techdocs-node/src/stages/generate/helpers.ts diff --git a/packages/techdocs-common/src/stages/generate/index.ts b/plugins/techdocs-node/src/stages/generate/index.ts similarity index 100% rename from packages/techdocs-common/src/stages/generate/index.ts rename to plugins/techdocs-node/src/stages/generate/index.ts diff --git a/packages/techdocs-common/src/stages/generate/mkDocsPatchers.ts b/plugins/techdocs-node/src/stages/generate/mkDocsPatchers.ts similarity index 100% rename from packages/techdocs-common/src/stages/generate/mkDocsPatchers.ts rename to plugins/techdocs-node/src/stages/generate/mkDocsPatchers.ts diff --git a/packages/techdocs-common/src/stages/generate/techdocs.test.ts b/plugins/techdocs-node/src/stages/generate/techdocs.test.ts similarity index 100% rename from packages/techdocs-common/src/stages/generate/techdocs.test.ts rename to plugins/techdocs-node/src/stages/generate/techdocs.test.ts diff --git a/packages/techdocs-common/src/stages/generate/techdocs.ts b/plugins/techdocs-node/src/stages/generate/techdocs.ts similarity index 100% rename from packages/techdocs-common/src/stages/generate/techdocs.ts rename to plugins/techdocs-node/src/stages/generate/techdocs.ts diff --git a/packages/techdocs-common/src/stages/generate/types.ts b/plugins/techdocs-node/src/stages/generate/types.ts similarity index 100% rename from packages/techdocs-common/src/stages/generate/types.ts rename to plugins/techdocs-node/src/stages/generate/types.ts diff --git a/packages/techdocs-common/src/stages/index.ts b/plugins/techdocs-node/src/stages/index.ts similarity index 100% rename from packages/techdocs-common/src/stages/index.ts rename to plugins/techdocs-node/src/stages/index.ts diff --git a/packages/techdocs-common/src/stages/prepare/dir.test.ts b/plugins/techdocs-node/src/stages/prepare/dir.test.ts similarity index 100% rename from packages/techdocs-common/src/stages/prepare/dir.test.ts rename to plugins/techdocs-node/src/stages/prepare/dir.test.ts diff --git a/packages/techdocs-common/src/stages/prepare/dir.ts b/plugins/techdocs-node/src/stages/prepare/dir.ts similarity index 100% rename from packages/techdocs-common/src/stages/prepare/dir.ts rename to plugins/techdocs-node/src/stages/prepare/dir.ts diff --git a/packages/techdocs-common/src/stages/prepare/index.ts b/plugins/techdocs-node/src/stages/prepare/index.ts similarity index 100% rename from packages/techdocs-common/src/stages/prepare/index.ts rename to plugins/techdocs-node/src/stages/prepare/index.ts diff --git a/packages/techdocs-common/src/stages/prepare/preparers.ts b/plugins/techdocs-node/src/stages/prepare/preparers.ts similarity index 100% rename from packages/techdocs-common/src/stages/prepare/preparers.ts rename to plugins/techdocs-node/src/stages/prepare/preparers.ts diff --git a/packages/techdocs-common/src/stages/prepare/types.ts b/plugins/techdocs-node/src/stages/prepare/types.ts similarity index 100% rename from packages/techdocs-common/src/stages/prepare/types.ts rename to plugins/techdocs-node/src/stages/prepare/types.ts diff --git a/packages/techdocs-common/src/stages/prepare/url.ts b/plugins/techdocs-node/src/stages/prepare/url.ts similarity index 100% rename from packages/techdocs-common/src/stages/prepare/url.ts rename to plugins/techdocs-node/src/stages/prepare/url.ts diff --git a/packages/techdocs-common/src/stages/publish/awsS3.test.ts b/plugins/techdocs-node/src/stages/publish/awsS3.test.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/awsS3.test.ts rename to plugins/techdocs-node/src/stages/publish/awsS3.test.ts diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/plugins/techdocs-node/src/stages/publish/awsS3.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/awsS3.ts rename to plugins/techdocs-node/src/stages/publish/awsS3.ts diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts b/plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/azureBlobStorage.test.ts rename to plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts b/plugins/techdocs-node/src/stages/publish/azureBlobStorage.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/azureBlobStorage.ts rename to plugins/techdocs-node/src/stages/publish/azureBlobStorage.ts diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.test.ts b/plugins/techdocs-node/src/stages/publish/googleStorage.test.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/googleStorage.test.ts rename to plugins/techdocs-node/src/stages/publish/googleStorage.test.ts diff --git a/packages/techdocs-common/src/stages/publish/googleStorage.ts b/plugins/techdocs-node/src/stages/publish/googleStorage.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/googleStorage.ts rename to plugins/techdocs-node/src/stages/publish/googleStorage.ts diff --git a/packages/techdocs-common/src/stages/publish/helpers.test.ts b/plugins/techdocs-node/src/stages/publish/helpers.test.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/helpers.test.ts rename to plugins/techdocs-node/src/stages/publish/helpers.test.ts diff --git a/packages/techdocs-common/src/stages/publish/helpers.ts b/plugins/techdocs-node/src/stages/publish/helpers.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/helpers.ts rename to plugins/techdocs-node/src/stages/publish/helpers.ts diff --git a/packages/techdocs-common/src/stages/publish/index.ts b/plugins/techdocs-node/src/stages/publish/index.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/index.ts rename to plugins/techdocs-node/src/stages/publish/index.ts diff --git a/packages/techdocs-common/src/stages/publish/local.test.ts b/plugins/techdocs-node/src/stages/publish/local.test.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/local.test.ts rename to plugins/techdocs-node/src/stages/publish/local.test.ts diff --git a/packages/techdocs-common/src/stages/publish/local.ts b/plugins/techdocs-node/src/stages/publish/local.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/local.ts rename to plugins/techdocs-node/src/stages/publish/local.ts diff --git a/packages/techdocs-common/src/stages/publish/migrations/GoogleMigration.ts b/plugins/techdocs-node/src/stages/publish/migrations/GoogleMigration.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/migrations/GoogleMigration.ts rename to plugins/techdocs-node/src/stages/publish/migrations/GoogleMigration.ts diff --git a/packages/techdocs-common/src/stages/publish/migrations/index.ts b/plugins/techdocs-node/src/stages/publish/migrations/index.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/migrations/index.ts rename to plugins/techdocs-node/src/stages/publish/migrations/index.ts diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts b/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/openStackSwift.test.ts rename to plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.ts b/plugins/techdocs-node/src/stages/publish/openStackSwift.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/openStackSwift.ts rename to plugins/techdocs-node/src/stages/publish/openStackSwift.ts diff --git a/packages/techdocs-common/src/stages/publish/publish.test.ts b/plugins/techdocs-node/src/stages/publish/publish.test.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/publish.test.ts rename to plugins/techdocs-node/src/stages/publish/publish.test.ts diff --git a/packages/techdocs-common/src/stages/publish/publish.ts b/plugins/techdocs-node/src/stages/publish/publish.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/publish.ts rename to plugins/techdocs-node/src/stages/publish/publish.ts diff --git a/packages/techdocs-common/src/stages/publish/types.ts b/plugins/techdocs-node/src/stages/publish/types.ts similarity index 100% rename from packages/techdocs-common/src/stages/publish/types.ts rename to plugins/techdocs-node/src/stages/publish/types.ts diff --git a/packages/techdocs-common/src/techdocsTypes.ts b/plugins/techdocs-node/src/techdocsTypes.ts similarity index 100% rename from packages/techdocs-common/src/techdocsTypes.ts rename to plugins/techdocs-node/src/techdocsTypes.ts diff --git a/packages/techdocs-common/src/testUtils/StorageFilesMock.ts b/plugins/techdocs-node/src/testUtils/StorageFilesMock.ts similarity index 100% rename from packages/techdocs-common/src/testUtils/StorageFilesMock.ts rename to plugins/techdocs-node/src/testUtils/StorageFilesMock.ts diff --git a/packages/techdocs-common/src/testUtils/types.ts b/plugins/techdocs-node/src/testUtils/types.ts similarity index 100% rename from packages/techdocs-common/src/testUtils/types.ts rename to plugins/techdocs-node/src/testUtils/types.ts From 5c8887c0ad6f9621531daa0be0272eb8de76cac2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Mar 2022 16:48:26 +0000 Subject: [PATCH 174/353] chore(deps-dev): bump @graphql-codegen/cli from 2.3.1 to 2.6.2 Bumps [@graphql-codegen/cli](https://github.com/dotansimha/graphql-code-generator/tree/HEAD/packages/graphql-codegen-cli) from 2.3.1 to 2.6.2. - [Release notes](https://github.com/dotansimha/graphql-code-generator/releases) - [Changelog](https://github.com/dotansimha/graphql-code-generator/blob/master/packages/graphql-codegen-cli/CHANGELOG.md) - [Commits](https://github.com/dotansimha/graphql-code-generator/commits/@graphql-codegen/cli@2.6.2/packages/graphql-codegen-cli) --- updated-dependencies: - dependency-name: "@graphql-codegen/cli" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 114 ++++++++++++++++++------------------------------------ 1 file changed, 38 insertions(+), 76 deletions(-) diff --git a/yarn.lock b/yarn.lock index 48784ac13c..adff77c8f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2072,12 +2072,12 @@ meros "^1.1.4" "@graphql-codegen/cli@^2.3.1": - version "2.3.1" - resolved "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-2.3.1.tgz#66083293b60e3182603d70031210d59e6f1a16e5" - integrity sha512-xMSvYqFtnRXOp/sVJSyqiFTm70X8ouLXiq5o/R/D3yQtA6NNudAC+Q4oxg9/LZKnRDL6pehwdC8CNnQk0Tf7Sw== + version "2.6.2" + resolved "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-2.6.2.tgz#a9aa4656141ee0998cae8c7ad7d0bf9ca8e0c9ae" + integrity sha512-UO75msoVgvLEvfjCezM09cQQqp32+mR8Ma1ACsBpr7nroFvHbgcu2ulx1cMovg4sxDBCsvd9Eq/xOOMpARUxtw== dependencies: - "@graphql-codegen/core" "2.4.0" - "@graphql-codegen/plugin-helpers" "^2.3.2" + "@graphql-codegen/core" "2.5.1" + "@graphql-codegen/plugin-helpers" "^2.4.1" "@graphql-tools/apollo-engine-loader" "^7.0.5" "@graphql-tools/code-file-loader" "^7.0.6" "@graphql-tools/git-loader" "^7.0.5" @@ -2107,7 +2107,7 @@ listr "^0.14.3" listr-update-renderer "^0.5.0" log-symbols "^4.0.0" - minimatch "^3.0.4" + minimatch "^4.0.0" mkdirp "^1.0.4" string-env-interpolation "^1.0.1" ts-log "^2.2.3" @@ -2117,12 +2117,12 @@ yaml "^1.10.0" yargs "^17.0.0" -"@graphql-codegen/core@2.4.0": - version "2.4.0" - resolved "https://registry.npmjs.org/@graphql-codegen/core/-/core-2.4.0.tgz#d94dcc088b5e117c847ce5b10c4fe1eb7325e180" - integrity sha512-5RiYE1+07jayp/3w/bkyaCXtfKNeKmRabpPP4aRi369WeH2cH37l2K8NbhkIU+zhpnhoqMID61TO56x2fKldZQ== +"@graphql-codegen/core@2.5.1": + version "2.5.1" + resolved "https://registry.npmjs.org/@graphql-codegen/core/-/core-2.5.1.tgz#e3d50d3449b8c58b74ea08e97faf656a1b7fc8a1" + integrity sha512-alctBVl2hMnBXDLwkgmnFPrZVIiBDsWJSmxJcM4GKg1PB23+xuov35GE47YAyAhQItE1B1fbYnbb1PtGiDZ4LA== dependencies: - "@graphql-codegen/plugin-helpers" "^2.3.2" + "@graphql-codegen/plugin-helpers" "^2.4.1" "@graphql-tools/schema" "^8.1.2" "@graphql-tools/utils" "^8.1.1" tslib "~2.3.0" @@ -2139,10 +2139,10 @@ parse-filepath "^1.0.2" tslib "~2.3.0" -"@graphql-codegen/plugin-helpers@^2.3.2", "@graphql-codegen/plugin-helpers@^2.4.0": - version "2.4.1" - resolved "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-2.4.1.tgz#433845a89b0b4b3a2a0e959e0a2cfe444cf7aeac" - integrity sha512-OPMma7aUnES3Dh+M0BfiNBnJLmYuH60EnbULAhufxFDn/Y2OA0Ht/LQok9beX6VN4ASZEMCOAGItJezGJr5DJw== +"@graphql-codegen/plugin-helpers@^2.3.2", "@graphql-codegen/plugin-helpers@^2.4.0", "@graphql-codegen/plugin-helpers@^2.4.1": + version "2.4.2" + resolved "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-2.4.2.tgz#e4f6b74dddcf8a9974fef5ce48562ae0980f9fed" + integrity sha512-LJNvwAPv/sKtI3RnRDm+nPD+JeOfOuSOS4FFIpQCMUCyMnFcchV/CPTTv7tT12fLUpEg6XjuFfDBvOwndti30Q== dependencies: "@graphql-tools/utils" "^8.5.2" change-case-all "1.0.14" @@ -2385,7 +2385,7 @@ unixify "1.0.0" valid-url "1.0.9" -"@graphql-tools/load@^7.3.0": +"@graphql-tools/load@^7.3.0", "@graphql-tools/load@^7.4.1": version "7.5.1" resolved "https://registry.npmjs.org/@graphql-tools/load/-/load-7.5.1.tgz#8c7f846d2185ddc1d44fdfbf1ed9cb678f69e40b" integrity sha512-j9XcLYZPZdl/TzzqA83qveJmwcCxgGizt5L1+C1/Z68brTEmQHLdQCOR3Ma3ewESJt6DU05kSTu2raKaunkjRg== @@ -2395,16 +2395,6 @@ p-limit "3.1.0" tslib "~2.3.0" -"@graphql-tools/load@^7.4.1": - version "7.4.1" - resolved "https://registry.npmjs.org/@graphql-tools/load/-/load-7.4.1.tgz#aa572fcef11d6028097b6ef39c13fa9d62e5a441" - integrity sha512-UvBodW5hRHpgBUBVz5K5VIhJDOTFIbRRAGD6sQ2l9J5FDKBEs3u/6JjZDzbdL96br94D5cEd2Tk6auaHpTn7mQ== - dependencies: - "@graphql-tools/schema" "8.3.1" - "@graphql-tools/utils" "^8.5.1" - p-limit "3.1.0" - tslib "~2.3.0" - "@graphql-tools/merge@^6.0.0", "@graphql-tools/merge@^6.2.12": version "6.2.14" resolved "https://registry.npmjs.org/@graphql-tools/merge/-/merge-6.2.14.tgz#694e2a2785ba47558e5665687feddd2935e9d94e" @@ -2518,7 +2508,7 @@ valid-url "1.0.9" ws "7.4.5" -"@graphql-tools/url-loader@^7.0.11": +"@graphql-tools/url-loader@^7.0.11", "@graphql-tools/url-loader@^7.4.2": version "7.7.0" resolved "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-7.7.0.tgz#504f0030c75b61bca4ac07da49e8cd872c316972" integrity sha512-mBBb+aJqI4E0MVEzyfi76Pi/G6lGxGTVt/tP1YtKJly7UnonNoWOtDusdL3zIVAGhGgLsNrLbGhLDbwSd6TV6A== @@ -2543,32 +2533,7 @@ value-or-promise "^1.0.11" ws "^8.3.0" -"@graphql-tools/url-loader@^7.4.2": - version "7.5.3" - resolved "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-7.5.3.tgz#a594be40e3bc68d22f76746356e7f0b8117b7137" - integrity sha512-VKMRJ4TOeVIdulkCLGSBUr4stRRwOGcVRXDeoUF+86K32Ufo0H2V0lz7QwS/bCl8GXV19FMgHZCDl4BMJyOXEA== - dependencies: - "@graphql-tools/delegate" "^8.4.1" - "@graphql-tools/utils" "^8.5.1" - "@graphql-tools/wrap" "^8.3.1" - "@n1ru4l/graphql-live-query" "0.9.0" - "@types/websocket" "1.0.4" - "@types/ws" "^8.0.0" - cross-undici-fetch "^0.0.26" - dset "^3.1.0" - extract-files "11.0.0" - graphql-sse "^1.0.1" - graphql-ws "^5.4.1" - isomorphic-ws "4.0.1" - meros "1.1.4" - subscriptions-transport-ws "^0.11.0" - sync-fetch "0.3.1" - tslib "~2.3.0" - valid-url "1.0.9" - value-or-promise "1.0.11" - ws "8.3.0" - -"@graphql-tools/utils@8.5.3", "@graphql-tools/utils@^8.5.1", "@graphql-tools/utils@^8.5.3": +"@graphql-tools/utils@8.5.3": version "8.5.3" resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.5.3.tgz#404062e62cae9453501197039687749c4885356e" integrity sha512-HDNGWFVa8QQkoQB0H1lftvaO1X5xUaUDk1zr1qDe0xN1NL0E/CrQdJ5UKLqOvH4hkqVUPxQsyOoAZFkaH6rLHg== @@ -2591,7 +2556,7 @@ camel-case "4.1.2" tslib "~2.2.0" -"@graphql-tools/utils@^8.1.1", "@graphql-tools/utils@^8.3.0", "@graphql-tools/utils@^8.5.2", "@graphql-tools/utils@^8.6.0": +"@graphql-tools/utils@^8.1.1", "@graphql-tools/utils@^8.3.0", "@graphql-tools/utils@^8.5.1", "@graphql-tools/utils@^8.5.2", "@graphql-tools/utils@^8.5.3", "@graphql-tools/utils@^8.6.0": version "8.6.0" resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.6.0.tgz#f424256a1f3b87d1dcf6f9f675739b2d3627be33" integrity sha512-rnk+RHaOCeWnfekeQGRh5ycXK1ZAI7Nm0pbeLjA3SiysTdqhWyxNCp5ON4Mvtlid84OY/KB253fQq/2rotznCA== @@ -4198,7 +4163,7 @@ outvariant "^1.2.0" strict-event-emitter "^0.2.0" -"@n1ru4l/graphql-live-query@0.9.0", "@n1ru4l/graphql-live-query@^0.9.0": +"@n1ru4l/graphql-live-query@^0.9.0": version "0.9.0" resolved "https://registry.npmjs.org/@n1ru4l/graphql-live-query/-/graphql-live-query-0.9.0.tgz#defaebdd31f625bee49e6745934f36312532b2bc" integrity sha512-BTpWy1e+FxN82RnLz4x1+JcEewVdfmUhV1C6/XYD5AjS7PQp9QFF7K8bCD6gzPTr2l+prvqOyVueQhFJxB1vfg== @@ -6722,7 +6687,7 @@ dependencies: "@types/node" "*" -"@types/websocket@1.0.4", "@types/websocket@^1.0.4": +"@types/websocket@^1.0.4": version "1.0.4" resolved "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.4.tgz#1dc497280d8049a5450854dd698ee7e6ea9e60b8" integrity sha512-qn1LkcFEKK8RPp459jkjzsfpbsx36BBt3oC3pITYtkoBw/aVX+EZFa5j3ThCRTNpLFvIMr5dSTD4RaMdilIOpA== @@ -9881,16 +9846,6 @@ cross-undici-fetch@^0.0.20: node-fetch "^2.6.5" undici "^4.9.3" -cross-undici-fetch@^0.0.26: - version "0.0.26" - resolved "https://registry.npmjs.org/cross-undici-fetch/-/cross-undici-fetch-0.0.26.tgz#29d93d56609f4d2334f9d5333d23ef7a242842a7" - integrity sha512-aMDRrLbWr0TGXfY92stlV+XOGpskeqFmWmrKSWsnc8w6gK5LPE83NBh7O7N6gCb2xjwHcm1Yn2nBXMEVH2RBcA== - dependencies: - abort-controller "^3.0.0" - form-data "^4.0.0" - node-fetch "^2.6.5" - undici "^4.9.3" - cross-undici-fetch@^0.1.4: version "0.1.13" resolved "https://registry.npmjs.org/cross-undici-fetch/-/cross-undici-fetch-0.1.13.tgz#807d17ce5c524c21bc0a6486e97ecccb901c6529" @@ -12315,16 +12270,16 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-files@11.0.0, extract-files@^11.0.0: - version "11.0.0" - resolved "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz#b72d428712f787eef1f5193aff8ab5351ca8469a" - integrity sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ== - extract-files@9.0.0, extract-files@^9.0.0: version "9.0.0" resolved "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz#8a7744f2437f81f5ed3250ed9f1550de902fe54a" integrity sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ== +extract-files@^11.0.0: + version "11.0.0" + resolved "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz#b72d428712f787eef1f5193aff8ab5351ca8469a" + integrity sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ== + extract-zip@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" @@ -17913,6 +17868,13 @@ minimatch@5.0.0, minimatch@^5.0.0: dependencies: brace-expansion "^2.0.1" +minimatch@^4.0.0: + version "4.2.1" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" + integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== + dependencies: + brace-expansion "^1.1.7" + minimist-options@4.1.0, minimist-options@^4.0.2: version "4.1.0" resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -25614,16 +25576,16 @@ ws@7.4.5: resolved "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz#a484dd851e9beb6fdb420027e3885e8ce48986c1" integrity sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g== -ws@8.3.0, "ws@^5.2.0 || ^6.0.0 || ^7.0.0", ws@^7.2.3, ws@^7.3.1, ws@^7.4.6, ws@^8.3.0: - version "7.5.6" - resolved "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" - integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== - ws@8.5.0, ws@^8.1.0: version "8.5.0" resolved "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== +"ws@^5.2.0 || ^6.0.0 || ^7.0.0", ws@^7.2.3, ws@^7.3.1, ws@^7.4.6, ws@^8.3.0: + version "7.5.6" + resolved "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" + integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== + ws@~7.4.2: version "7.4.6" resolved "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" From 7b058b37db2db6e680e696880de2bfc2424ffb56 Mon Sep 17 00:00:00 2001 From: Peiman Jafari Date: Thu, 3 Mar 2022 10:16:43 -0800 Subject: [PATCH 175/353] make new inputs optional Signed-off-by: Peiman Jafari --- plugins/scaffolder-backend/api-report.md | 8 ++++---- .../src/scaffolder/actions/builtin/publish/github.test.ts | 4 ---- .../src/scaffolder/actions/builtin/publish/github.ts | 8 ++++---- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 3328a11343..b18e199f40 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -233,10 +233,10 @@ export function createPublishGithubAction(options: { description?: string | undefined; access?: string | undefined; defaultBranch?: string | undefined; - deleteBranchOnMerge: boolean; - allowRebaseMerge: boolean; - allowSquashMerge: boolean; - allowMergeCommit: boolean; + deleteBranchOnMerge?: boolean | undefined; + allowRebaseMerge?: boolean | undefined; + allowSquashMerge?: boolean | undefined; + allowMergeCommit?: boolean | undefined; sourcePath?: string | undefined; requireCodeOwnerReviews?: boolean | undefined; repoVisibility?: 'internal' | 'private' | 'public' | undefined; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index e997c30a32..ec564e93dc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -54,10 +54,6 @@ describe('publish:github', () => { description: 'description', repoVisibility: 'private' as const, access: 'owner/blam', - deleteBranchOnMerge: false, - allowMergeCommit: true, - allowSquashMerge: true, - allowRebaseMerge: true, }, workspacePath: 'lol', logger: getVoidLogger(), diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index 32969e1be3..7f1e7a1c93 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -46,10 +46,10 @@ export function createPublishGithubAction(options: { description?: string; access?: string; defaultBranch?: string; - deleteBranchOnMerge: boolean; - allowRebaseMerge: boolean; - allowSquashMerge: boolean; - allowMergeCommit: boolean; + deleteBranchOnMerge?: boolean; + allowRebaseMerge?: boolean; + allowSquashMerge?: boolean; + allowMergeCommit?: boolean; sourcePath?: string; requireCodeOwnerReviews?: boolean; repoVisibility?: 'private' | 'internal' | 'public'; From d1d488e371ae089fc5941c55643ee53dbac7209c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 3 Mar 2022 18:49:20 +0100 Subject: [PATCH 176/353] update the common validators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/slow-vans-rhyme.md | 5 +++++ .changeset/ten-rats-join.md | 8 ++++++++ packages/catalog-model/api-report.md | 3 +++ .../entity/policies/FieldFormatEntityPolicy.ts | 3 ++- .../validation/CommonValidatorFunctions.test.ts | 16 ++++++++++++++++ .../src/validation/CommonValidatorFunctions.ts | 13 ++++++++++++- .../src/validation/makeValidator.ts | 11 +++++++++-- 7 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 .changeset/slow-vans-rhyme.md create mode 100644 .changeset/ten-rats-join.md diff --git a/.changeset/slow-vans-rhyme.md b/.changeset/slow-vans-rhyme.md new file mode 100644 index 0000000000..6de924899f --- /dev/null +++ b/.changeset/slow-vans-rhyme.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': minor +--- + +**BREAKING**: The default validator for `metadata.tags` now permits the colon (`:`) character as well. diff --git a/.changeset/ten-rats-join.md b/.changeset/ten-rats-join.md new file mode 100644 index 0000000000..6aa97be581 --- /dev/null +++ b/.changeset/ten-rats-join.md @@ -0,0 +1,8 @@ +--- +'@backstage/catalog-model': patch +--- + +**DEPRECATION**: + +- Deprecated `CommonValidatorFunctions.isValidString`, please use `isNonEmptyString` instead which is equivalent but better named. +- Deprecated `CommonValidatorFunctions.isValidTag`, with no replacement. Its purpose was too specific and not reusable, so it will be removed. diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 99b4b39038..9a65401d55 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -51,6 +51,7 @@ export const apiEntityV1alpha1Validator: KindValidator; // @public export class CommonValidatorFunctions { static isJsonSafe(value: unknown): boolean; + static isNonEmptyString(value: unknown): value is string; static isValidDnsLabel(value: unknown): boolean; static isValidDnsSubdomain(value: unknown): boolean; static isValidPrefixAndOrSuffix( @@ -59,7 +60,9 @@ export class CommonValidatorFunctions { isValidPrefix: (value: string) => boolean, isValidSuffix: (value: string) => boolean, ): boolean; + // @deprecated static isValidString(value: unknown): boolean; + // @deprecated static isValidTag(value: unknown): boolean; static isValidUrl(value: unknown): boolean; } diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index 51059328a6..22d0e5e997 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -95,6 +95,7 @@ export class FieldFormatEntityPolicy implements EntityPolicy { expectation = 'a string that is a valid url'; break; case 'isValidString': + case 'isNonEmptyString': expectation = 'a non empty string'; break; default: @@ -156,7 +157,7 @@ export class FieldFormatEntityPolicy implements EntityPolicy { optional( `links.${i}.title`, links[i]?.title, - CommonValidatorFunctions.isValidString, + CommonValidatorFunctions.isNonEmptyString, ); optional( `links.${i}.icon`, diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts index 88e3f9b002..9a9ec8bcda 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts @@ -226,4 +226,20 @@ describe('CommonValidatorFunctions', () => { ])(`isValidString %p ? %p`, (value, result) => { expect(CommonValidatorFunctions.isValidString(value)).toBe(result); }); + + it.each([ + [null, false], + [true, false], + [7, false], + [{}, false], + ['', false], + [' ', false], + [' ', false], + ['abc', true], + [' abc ', true], + ['abc xyz', true], + ['abc xyz abc.', true], + ])(`isNonEmptyString %p ? %p`, (value, result) => { + expect(CommonValidatorFunctions.isNonEmptyString(value)).toBe(result); + }); }); diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts index d0357660fc..8c814412c1 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts @@ -98,6 +98,7 @@ export class CommonValidatorFunctions { /** * Checks that the value is a valid tag. * + * @deprecated This will be removed in a future release * @param value - The value to check */ static isValidTag(value: unknown): boolean { @@ -110,7 +111,7 @@ export class CommonValidatorFunctions { } /** - * Checks that the value is a valid URL. + * Checks that the value is a valid string URL. * * @param value - The value to check */ @@ -131,9 +132,19 @@ export class CommonValidatorFunctions { /** * Checks that the value is a non empty string value. * + * @deprecated use isNonEmptyString instead * @param value - The value to check */ static isValidString(value: unknown): boolean { return typeof value === 'string' && value?.trim()?.length >= 1; } + + /** + * Checks that the value is a string value that's not empty. + * + * @param value - The value to check + */ + static isNonEmptyString(value: unknown): value is string { + return typeof value === 'string' && value?.trim()?.length >= 1; + } } diff --git a/packages/catalog-model/src/validation/makeValidator.ts b/packages/catalog-model/src/validation/makeValidator.ts index ad3f52e563..c3c7182680 100644 --- a/packages/catalog-model/src/validation/makeValidator.ts +++ b/packages/catalog-model/src/validation/makeValidator.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { CommonValidatorFunctions } from './CommonValidatorFunctions'; import { KubernetesValidatorFunctions } from './KubernetesValidatorFunctions'; import { Validators } from './types'; @@ -27,7 +26,15 @@ const defaultValidators: Validators = { isValidLabelValue: KubernetesValidatorFunctions.isValidLabelValue, isValidAnnotationKey: KubernetesValidatorFunctions.isValidAnnotationKey, isValidAnnotationValue: KubernetesValidatorFunctions.isValidAnnotationValue, - isValidTag: CommonValidatorFunctions.isValidTag, + isValidTag: (value: unknown): boolean => { + // NOTE(freben): This one is a bit of an oddball and doesn't fit well anywhere to delegate to, so it's just inlined for now. + return ( + typeof value === 'string' && + value.length >= 1 && + value.length <= 63 && + /^[a-z0-9:+#]+(\-[a-z0-9:+#]+)*$/.test(value) + ); + }, }; /** From f06da37290e6a0567da120cc3d2a862e7544b505 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 3 Mar 2022 20:14:05 +0100 Subject: [PATCH 177/353] cli: always use main entry point in packages for backend development Signed-off-by: Patrik Oldsberg --- .changeset/spotty-swans-run.md | 5 +++++ packages/cli/src/lib/bundler/config.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/spotty-swans-run.md diff --git a/.changeset/spotty-swans-run.md b/.changeset/spotty-swans-run.md new file mode 100644 index 0000000000..7a16343d69 --- /dev/null +++ b/.changeset/spotty-swans-run.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The backend development setup now ignores the `"browser"` and `"module"` entry points in `package.json`, and instead always uses `"main"`. diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index bfd05807ad..575af4ea8b 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -272,7 +272,7 @@ export async function createBackendConfig( ], resolve: { extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'], - mainFields: ['browser', 'module', 'main'], + mainFields: ['main'], modules: [paths.rootNodeModules, ...moduleDirs], plugins: [ new LinkedPackageResolvePlugin(paths.rootNodeModules, externalPkgs), From dc2a5c307c79d2c8cd4422ce5b1dd47187529652 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Thu, 3 Mar 2022 14:25:28 -0800 Subject: [PATCH 178/353] Missing async from `getCollator` causes type mismatch Signed-off-by: Taras Mankovski --- docs/features/search/how-to-guides.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/search/how-to-guides.md b/docs/features/search/how-to-guides.md index ac36d64673..0a5f53d2f2 100644 --- a/docs/features/search/how-to-guides.md +++ b/docs/features/search/how-to-guides.md @@ -219,7 +219,7 @@ export class YourCollatorFactory implements DocumentCollatorFactory { }; } } - getCollator() { + async getCollator() { return Readable.from(this.execute()); } } From c51587701b1f386e7e023fd278f25344e4f9ca11 Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Thu, 3 Mar 2022 23:48:10 +0000 Subject: [PATCH 179/353] Remove all `useState` from EntityAirbrakeWidget.tsx Signed-off-by: Karan Shah --- plugins/airbrake/package.json | 1 + plugins/airbrake/src/api/AirbrakeApi.ts | 3 +- .../EntityAirbrakeWidget.test.tsx | 28 ++++------ .../EntityAirbrakeWidget.tsx | 55 ++++++++----------- 4 files changed, 37 insertions(+), 50 deletions(-) diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index 7342fdd839..47dc6b31c9 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -27,6 +27,7 @@ "@backstage/core-components": "^0.8.10", "@backstage/core-plugin-api": "^0.7.0", "@backstage/dev-utils": "^0.2.23", + "@backstage/errors": "^0.2.2", "@backstage/plugin-catalog-react": "^0.7.0", "@backstage/test-utils": "^0.2.6", "@backstage/theme": "^0.2.15", diff --git a/plugins/airbrake/src/api/AirbrakeApi.ts b/plugins/airbrake/src/api/AirbrakeApi.ts index 672b1f92f4..3abe745b5b 100644 --- a/plugins/airbrake/src/api/AirbrakeApi.ts +++ b/plugins/airbrake/src/api/AirbrakeApi.ts @@ -16,6 +16,7 @@ import { Groups } from './airbrakeGroups'; import { createApiRef } from '@backstage/core-plugin-api'; +import { CustomErrorBase } from '@backstage/errors'; export const airbrakeApiRef = createApiRef({ id: 'plugin.airbrake.service', @@ -25,7 +26,7 @@ export interface AirbrakeApi { fetchGroups(projectId: string): Promise; } -export class NoProjectIdError extends Error { +export class NoProjectIdError extends CustomErrorBase { constructor() { super('Project ID is not present'); } diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx index 64f1f11223..e8d09b5e65 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.test.tsx @@ -18,7 +18,6 @@ import React from 'react'; import { EntityAirbrakeWidget } from './EntityAirbrakeWidget'; import exampleData from '../../api/mock/airbrakeGroupsApiMock.json'; import { - MockErrorApi, renderInTestApp, setupRequestMockHandlers, TestApiProvider, @@ -30,7 +29,6 @@ import { MockAirbrakeApi, ProductionAirbrakeApi, } from '../../api'; -import { errorApiRef } from '@backstage/core-plugin-api'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -53,14 +51,9 @@ describe('EntityAirbrakeWidget', () => { }); it('states that the annotation is missing if no project ID annotation is provided but does not error', async () => { - const mockErrorApi = new MockErrorApi({ collect: true }); - const widget = await renderInTestApp( , @@ -68,7 +61,9 @@ describe('EntityAirbrakeWidget', () => { await expect( widget.findByText('Missing Annotation'), ).resolves.toBeInTheDocument(); - expect(mockErrorApi.getErrors().length).toBe(0); + expect( + widget.queryByText(/.*Failed fetching Airbrake groups.*/), + ).not.toBeInTheDocument(); }); it('states that an error occurred if the API call fails', async () => { @@ -80,14 +75,10 @@ describe('EntityAirbrakeWidget', () => { }, ), ); - const mockErrorApi = new MockErrorApi({ collect: true }); const widget = await renderInTestApp( , @@ -96,9 +87,10 @@ describe('EntityAirbrakeWidget', () => { await expect( widget.findByText(/.*there was an issue communicating with Airbrake.*/), ).resolves.toBeInTheDocument(); - expect(mockErrorApi.getErrors().length).toBe(1); - expect(mockErrorApi.getErrors()[0].error.message).toStrictEqual( - 'Failed fetching Airbrake groups', - ); + expect( + widget.getByRole('heading', { + name: /.*Failed fetching Airbrake groups.*/, + }), + ).toBeInTheDocument(); }); }); diff --git a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx index 29260427a9..f235a3e2f5 100644 --- a/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx +++ b/plugins/airbrake/src/components/EntityAirbrakeWidget/EntityAirbrakeWidget.tsx @@ -14,10 +14,11 @@ * limitations under the License. */ import { Entity } from '@backstage/catalog-model'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { Grid, Typography } from '@material-ui/core'; import { EmptyState, + ErrorPanel, InfoCard, MissingAnnotationEmptyState, Progress, @@ -25,7 +26,7 @@ import { import hash from 'object-hash'; import { makeStyles } from '@material-ui/core/styles'; import { BackstageTheme } from '@backstage/theme'; -import { ErrorApi, errorApiRef, useApi } from '@backstage/core-plugin-api'; +import { useApi } from '@backstage/core-plugin-api'; import { airbrakeApiRef } from '../../api'; import useAsync from 'react-use/lib/useAsync'; import { AIRBRAKE_PROJECT_ID_ANNOTATION, useProjectId } from '../useProjectId'; @@ -47,33 +48,22 @@ export const EntityAirbrakeWidget = ({ entity }: { entity: Entity }) => { const classes = useStyles(); const projectId = useProjectId(entity); - const errorApi = useApi(errorApiRef); const airbrakeApi = useApi(airbrakeApiRef); - const [componentState, setComponentState] = useState( - ComponentState.Loading, - ); + let componentState = ComponentState.Loading; - const { loading, value } = useAsync(async () => { - try { - const result = await airbrakeApi.fetchGroups(projectId); - setComponentState(ComponentState.Loaded); - return result; - } catch (e) { - if (!projectId) { - setComponentState(ComponentState.NoProjectId); - } else { - setComponentState(ComponentState.Error); - errorApi.post(e); - } - throw e; - } - }, [airbrakeApi, errorApi, projectId]); + const { loading, value, error } = useAsync(() => { + return airbrakeApi.fetchGroups(projectId); + }, [airbrakeApi, projectId]); - useEffect(() => { - if (loading) { - setComponentState(ComponentState.Loading); - } - }, [loading]); + if (loading) { + componentState = ComponentState.Loading; + } else if (!projectId) { + componentState = ComponentState.NoProjectId; + } else if (error) { + componentState = ComponentState.Error; + } else if (value) { + componentState = ComponentState.Loaded; + } switch (componentState) { case ComponentState.Loaded: @@ -103,11 +93,14 @@ export const EntityAirbrakeWidget = ({ entity }: { entity: Entity }) => { case ComponentState.Error: default: return ( - + <> + + + ); } }; From d3d1b82198d2974bd078e02d071d6bff87165bad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Mar 2022 04:13:11 +0000 Subject: [PATCH 180/353] chore(deps): bump minimatch from 5.0.0 to 5.0.1 Bumps [minimatch](https://github.com/isaacs/minimatch) from 5.0.0 to 5.0.1. - [Release notes](https://github.com/isaacs/minimatch/releases) - [Commits](https://github.com/isaacs/minimatch/compare/v5.0.0...v5.0.1) --- updated-dependencies: - dependency-name: minimatch dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .changeset/dependabot-4014eb7.md | 5 +++++ packages/cli/package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .changeset/dependabot-4014eb7.md diff --git a/.changeset/dependabot-4014eb7.md b/.changeset/dependabot-4014eb7.md new file mode 100644 index 0000000000..7b4d64523a --- /dev/null +++ b/.changeset/dependabot-4014eb7.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +chore(deps): bump `minimatch` from 5.0.0 to 5.0.1 diff --git a/packages/cli/package.json b/packages/cli/package.json index 26b941c699..af2c3afb2d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -89,7 +89,7 @@ "json-schema": "^0.4.0", "jest-transform-yaml": "^1.0.0", "lodash": "^4.17.21", - "minimatch": "5.0.0", + "minimatch": "5.0.1", "mini-css-extract-plugin": "^2.4.2", "npm-packlist": "^3.0.0", "node-libs-browser": "^2.2.1", diff --git a/yarn.lock b/yarn.lock index 48784ac13c..e599c0ca31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17906,10 +17906,10 @@ minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimatch@5.0.0, minimatch@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.0.0.tgz#281d8402aaaeed18a9e8406ad99c46a19206c6ef" - integrity sha512-EU+GCVjXD00yOUf1TwAHVP7v3fBD3A8RkkPYsWWKGWesxM/572sL53wJQnHxquHlRhYUV36wHkqrN8cdikKc2g== +minimatch@5.0.1, minimatch@^5.0.0: + version "5.0.1" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== dependencies: brace-expansion "^2.0.1" From 23568dd328a7cf3879eda51d176e3aac42f2ae5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Mar 2022 04:13:40 +0000 Subject: [PATCH 181/353] chore(deps): bump @react-hookz/web from 12.3.0 to 13.0.0 Bumps [@react-hookz/web](https://github.com/react-hookz/web) from 12.3.0 to 13.0.0. - [Release notes](https://github.com/react-hookz/web/releases) - [Changelog](https://github.com/react-hookz/web/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hookz/web/compare/v12.3.0...v13.0.0) --- updated-dependencies: - dependency-name: "@react-hookz/web" dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .changeset/dependabot-ae5fb9c.md | 6 ++++++ packages/core-components/package.json | 2 +- plugins/gcp-projects/package.json | 2 +- yarn.lock | 7 +++++++ 4 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 .changeset/dependabot-ae5fb9c.md diff --git a/.changeset/dependabot-ae5fb9c.md b/.changeset/dependabot-ae5fb9c.md new file mode 100644 index 0000000000..5648da2be7 --- /dev/null +++ b/.changeset/dependabot-ae5fb9c.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-gcp-projects': patch +--- + +chore(deps): bump `@react-hookz/web` from 12.3.0 to 13.0.0 diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 93fa015b89..4e896fd178 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -65,7 +65,7 @@ "react-syntax-highlighter": "^15.4.5", "react-text-truncate": "^0.18.0", "react-use": "^17.3.2", - "@react-hookz/web": "^12.3.0", + "@react-hookz/web": "^13.0.0", "react-virtualized-auto-sizer": "^1.0.6", "react-window": "^1.8.6", "remark-gfm": "^3.0.1", diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 3cb3be5382..b56565ab22 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -41,7 +41,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "react-router-dom": "6.0.0-beta.0", - "@react-hookz/web": "^12.3.0" + "@react-hookz/web": "^13.0.0" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0" diff --git a/yarn.lock b/yarn.lock index 48784ac13c..5f6da29a39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4851,6 +4851,13 @@ dependencies: "@react-hookz/deep-equal" "^1.0.1" +"@react-hookz/web@^13.0.0": + version "13.0.0" + resolved "https://registry.npmjs.org/@react-hookz/web/-/web-13.0.0.tgz#7c4d54fb4c1edf885879914d719e35086964e46e" + integrity sha512-sAMTOiOnAvxpJtHea7Vk+2+KJdTC/o77wPII+glnH+astNUSSiduptLaMPeoIXXwqjfJT+IJ9JzWyZPrLpdyFw== + dependencies: + "@react-hookz/deep-equal" "^1.0.1" + "@rjsf/core@^3.2.1": version "3.2.1" resolved "https://registry.npmjs.org/@rjsf/core/-/core-3.2.1.tgz#8a7b24c9a6f01f0ecb093fdfc777172c12b1b009" From 91bf1e6c1ac760e7b0fa7da869bbb5d586eb6063 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 3 Mar 2022 13:33:47 +0100 Subject: [PATCH 182/353] Use @backstage/plugin-techdocs-node instead of @backstage/techdocs-common Signed-off-by: Eric Peterson --- .changeset/techdocs-node-one.md | 6 ++++++ .github/CODEOWNERS | 2 +- docs/features/techdocs/cli.md | 2 +- docs/getting-started/project-structure.md | 4 ++-- packages/techdocs-cli/package.json | 2 +- .../techdocs-cli/src/commands/generate/generate.ts | 6 +++--- packages/techdocs-cli/src/commands/index.ts | 2 +- packages/techdocs-cli/src/commands/migrate/migrate.ts | 2 +- packages/techdocs-cli/src/commands/publish/publish.ts | 2 +- packages/techdocs-cli/src/lib/utility.ts | 2 +- plugins/techdocs-backend/README.md | 2 +- plugins/techdocs-backend/api-report.md | 10 +++++----- plugins/techdocs-backend/package.json | 2 +- plugins/techdocs-backend/src/DocsBuilder/builder.ts | 2 +- plugins/techdocs-backend/src/index.ts | 6 +++--- .../src/search/DefaultTechDocsCollator.ts | 2 +- .../src/search/DefaultTechDocsCollatorFactory.ts | 2 +- .../src/service/DocsSynchronizer.test.ts | 2 +- .../techdocs-backend/src/service/DocsSynchronizer.ts | 2 +- plugins/techdocs-backend/src/service/router.test.ts | 2 +- plugins/techdocs-backend/src/service/router.ts | 2 +- .../techdocs-backend/src/service/standaloneServer.ts | 2 +- plugins/techdocs-node/CHANGELOG.md | 2 +- plugins/techdocs-node/README.md | 10 +++++----- plugins/techdocs-node/api-report.md | 2 +- plugins/techdocs-node/package.json | 6 +++--- plugins/techdocs-node/src/stages/generate/techdocs.ts | 2 +- plugins/techdocs-node/src/stages/publish/awsS3.test.ts | 2 +- .../src/stages/publish/azureBlobStorage.test.ts | 2 +- .../src/stages/publish/googleStorage.test.ts | 2 +- plugins/techdocs-node/src/stages/publish/local.ts | 2 +- .../src/stages/publish/openStackSwift.test.ts | 2 +- plugins/techdocs-node/src/stages/publish/types.ts | 2 +- scripts/api-extractor.ts | 1 + 34 files changed, 54 insertions(+), 47 deletions(-) create mode 100644 .changeset/techdocs-node-one.md diff --git a/.changeset/techdocs-node-one.md b/.changeset/techdocs-node-one.md new file mode 100644 index 0000000000..706c708c4a --- /dev/null +++ b/.changeset/techdocs-node-one.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-techdocs-backend': patch +'@techdocs/cli': patch +--- + +Use `@backstage/plugin-techdocs-node` package instead of `@backstage/techdocs-common`. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1a0f827fb1..44c61f9348 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -46,7 +46,7 @@ /plugins/search-* @backstage/reviewers @backstage/techdocs-core /plugins/sonarqube @backstage/reviewers @backstage/sda-se-reviewers /plugins/techdocs @backstage/reviewers @backstage/techdocs-core -/plugins/techdocs-backend @backstage/reviewers @backstage/techdocs-core +/plugins/techdocs-* @backstage/reviewers @backstage/techdocs-core /tech-insights-backend @backstage/reviewers @xantier @iain-b /tech-insights-backend-module-jsonfc @backstage/reviewers @xantier @iain-b /tech-insights-tech-insights-common @backstage/reviewers @xantier @iain-b diff --git a/docs/features/techdocs/cli.md b/docs/features/techdocs/cli.md index 2a11e1181a..6efb83549e 100644 --- a/docs/features/techdocs/cli.md +++ b/docs/features/techdocs/cli.md @@ -97,7 +97,7 @@ techdocs-cli generate Alias: `techdocs-cli build` The generate command uses the -[`@backstage/techdocs-common`](https://github.com/backstage/backstage/tree/master/packages/techdocs-common) +[`@backstage/plugin-techdocs-node`](https://github.com/backstage/backstage/tree/master/plugins/techdocs-node) package from Backstage for consistency. A Backstage app can also generate and publish TechDocs sites if `techdocs.builder` is set to `'local'` in `app-config.yaml`. See diff --git a/docs/getting-started/project-structure.md b/docs/getting-started/project-structure.md index 6c49c798d0..b67e847272 100644 --- a/docs/getting-started/project-structure.md +++ b/docs/getting-started/project-structure.md @@ -160,8 +160,8 @@ are separated out into their own folder, see further down. reusable React components. Stories are within the core package, and are published in the [Backstage Storybook](https://backstage.io/storybook). -- [`techdocs-common/`](https://github.com/backstage/backstage/tree/master/packages/techdocs-common) - - Common functionalities for TechDocs, to be shared between +- [`techdocs-node/`](https://github.com/backstage/backstage/tree/master/plugins/techdocs-node) - + Common node.js functionalities for TechDocs, to be shared between [techdocs-backend](https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend) plugin and [techdocs-cli](https://github.com/backstage/techdocs-cli). diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 32eb70602c..e6cfab0916 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -66,7 +66,7 @@ "@backstage/catalog-model": "^0.12.0", "@backstage/cli-common": "^0.1.8", "@backstage/config": "^0.1.15", - "@backstage/techdocs-common": "^0.11.11", + "@backstage/plugin-techdocs-node": "^0.11.11", "@types/dockerode": "^3.3.0", "commander": "^6.1.0", "dockerode": "^3.3.1", diff --git a/packages/techdocs-cli/src/commands/generate/generate.ts b/packages/techdocs-cli/src/commands/generate/generate.ts index 0f5213df34..98529698ca 100644 --- a/packages/techdocs-cli/src/commands/generate/generate.ts +++ b/packages/techdocs-cli/src/commands/generate/generate.ts @@ -21,7 +21,7 @@ import Docker from 'dockerode'; import { TechdocsGenerator, ParsedLocationAnnotation, -} from '@backstage/techdocs-common'; +} from '@backstage/plugin-techdocs-node'; import { DockerContainerRunner } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { @@ -31,7 +31,7 @@ import { import { stdout } from 'process'; export default async function generate(cmd: Command) { - // Use techdocs-common package to generate docs. Keep consistency between Backstage and CI generating docs. + // Use techdocs-node package to generate docs. Keep consistency between Backstage and CI generating docs. // Docs can be prepared using actions/checkout or git clone, or similar paradigms on CI. The TechDocs CI workflow // will run on the CI pipeline containing the documentation files. @@ -78,7 +78,7 @@ export default async function generate(cmd: Command) { } } - // Generate docs using @backstage/techdocs-common + // Generate docs using @backstage/plugin-techdocs-node const techdocsGenerator = await TechdocsGenerator.fromConfig(config, { logger, containerRunner, diff --git a/packages/techdocs-cli/src/commands/index.ts b/packages/techdocs-cli/src/commands/index.ts index 6f1d9847e1..6ad62c5668 100644 --- a/packages/techdocs-cli/src/commands/index.ts +++ b/packages/techdocs-cli/src/commands/index.ts @@ -15,7 +15,7 @@ */ import { CommanderStatic } from 'commander'; -import { TechdocsGenerator } from '@backstage/techdocs-common'; +import { TechdocsGenerator } from '@backstage/plugin-techdocs-node'; const defaultDockerImage = TechdocsGenerator.defaultDockerImage; diff --git a/packages/techdocs-cli/src/commands/migrate/migrate.ts b/packages/techdocs-cli/src/commands/migrate/migrate.ts index 1cd8fca65b..067bf1f173 100644 --- a/packages/techdocs-cli/src/commands/migrate/migrate.ts +++ b/packages/techdocs-cli/src/commands/migrate/migrate.ts @@ -15,7 +15,7 @@ */ import { SingleHostDiscovery } from '@backstage/backend-common'; -import { Publisher } from '@backstage/techdocs-common'; +import { Publisher } from '@backstage/plugin-techdocs-node'; import { Command } from 'commander'; import { createLogger } from '../../lib/utility'; import { PublisherConfig } from '../../lib/PublisherConfig'; diff --git a/packages/techdocs-cli/src/commands/publish/publish.ts b/packages/techdocs-cli/src/commands/publish/publish.ts index 11a47d4a71..c84180b032 100644 --- a/packages/techdocs-cli/src/commands/publish/publish.ts +++ b/packages/techdocs-cli/src/commands/publish/publish.ts @@ -18,7 +18,7 @@ import { resolve } from 'path'; import { Command } from 'commander'; import { createLogger } from '../../lib/utility'; import { SingleHostDiscovery } from '@backstage/backend-common'; -import { Publisher } from '@backstage/techdocs-common'; +import { Publisher } from '@backstage/plugin-techdocs-node'; import { Entity } from '@backstage/catalog-model'; import { PublisherConfig } from '../../lib/PublisherConfig'; diff --git a/packages/techdocs-cli/src/lib/utility.ts b/packages/techdocs-cli/src/lib/utility.ts index 87dbbfd08d..3f91f3c82d 100644 --- a/packages/techdocs-cli/src/lib/utility.ts +++ b/packages/techdocs-cli/src/lib/utility.ts @@ -16,7 +16,7 @@ import { RemoteProtocol, ParsedLocationAnnotation, -} from '@backstage/techdocs-common'; +} from '@backstage/plugin-techdocs-node'; import * as winston from 'winston'; export const convertTechDocsRefToLocationAnnotation = ( diff --git a/plugins/techdocs-backend/README.md b/plugins/techdocs-backend/README.md index 57bc477f12..a1d7d22855 100644 --- a/plugins/techdocs-backend/README.md +++ b/plugins/techdocs-backend/README.md @@ -21,7 +21,7 @@ yarn start This provides serving and building of documentation for any entity. To configure various storage providers and building options, see http://backstage.io/docs/features/techdocs/configuration. -The techdocs-backend re-exports the [techdocs-common](https://github.com/backstage/backstage/tree/master/packages/techdocs-common) package which has the features to prepare, generate and publish docs. +The techdocs-backend re-exports the [techdocs-node](https://github.com/backstage/backstage/tree/master/plugins/techdocs-node) package which has the features to prepare, generate and publish docs. The Publishers are also used to fetch the static documentation files and render them in TechDocs. ## Links diff --git a/plugins/techdocs-backend/api-report.md b/plugins/techdocs-backend/api-report.md index 155f096614..acce0b4981 100644 --- a/plugins/techdocs-backend/api-report.md +++ b/plugins/techdocs-backend/api-report.md @@ -10,16 +10,16 @@ import { Config } from '@backstage/config'; import { DocumentCollatorFactory } from '@backstage/search-common'; import { Entity } from '@backstage/catalog-model'; import express from 'express'; -import { GeneratorBuilder } from '@backstage/techdocs-common'; +import { GeneratorBuilder } from '@backstage/plugin-techdocs-node'; import { Knex } from 'knex'; import { Logger as Logger_2 } from 'winston'; import { Permission } from '@backstage/plugin-permission-common'; import { PluginCacheManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; -import { PreparerBuilder } from '@backstage/techdocs-common'; -import { PublisherBase } from '@backstage/techdocs-common'; +import { PreparerBuilder } from '@backstage/plugin-techdocs-node'; +import { PublisherBase } from '@backstage/plugin-techdocs-node'; import { Readable } from 'stream'; -import { TechDocsDocument } from '@backstage/techdocs-common'; +import { TechDocsDocument } from '@backstage/plugin-techdocs-node'; import { TokenManager } from '@backstage/backend-common'; // @public @@ -123,5 +123,5 @@ export type TechDocsCollatorOptions = { export { TechDocsDocument }; -export * from '@backstage/techdocs-common'; +export * from '@backstage/plugin-techdocs-node'; ``` diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 81aa19a937..aea75b1c3d 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -41,8 +41,8 @@ "@backstage/errors": "^0.2.2", "@backstage/integration": "^0.8.0", "@backstage/plugin-catalog-common": "^0.2.0", + "@backstage/plugin-techdocs-node": "^0.11.11", "@backstage/search-common": "^0.3.0", - "@backstage/techdocs-common": "^0.11.11", "@types/express": "^4.17.6", "dockerode": "^3.3.1", "express": "^4.17.1", diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index 05e9c0ce52..09eee1fc12 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -29,7 +29,7 @@ import { PreparerBuilder, PublisherBase, UrlPreparer, -} from '@backstage/techdocs-common'; +} from '@backstage/plugin-techdocs-node'; import fs from 'fs-extra'; import os from 'os'; import path from 'path'; diff --git a/plugins/techdocs-backend/src/index.ts b/plugins/techdocs-backend/src/index.ts index 570da95092..dc5d87d1b3 100644 --- a/plugins/techdocs-backend/src/index.ts +++ b/plugins/techdocs-backend/src/index.ts @@ -39,8 +39,8 @@ export type { } from './search'; /** - * @deprecated Use directly from @backstage/techdocs-common + * @deprecated Use directly from @backstage/plugin-techdocs-node */ -export type { TechDocsDocument } from '@backstage/techdocs-common'; +export type { TechDocsDocument } from '@backstage/plugin-techdocs-node'; -export * from '@backstage/techdocs-common'; +export * from '@backstage/plugin-techdocs-node'; diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts index d36e1c0ae5..7529eaca66 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts @@ -35,7 +35,7 @@ import { CatalogClient, CATALOG_FILTER_EXISTS, } from '@backstage/catalog-client'; -import { TechDocsDocument } from '@backstage/techdocs-common'; +import { TechDocsDocument } from '@backstage/plugin-techdocs-node'; interface MkSearchIndexDoc { title: string; diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts index 181f032b54..7ba76350e4 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts @@ -32,7 +32,7 @@ import { import { Config } from '@backstage/config'; import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common'; import { DocumentCollatorFactory } from '@backstage/search-common'; -import { TechDocsDocument } from '@backstage/techdocs-common'; +import { TechDocsDocument } from '@backstage/plugin-techdocs-node'; import unescape from 'lodash/unescape'; import fetch from 'node-fetch'; import pLimit from 'p-limit'; diff --git a/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts b/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts index 1830dbfcf0..05dfb5bdcc 100644 --- a/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts +++ b/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts @@ -24,7 +24,7 @@ import { GeneratorBuilder, PreparerBuilder, PublisherBase, -} from '@backstage/techdocs-common'; +} from '@backstage/plugin-techdocs-node'; import { TechDocsCache } from '../cache'; import { DocsBuilder, shouldCheckForUpdate } from '../DocsBuilder'; import { DocsSynchronizer, DocsSynchronizerSyncOpts } from './DocsSynchronizer'; diff --git a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts index 2979d2efe4..090efc4639 100644 --- a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts +++ b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts @@ -23,7 +23,7 @@ import { GeneratorBuilder, PreparerBuilder, PublisherBase, -} from '@backstage/techdocs-common'; +} from '@backstage/plugin-techdocs-node'; import fetch from 'node-fetch'; import { PassThrough } from 'stream'; import * as winston from 'winston'; diff --git a/plugins/techdocs-backend/src/service/router.test.ts b/plugins/techdocs-backend/src/service/router.test.ts index c7cda203bb..2e707c9513 100644 --- a/plugins/techdocs-backend/src/service/router.test.ts +++ b/plugins/techdocs-backend/src/service/router.test.ts @@ -26,7 +26,7 @@ import { GeneratorBuilder, PreparerBuilder, PublisherBase, -} from '@backstage/techdocs-common'; +} from '@backstage/plugin-techdocs-node'; import express, { Response } from 'express'; import request from 'supertest'; import { DocsSynchronizer, DocsSynchronizerSyncOpts } from './DocsSynchronizer'; diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index 73f212d049..9ebad9411d 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -26,7 +26,7 @@ import { getLocationForEntity, PreparerBuilder, PublisherBase, -} from '@backstage/techdocs-common'; +} from '@backstage/plugin-techdocs-node'; import express, { Response } from 'express'; import Router from 'express-promise-router'; import { Knex } from 'knex'; diff --git a/plugins/techdocs-backend/src/service/standaloneServer.ts b/plugins/techdocs-backend/src/service/standaloneServer.ts index eeb85bfad0..4ab4c084e0 100644 --- a/plugins/techdocs-backend/src/service/standaloneServer.ts +++ b/plugins/techdocs-backend/src/service/standaloneServer.ts @@ -28,7 +28,7 @@ import { Preparers, Publisher, TechdocsGenerator, -} from '@backstage/techdocs-common'; +} from '@backstage/plugin-techdocs-node'; import Docker from 'dockerode'; import { Server } from 'http'; import { Logger } from 'winston'; diff --git a/plugins/techdocs-node/CHANGELOG.md b/plugins/techdocs-node/CHANGELOG.md index f41c944a89..77f240314b 100644 --- a/plugins/techdocs-node/CHANGELOG.md +++ b/plugins/techdocs-node/CHANGELOG.md @@ -1,4 +1,4 @@ -# @backstage/techdocs-common +# @backstage/plugin-techdocs-node ## 0.11.11 diff --git a/plugins/techdocs-node/README.md b/plugins/techdocs-node/README.md index bbaac85e9b..7bf2e5ba9d 100644 --- a/plugins/techdocs-node/README.md +++ b/plugins/techdocs-node/README.md @@ -1,15 +1,15 @@ -# @backstage/techdocs-common +# @backstage/plugin-techdocs-node -Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli +Common node.js functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli This package is used by `techdocs-backend` to serve docs from different types of publishers (Google GCS, Local, etc.). It is also used to build docs and publish them to storage, by both `techdocs-backend` and `techdocs-cli`. ## Usage -Create a preparer instance from the [preparers available](/packages/techdocs-common/src/stages/prepare) at which takes an Entity instance. -Run the [docs generator](/packages/techdocs-common/src/stages/generate) on the prepared directory. -Publish the generated directory files to a [storage](/packages/techdocs-common/src/stages/publish) of your choice. +Create a preparer instance from the [preparers available](/plugins/techdocs-node/src/stages/prepare) at which takes an Entity instance. +Run the [docs generator](/plugins/techdocs-node/src/stages/generate) on the prepared directory. +Publish the generated directory files to a [storage](/plugins/techdocs-node/src/stages/publish) of your choice. Example: diff --git a/plugins/techdocs-node/api-report.md b/plugins/techdocs-node/api-report.md index 13d610f66b..9ec1aafb09 100644 --- a/plugins/techdocs-node/api-report.md +++ b/plugins/techdocs-node/api-report.md @@ -1,4 +1,4 @@ -## API Report File for "@backstage/techdocs-common" +## API Report File for "@backstage/plugin-techdocs-node" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json index 04e13065c8..7cbf49f954 100644 --- a/plugins/techdocs-node/package.json +++ b/plugins/techdocs-node/package.json @@ -1,6 +1,6 @@ { - "name": "@backstage/techdocs-common", - "description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", + "name": "@backstage/plugin-techdocs-node", + "description": "Common node.js functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", "version": "0.11.11", "main": "src/index.ts", "types": "src/index.ts", @@ -17,7 +17,7 @@ "repository": { "type": "git", "url": "https://github.com/backstage/backstage", - "directory": "packages/techdocs-common" + "directory": "plugins/techdocs-node" }, "keywords": [ "techdocs", diff --git a/plugins/techdocs-node/src/stages/generate/techdocs.ts b/plugins/techdocs-node/src/stages/generate/techdocs.ts index b46f52f99a..fd05c3bf8b 100644 --- a/plugins/techdocs-node/src/stages/generate/techdocs.ts +++ b/plugins/techdocs-node/src/stages/generate/techdocs.ts @@ -51,7 +51,7 @@ import { ForwardedError } from '@backstage/errors'; export class TechdocsGenerator implements GeneratorBase { /** * The default docker image (and version) used to generate content. Public - * and static so that techdocs-common consumers can use the same version. + * and static so that techdocs-node consumers can use the same version. */ public static readonly defaultDockerImage = 'spotify/techdocs:v0.3.7'; private readonly logger: Logger; diff --git a/plugins/techdocs-node/src/stages/publish/awsS3.test.ts b/plugins/techdocs-node/src/stages/publish/awsS3.test.ts index 357df73c8c..326e587a83 100644 --- a/plugins/techdocs-node/src/stages/publish/awsS3.test.ts +++ b/plugins/techdocs-node/src/stages/publish/awsS3.test.ts @@ -24,7 +24,7 @@ import path from 'path'; import fs from 'fs-extra'; import { AwsS3Publish } from './awsS3'; -// NOTE: /packages/techdocs-common/__mocks__ is being used to mock aws-sdk client library +// NOTE: /plugins/techdocs-node/__mocks__ is being used to mock aws-sdk client library const rootDir = (global as any).rootDir; // Set by setupTests.ts diff --git a/plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts b/plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts index f1b660555e..3e5e420e4b 100644 --- a/plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts +++ b/plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts @@ -24,7 +24,7 @@ import path from 'path'; import fs from 'fs-extra'; import { AzureBlobStoragePublish } from './azureBlobStorage'; -// NOTE: /packages/techdocs-common/__mocks__ is being used to mock Azure client library +// NOTE: /plugins/techdocs-node/__mocks__ is being used to mock Azure client library const rootDir = (global as any).rootDir; // Set by setupTests.ts diff --git a/plugins/techdocs-node/src/stages/publish/googleStorage.test.ts b/plugins/techdocs-node/src/stages/publish/googleStorage.test.ts index 87a38402d0..6dac092cec 100644 --- a/plugins/techdocs-node/src/stages/publish/googleStorage.test.ts +++ b/plugins/techdocs-node/src/stages/publish/googleStorage.test.ts @@ -24,7 +24,7 @@ import path from 'path'; import fs from 'fs-extra'; import { GoogleGCSPublish } from './googleStorage'; -// NOTE: /packages/techdocs-common/__mocks__ is being used to mock Google Cloud Storage client library +// NOTE: /plugins/techdocs-node/__mocks__ is being used to mock Google Cloud Storage client library const rootDir = (global as any).rootDir; // Set by setupTests.ts diff --git a/plugins/techdocs-node/src/stages/publish/local.ts b/plugins/techdocs-node/src/stages/publish/local.ts index af9bb30d46..4539d389df 100644 --- a/plugins/techdocs-node/src/stages/publish/local.ts +++ b/plugins/techdocs-node/src/stages/publish/local.ts @@ -49,7 +49,7 @@ try { ); } catch (err) { // This will most probably never be used. - // The try/catch is introduced so that techdocs-cli can import @backstage/techdocs-common + // The try/catch is introduced so that techdocs-cli can import @backstage/plugin-techdocs-node // on CI/CD without installing techdocs backend plugin. staticDocsDir = os.tmpdir(); } diff --git a/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts b/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts index da0a60343f..aeb4e120f5 100644 --- a/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts +++ b/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts @@ -29,7 +29,7 @@ import path from 'path'; import { OpenStackSwiftPublish } from './openStackSwift'; import { PublisherBase, TechDocsMetadata } from './types'; -// NOTE: /packages/techdocs-common/__mocks__ is being used to mock @trendyol-js/openstack-swift-sdk client library +// NOTE: /plugins/techdocs-node/__mocks__ is being used to mock @trendyol-js/openstack-swift-sdk client library const createMockEntity = (annotations = {}): Entity => { return { diff --git a/plugins/techdocs-node/src/stages/publish/types.ts b/plugins/techdocs-node/src/stages/publish/types.ts index 86772c25f4..ff5b9acade 100644 --- a/plugins/techdocs-node/src/stages/publish/types.ts +++ b/plugins/techdocs-node/src/stages/publish/types.ts @@ -153,7 +153,7 @@ export interface PublisherBase { * `techdocs-cli` version `{0.x.y}` and `techdocs-backend` version `{0.x.y}`. * * Implementation of this method is unnecessary in publishers introduced - * after version `{0.x.y}` of `techdocs-common`. + * after version `{0.x.y}` of `techdocs-node`. */ migrateDocsCase?(migrateRequest: MigrateRequest): Promise; } diff --git a/scripts/api-extractor.ts b/scripts/api-extractor.ts index afe2c1a1bb..d37fa51c60 100644 --- a/scripts/api-extractor.ts +++ b/scripts/api-extractor.ts @@ -238,6 +238,7 @@ const NO_WARNING_PACKAGES = [ 'plugins/scaffolder-common', 'plugins/search-backend-node', 'plugins/techdocs-backend', + 'plugins/techdocs-node', 'plugins/tech-insights', 'plugins/tech-insights-backend', 'plugins/tech-insights-backend-module-jsonfc', From 0e39d0b4fe32ca0504abb539a8ae68248f695889 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 4 Mar 2022 10:51:03 +0100 Subject: [PATCH 183/353] Update documentation to reflect search beta. Signed-off-by: Eric Peterson --- docs/features/search/README.md | 12 ++++++------ docs/features/search/how-to-guides.md | 18 +++++++++++------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/features/search/README.md b/docs/features/search/README.md index 0856451882..f25087d59b 100644 --- a/docs/features/search/README.md +++ b/docs/features/search/README.md @@ -24,12 +24,12 @@ Backstage ecosystem. ## Project roadmap -| Version | Description | -| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Backstage Search Pre-Alpha ✅ | Search Frontend letting you search through the entities of the software catalog. [See Pre-Alpha Use Cases.](#backstage-search-pre-alpha) | -| Backstage Search Alpha ✅ | Basic “out-of-the-box” in-memory indexing process of entities, and their metadata, registered to the Software Catalog. [See Alpha Use Cases](#backstage-search-alpha). | -| [Backstage Search Beta ⌛][beta] | At least one production-ready search engine that supports the same use-cases as in the alpha. [See Beta Use Cases](#backstage-search-beta). | -| [Backstage Search GA ⌛][ga] | A stable Search API for plugin developers to add search to their plugins, and app integrators to expose that to their users. [See GA Use Cases](#backstage-search-ga). | +| Version | Description | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Backstage Search Pre-Alpha ✅ | Search Frontend letting you search through the entities of the software catalog. [See Pre-Alpha Use Cases.](#backstage-search-pre-alpha) | +| Backstage Search Alpha ✅ | Basic “out-of-the-box” in-memory indexing process of entities, and their metadata, registered to the Software Catalog. [See Alpha Use Cases](#backstage-search-alpha). | +| Backstage Search Beta ✅ | At least one production-ready search engine that supports the same use-cases as in the alpha. [See Beta Use Cases](#backstage-search-beta). | +| [Backstage Search GA ⌛][ga] | A stable Search API for plugin developers to add search to their plugins, and app integrators to expose that to their users. [See GA Use Cases](#backstage-search-ga). | [beta]: https://github.com/backstage/backstage/milestone/27 [ga]: https://github.com/backstage/backstage/milestone/28 diff --git a/docs/features/search/how-to-guides.md b/docs/features/search/how-to-guides.md index 0a5f53d2f2..16596c6e92 100644 --- a/docs/features/search/how-to-guides.md +++ b/docs/features/search/how-to-guides.md @@ -137,9 +137,10 @@ entities are indexed by the search engine. For the purposes of this guide, Search Beta version is defined as: -- **Search Plugin**: At least `v0.x.y` -- **Search Backend Plugin**: At least `v0.x.y` -- **Search Backend Node**: At least `v0.x.y` +- **Search Plugin**: At least `v0.7.2` +- **Search Backend Plugin**: At least `v0.4.6` +- **Search Backend Node**: At least `v0.5.0` +- **Search Common**: At least `v0.3.0` In the Beta version, the Search Platform's indexing process has been rewritten as a stream pipeline in order to improve efficiency and performance on large @@ -149,11 +150,14 @@ If you've not yet extended the Search Platform with custom code, and have instead taken advantage of default collators, decorators, and search engines provided by existing plugins, the migration process is fairly straightforward: -1. Upgrade to at least version `0.x.y` of +1. Upgrade to at least version `0.5.0` of `@backstage/plugin-search-backend-node`, as well as any backend plugins whose - collators you are using (e.g. at least version `0.x.y` of - `@backstage/plugin-catalog-backend` and/or version `0.x.y` of - `@backstage/plugin-techdocs-backend`). + collators you are using (e.g. at least version `0.23.0` of + `@backstage/plugin-catalog-backend` and/or version `0.14.1` of + `@backstage/plugin-techdocs-backend`), as well as any search-engine specific + plugin you are using (e.g. at least version `0.3.0` of + `@backstage/plugin-search-backend-module-pg` or version `0.1.0` of + `@backstage/plugin-search-backend-module-elasticsearch`). 2. Then, make the following changes to your `/packages/backend/src/plugins/search.ts` file: From 5ea9509e6a048101a9d029b80e9a95b8debb416b Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 4 Mar 2022 11:23:47 +0100 Subject: [PATCH 184/353] catalog: Remove CatalogResultListItem Signed-off-by: Johan Haals --- .changeset/fifty-brooms-whisper.md | 5 +++++ .../CatalogSearchResultListItem.tsx | 12 ------------ .../components/CatalogSearchResultListItem/index.ts | 10 ++-------- 3 files changed, 7 insertions(+), 20 deletions(-) create mode 100644 .changeset/fifty-brooms-whisper.md diff --git a/.changeset/fifty-brooms-whisper.md b/.changeset/fifty-brooms-whisper.md new file mode 100644 index 0000000000..b4c45555a3 --- /dev/null +++ b/.changeset/fifty-brooms-whisper.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': minor +--- + +**BREAKING**: Removed `CatalogResultListItemProps` and `CatalogResultListItem`, replaced by `CatalogSearchResultListItemProps` and `CatalogSearchResultListItem`. diff --git a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx index 54cfe43364..9762eb3da9 100644 --- a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx +++ b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx @@ -73,15 +73,3 @@ export function CatalogSearchResultListItem( ); } - -/** - * @public - * @deprecated use {@link CatalogSearchResultListItemProps} instead - */ -export type CatalogResultListItemProps = CatalogSearchResultListItemProps; - -/** - * @public - * @deprecated use {@link CatalogSearchResultListItem} instead - */ -export const CatalogResultListItem = CatalogSearchResultListItem; diff --git a/plugins/catalog/src/components/CatalogSearchResultListItem/index.ts b/plugins/catalog/src/components/CatalogSearchResultListItem/index.ts index 0396117825..ba0e6d29be 100644 --- a/plugins/catalog/src/components/CatalogSearchResultListItem/index.ts +++ b/plugins/catalog/src/components/CatalogSearchResultListItem/index.ts @@ -14,11 +14,5 @@ * limitations under the License. */ -export { - CatalogSearchResultListItem, - CatalogResultListItem, -} from './CatalogSearchResultListItem'; -export type { - CatalogSearchResultListItemProps, - CatalogResultListItemProps, -} from './CatalogSearchResultListItem'; +export { CatalogSearchResultListItem } from './CatalogSearchResultListItem'; +export type { CatalogSearchResultListItemProps } from './CatalogSearchResultListItem'; From 50e69335c71198a742a8c4f2441cd3e329b1740c Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 4 Mar 2022 11:24:12 +0100 Subject: [PATCH 185/353] Trigger upgrade helper release once Signed-off-by: Vincenzo Scamporlino --- .github/workflows/sync_release-manifest.yml | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/.github/workflows/sync_release-manifest.yml b/.github/workflows/sync_release-manifest.yml index f4d9afc0df..328c64fb58 100644 --- a/.github/workflows/sync_release-manifest.yml +++ b/.github/workflows/sync_release-manifest.yml @@ -56,18 +56,7 @@ jobs: github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} # TODO(Rugvip): Remove the create-app dispatch once we've been on the release version for a while script: | - console.log('Dispatching upgrade helper sync - release version'); - await github.rest.actions.createWorkflowDispatch({ - owner: 'backstage', - repo: 'upgrade-helper-diff', - workflow_id: 'release.yml', - ref: 'master', - inputs: { - version: require('./backstage/package.json').version, - }, - }); - - console.log('Dispatching upgrade helper sync - create-app version'); + console.log('Dispatching upgrade helper sync'); await github.rest.actions.createWorkflowDispatch({ owner: 'backstage', repo: 'upgrade-helper-diff', @@ -75,5 +64,6 @@ jobs: ref: 'master', inputs: { version: require('./backstage/packages/create-app/package.json').version, + releaseVersion: require('./backstage/package.json').version }, }); From e4aee4e08f0eb077c1a3126fc60a6f2bdc2921e2 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 4 Mar 2022 11:24:33 +0100 Subject: [PATCH 186/353] Update api report Signed-off-by: Johan Haals --- plugins/catalog/api-report.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 97a36ad020..f95b7b1682 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -104,12 +104,6 @@ export const catalogPlugin: BackstagePlugin< } >; -// @public @deprecated (undocumented) -export const CatalogResultListItem: typeof CatalogSearchResultListItem; - -// @public @deprecated (undocumented) -export type CatalogResultListItemProps = CatalogSearchResultListItemProps; - // @public (undocumented) export function CatalogSearchResultListItem( props: CatalogSearchResultListItemProps, From c543fe3ff2687a12ba03d767d2976f5c8fc6da8d Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Fri, 4 Mar 2022 11:27:43 +0100 Subject: [PATCH 187/353] Respect PG selection in create-app for search. Signed-off-by: Eric Peterson --- .changeset/search-talking-in-code.md | 7 +++++++ packages/create-app/package.json | 1 + packages/create-app/src/lib/versions.ts | 2 ++ .../default-app/packages/backend/package.json.hbs | 3 +++ .../backend/src/plugins/{search.ts => search.ts.hbs} | 10 ++++++++++ packages/e2e-test/src/commands/run.ts | 12 +++++++++--- 6 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 .changeset/search-talking-in-code.md rename packages/create-app/templates/default-app/packages/backend/src/plugins/{search.ts => search.ts.hbs} (85%) diff --git a/.changeset/search-talking-in-code.md b/.changeset/search-talking-in-code.md new file mode 100644 index 0000000000..46de5f8343 --- /dev/null +++ b/.changeset/search-talking-in-code.md @@ -0,0 +1,7 @@ +--- +'@backstage/create-app': patch +--- + +Postgres-based search is now installed when PG is chosen as the desired database for Backstage. + +There is no need to make this change in an existing Backstage backend. See [supported search engines](https://backstage.io/docs/features/search/search-engines) for details about production-ready search engines. diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 975d2cee7e..9fd8c33837 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -86,6 +86,7 @@ "@backstage/plugin-scaffolder-backend": "", "@backstage/plugin-search": "", "@backstage/plugin-search-backend": "", + "@backstage/plugin-search-backend-module-pg": "", "@backstage/plugin-search-backend-node": "", "@backstage/plugin-tech-radar": "", "@backstage/plugin-techdocs": "", diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index 7179b7924f..e79e86e3be 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -68,6 +68,7 @@ import { version as pluginScaffolder } from '../../../../plugins/scaffolder/pack import { version as pluginScaffolderBackend } from '../../../../plugins/scaffolder-backend/package.json'; import { version as pluginSearch } from '../../../../plugins/search/package.json'; import { version as pluginSearchBackend } from '../../../../plugins/search-backend/package.json'; +import { version as pluginSearchBackendModulePg } from '../../../../plugins/search-backend-module-pg/package.json'; import { version as pluginSearchBackendNode } from '../../../../plugins/search-backend-node/package.json'; import { version as pluginTechRadar } from '../../../../plugins/tech-radar/package.json'; import { version as pluginTechdocs } from '../../../../plugins/techdocs/package.json'; @@ -110,6 +111,7 @@ export const packageVersions = { '@backstage/plugin-scaffolder-backend': pluginScaffolderBackend, '@backstage/plugin-search': pluginSearch, '@backstage/plugin-search-backend': pluginSearchBackend, + '@backstage/plugin-search-backend-module-pg': pluginSearchBackendModulePg, '@backstage/plugin-search-backend-node': pluginSearchBackendNode, '@backstage/plugin-tech-radar': pluginTechRadar, '@backstage/plugin-techdocs': pluginTechdocs, 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 5dd70286bc..feba5169ba 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 @@ -28,6 +28,9 @@ "@backstage/plugin-proxy-backend": "^{{version '@backstage/plugin-proxy-backend'}}", "@backstage/plugin-scaffolder-backend": "^{{version '@backstage/plugin-scaffolder-backend'}}", "@backstage/plugin-search-backend": "^{{version '@backstage/plugin-search-backend'}}", + {{#if dbTypePG}} + "@backstage/plugin-search-backend-module-pg": "^{{version '@backstage/plugin-search-backend-module-pg'}}", + {{/if}} "@backstage/plugin-search-backend-node": "^{{version '@backstage/plugin-search-backend-node'}}", "@backstage/plugin-techdocs-backend": "^{{version '@backstage/plugin-techdocs-backend'}}", "@gitbeaker/node": "^34.6.0", diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts.hbs similarity index 85% rename from packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts rename to packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts.hbs index c359cb4986..d8ad991e8a 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/search.ts.hbs @@ -4,6 +4,9 @@ import { IndexBuilder, LunrSearchEngine, } from '@backstage/plugin-search-backend-node'; +{{#if dbTypePG}} +import { PgSearchEngine } from '@backstage/plugin-search-backend-module-pg'; +{{/if}} import { PluginEnvironment } from '../types'; import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend'; import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend'; @@ -16,7 +19,14 @@ export default async function createPlugin({ tokenManager, }: PluginEnvironment) { // Initialize a connection to a search engine. + {{#if dbTypeSqlite}} const searchEngine = new LunrSearchEngine({ logger }); + {{/if}} + {{#if dbTypePG}} + const searchEngine = (await PgSearchEngine.supported(database)) + ? await PgSearchEngine.from({ database }) + : new LunrSearchEngine({ logger }); + {{/if}} const indexBuilder = new IndexBuilder({ logger, searchEngine }); // Collators are responsible for gathering documents known to plugins. This diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index a7456f91dd..1ec9499b92 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -420,9 +420,15 @@ async function testBackendStart(appDir: string, isPostgres: boolean) { if (isPostgres) { print('Dropping old DBs'); await Promise.all( - ['catalog', 'scaffolder', 'auth', 'identity', 'proxy', 'techdocs'].map( - name => dropDB(`backstage_plugin_${name}`), - ), + [ + 'catalog', + 'scaffolder', + 'auth', + 'identity', + 'proxy', + 'techdocs', + 'search', + ].map(name => dropDB(`backstage_plugin_${name}`)), ); print('Created DBs'); } From 1201383b6069915f333d00d6364eb1218b25d77a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 4 Mar 2022 11:22:52 +0100 Subject: [PATCH 188/353] create-app: switch to storing release version in backstage.json Signed-off-by: Patrik Oldsberg --- .changeset/olive-roses-sleep.md | 5 +++++ packages/create-app/src/createApp.test.ts | 6 +++--- packages/create-app/src/createApp.ts | 6 +++--- packages/create-app/src/index.ts | 2 +- packages/create-app/src/lib/tasks.test.ts | 5 +++-- packages/create-app/src/lib/tasks.ts | 9 --------- packages/create-app/src/lib/versions.ts | 3 +++ .../create-app/templates/default-app/backstage.json.hbs | 3 +++ 8 files changed, 21 insertions(+), 18 deletions(-) create mode 100644 .changeset/olive-roses-sleep.md create mode 100644 packages/create-app/templates/default-app/backstage.json.hbs diff --git a/.changeset/olive-roses-sleep.md b/.changeset/olive-roses-sleep.md new file mode 100644 index 0000000000..b402a5b2aa --- /dev/null +++ b/.changeset/olive-roses-sleep.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Updated the template to write the Backstage release version to `backstage.json`, rather than the version of `@backstage/create-app`. This change is applied automatically when running `backstage-cli versions:bump` in the latest version of the Backstage CLI. diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts index ee01e8fd42..09099e2d15 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -59,7 +59,7 @@ describe('command entrypoint', () => { test('should call expected tasks with no path option', async () => { const cmd = {} as unknown as Command; - await createApp(cmd, '1.0.0'); + await createApp(cmd); expect(checkAppExistsMock).toHaveBeenCalled(); expect(createTemporaryAppFolderMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); @@ -69,7 +69,7 @@ describe('command entrypoint', () => { it('should call expected tasks with path option', async () => { const cmd = { path: 'myDirectory' } as unknown as Command; - await createApp(cmd, '1.0.0'); + await createApp(cmd); expect(checkPathExistsMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); expect(buildAppMock).toHaveBeenCalled(); @@ -77,7 +77,7 @@ describe('command entrypoint', () => { it('should not call `buildAppTask` when `skipInstall` is supplied', async () => { const cmd = { skipInstall: true } as unknown as Command; - await createApp(cmd, '1.0.0'); + await createApp(cmd); expect(buildAppMock).not.toHaveBeenCalled(); }); }); diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index d4d30364dc..36143a4172 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -30,7 +30,7 @@ import { templatingTask, } from './lib/tasks'; -export default async (cmd: Command, version: string): Promise => { +export default async (cmd: Command): Promise => { /* eslint-disable-next-line no-restricted-syntax */ const paths = findPaths(__dirname); @@ -80,7 +80,7 @@ export default async (cmd: Command, version: string): Promise => { await checkPathExistsTask(appDir); Task.section('Preparing files'); - await templatingTask(templateDir, cmd.path, answers, version); + await templatingTask(templateDir, cmd.path, answers); } else { // Template to temporary location, and then move files @@ -91,7 +91,7 @@ export default async (cmd: Command, version: string): Promise => { await createTemporaryAppFolderTask(tempDir); Task.section('Preparing files'); - await templatingTask(templateDir, tempDir, answers, version); + await templatingTask(templateDir, tempDir, answers); Task.section('Moving to final location'); await moveAppTask(tempDir, appDir, answers.name); diff --git a/packages/create-app/src/index.ts b/packages/create-app/src/index.ts index 8a2da2fad8..a8ff3dc9b9 100644 --- a/packages/create-app/src/index.ts +++ b/packages/create-app/src/index.ts @@ -38,7 +38,7 @@ const main = (argv: string[]) => { '--skip-install', 'Skip the install and builds steps after creating the app', ) - .action(cmd => createApp(cmd, version)); + .action(cmd => createApp(cmd)); program.parse(argv); }; diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index d7b4692aa4..0c92173dbf 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -18,6 +18,7 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; import child_process from 'child_process'; import path from 'path'; +import { version as releaseVersion } from '../../../../package.json'; import { buildAppTask, checkAppExistsTask, @@ -195,11 +196,11 @@ describe('templatingTask', () => { name: 'SuperCoolBackstageInstance', dbTypeSqlite: true, }; - await templatingTask(templateDir, destinationDir, context, '1.0.0'); + await templatingTask(templateDir, destinationDir, context); expect(fs.existsSync('templatedApp/package.json')).toBe(true); expect(fs.existsSync('templatedApp/.dockerignore')).toBe(true); await expect(fs.readJson('templatedApp/backstage.json')).resolves.toEqual({ - version: '1.0.0', + version: releaseVersion, }); // catalog was populated with `context.name` expect( diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 4bd7050fc2..1650432969 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { BACKSTAGE_JSON } from '@backstage/cli-common'; import chalk from 'chalk'; import fs from 'fs-extra'; import handlebars from 'handlebars'; @@ -23,7 +22,6 @@ import recursive from 'recursive-readdir'; import { basename, dirname, - join, resolve as resolvePath, relative as relativePath, } from 'path'; @@ -86,7 +84,6 @@ export async function templatingTask( templateDir: string, destinationDir: string, context: any, - version: string, ) { const files = await recursive(templateDir).catch(error => { throw new Error(`Failed to read template directory: ${error.message}`); @@ -136,12 +133,6 @@ export async function templatingTask( }); } } - await Task.forItem('creating', BACKSTAGE_JSON, () => - fs.writeFile( - join(destinationDir, BACKSTAGE_JSON), - `{\n "version": ${JSON.stringify(version)}\n}\n`, - ), - ); } /** diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index 7179b7924f..3c898f1e02 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -30,6 +30,8 @@ Rollup will extract the value of the version field in each package at build time leaving any imports in place. */ +import { version as root } from '../../../../package.json'; + import { version as appDefaults } from '../../../app-defaults/package.json'; import { version as backendCommon } from '../../../backend-common/package.json'; import { version as backendTasks } from '../../../backend-tasks/package.json'; @@ -75,6 +77,7 @@ import { version as pluginTechdocsBackend } from '../../../../plugins/techdocs-b import { version as pluginUserSettings } from '../../../../plugins/user-settings/package.json'; export const packageVersions = { + root, '@backstage/app-defaults': appDefaults, '@backstage/backend-common': backendCommon, '@backstage/backend-tasks': backendTasks, diff --git a/packages/create-app/templates/default-app/backstage.json.hbs b/packages/create-app/templates/default-app/backstage.json.hbs new file mode 100644 index 0000000000..2aa00ecaee --- /dev/null +++ b/packages/create-app/templates/default-app/backstage.json.hbs @@ -0,0 +1,3 @@ +{ + "version": "{{version 'root'}}" +} From 51856359bffff8a45212feaea55160c7613d062f Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 4 Mar 2022 11:41:27 +0100 Subject: [PATCH 189/353] catalog: Remove AboutCard Signed-off-by: Johan Haals --- .changeset/brown-points-impress.md | 5 +++++ plugins/catalog/api-report.md | 3 --- plugins/catalog/src/components/AboutCard/AboutCard.tsx | 3 +-- plugins/catalog/src/index.ts | 7 ++++++- 4 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 .changeset/brown-points-impress.md diff --git a/.changeset/brown-points-impress.md b/.changeset/brown-points-impress.md new file mode 100644 index 0000000000..74d5e1019d --- /dev/null +++ b/.changeset/brown-points-impress.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': minor +--- + +**BREAKING**: Removed the `AboutCard` component which has been replaced by `EntityAboutCard`. diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 97a36ad020..7cfa8a581f 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -26,9 +26,6 @@ import { TableProps } from '@backstage/core-components'; import { TabProps } from '@material-ui/core'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; -// @public @deprecated (undocumented) -export function AboutCard(props: AboutCardProps): JSX.Element; - // @public export interface AboutCardProps { // (undocumented) diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index 55cef5a5bd..b634c999f9 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -72,7 +72,7 @@ const useStyles = makeStyles({ }); /** - * Props for {@link AboutCard}. + * Props for {@link EntityAboutCard}. * * @public */ @@ -82,7 +82,6 @@ export interface AboutCardProps { /** * @public - * @deprecated Please use EntityAboutCard instead */ export function AboutCard(props: AboutCardProps) { const { variant } = props; diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 662a920f8b..cfe4851d09 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -22,7 +22,12 @@ export * from './apis'; -export * from './components/AboutCard'; +export type { + AboutCardProps, + AboutContentProps, + AboutFieldProps, +} from './components/AboutCard'; +export { AboutContent, AboutField } from './components/AboutCard'; export * from './components/CatalogKindHeader'; export * from './components/CatalogSearchResultListItem'; export * from './components/CatalogTable'; From 1549c4be2e98f475cc8157ca01fba0a27de4103f Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 4 Mar 2022 11:51:22 +0100 Subject: [PATCH 190/353] update comment Signed-off-by: Johan Haals --- plugins/catalog/src/components/AboutCard/AboutCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index b634c999f9..b7ccd49b31 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -81,7 +81,7 @@ export interface AboutCardProps { } /** - * @public + * Exported publicly via the EntityAboutCard */ export function AboutCard(props: AboutCardProps) { const { variant } = props; From 8e426f464a10df3c297e4bf98dc08c7d449a0723 Mon Sep 17 00:00:00 2001 From: Simon Stamm Date: Fri, 4 Mar 2022 11:35:44 +0100 Subject: [PATCH 191/353] add TUI Group to adopters list Signed-off-by: Simon Stamm --- ADOPTERS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ADOPTERS.md b/ADOPTERS.md index 83a8886d3f..5aad5dcf72 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -102,3 +102,4 @@ _If you're using Backstage in your organization, please try to add your company | [Grupo Boticário](https://www.grupoboticario.com.br/) | [@chicoribas](https://github.com/chicoribas), [@fndiaz](https://github.com/fndiaz), [@digogid](https://github.com/digogid), [@manumbs](https://github.com/manumbs), [@haooliveira84](https://github.com/haooliveira84), [@Rhullyam](https://github.com/Rhullyam) | Centralized developer portal with catalog, documentation, and software templates to build a ready to go application, including pipelines (CI/CD) and cloud services provisioning | | [Snyk](https://snyk.io/) | [@robcresswell](https://github.com/robcresswell) | Developer portal for software discovery, API documentation and templating | | [Stilingue](https://www.stilingue.com.br/) | [@stilingue-inteligencia-artificial](https://github.com/Stilingue-IA), [@bbviana](https://github.com/bbviana) | Developer portal, services catalog and centralization of metrics from Grafana, Sentry and GCP. Furthermore, centralization of documentation and infra details like DNS, Network services, SSL and so on. | +| [TUI Group](https://www.tuigroup.com/) | [Simon Stamm](https://github.com/simonstamm), [Christian Rudolph](https://github.com/ChrisRu82) | Developer portal for all engineer to provide discoverability to all internal components, APIs, documentation and to scaffold templates with integrations to our internal tools extended by plugins from our community. | From ae9d6fb3df355bf0f36b062b823ef796a1b3867d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 4 Mar 2022 12:14:48 +0100 Subject: [PATCH 192/353] remove createDatabase and SingleConnectionDatabaseManager that were deprecated years ago :) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/thick-games-dress.md | 8 ++ .../tutorials/configuring-plugin-databases.md | 23 ----- packages/backend-common/api-report.md | 6 -- .../src/database/SingleConnection.test.ts | 99 ------------------- .../src/database/SingleConnection.ts | 28 ------ .../backend-common/src/database/connection.ts | 8 -- packages/backend-common/src/database/index.ts | 7 +- 7 files changed, 9 insertions(+), 170 deletions(-) create mode 100644 .changeset/thick-games-dress.md delete mode 100644 packages/backend-common/src/database/SingleConnection.test.ts delete mode 100644 packages/backend-common/src/database/SingleConnection.ts diff --git a/.changeset/thick-games-dress.md b/.changeset/thick-games-dress.md new file mode 100644 index 0000000000..5ce2decd3f --- /dev/null +++ b/.changeset/thick-games-dress.md @@ -0,0 +1,8 @@ +--- +'@backstage/backend-common': minor +--- + +**BREAKING**: + +- Removed the (since way back) deprecated `createDatabase` export, please use `createDatabaseClient` instead. +- Removed the (since way back) deprecated `SingleConnectionDatabaseManager` export, please use `DatabaseManager` instead. diff --git a/docs/tutorials/configuring-plugin-databases.md b/docs/tutorials/configuring-plugin-databases.md index 5c5f0ffb68..9277bb5028 100644 --- a/docs/tutorials/configuring-plugin-databases.md +++ b/docs/tutorials/configuring-plugin-databases.md @@ -49,29 +49,6 @@ yarn add sqlite3 From an operational perspective, you only need to install drivers for clients that are actively used. -### Database Manager - -Existing Backstage instances should be updated to use `DatabaseManager` from -`@backstage/backend-common` in your `packages/backend/src/index.ts` file, the -`SingleConnectionDatabaseManager` has been deprecated. Import the manager and -update the references as shown below if this is not the case: - -```diff -import { -- SingleConnectionDatabaseManager, -+ DatabaseManager, -} from '@backstage/backend-common'; - -// ... - -function makeCreateEnv(config: Config) { - // ... -- const databaseManager = SingleConnectionDatabaseManager.fromConfig(config); -+ const databaseManager = DatabaseManager.fromConfig(config); - // ... -} -``` - ## Configuration You should set the base database client and connection information in your diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 4ed9bb1514..f9fd164787 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -170,9 +170,6 @@ export class Contexts { ): Context; } -// @public @deprecated -export const createDatabase: typeof createDatabaseClient; - // @public export function createDatabaseClient( dbConfig: Config, @@ -574,9 +571,6 @@ export type ServiceBuilder = { // @public export function setRootLogger(newLogger: winston.Logger): void; -// @public @deprecated -export const SingleConnectionDatabaseManager: typeof DatabaseManager; - // @public export class SingleHostDiscovery implements PluginEndpointDiscovery { static fromConfig( diff --git a/packages/backend-common/src/database/SingleConnection.test.ts b/packages/backend-common/src/database/SingleConnection.test.ts deleted file mode 100644 index 46d7376d25..0000000000 --- a/packages/backend-common/src/database/SingleConnection.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ConfigReader } from '@backstage/config'; -import { createDatabaseClient, ensureDatabaseExists } from './connection'; -import { SingleConnectionDatabaseManager } from './SingleConnection'; - -jest.mock('./connection', () => ({ - ...jest.requireActual('./connection'), - createDatabaseClient: jest.fn(), - ensureDatabaseExists: jest.fn(), -})); - -describe('SingleConnectionDatabaseManager', () => { - const defaultConfigOptions = { - backend: { - database: { - client: 'pg', - connection: { - host: 'localhost', - user: 'foo', - password: 'bar', - database: 'foodb', - }, - }, - }, - }; - const defaultConfig = () => new ConfigReader(defaultConfigOptions); - - // This is similar to the ts-jest `mocked` helper. - const mocked = (f: Function) => f as jest.Mock; - - afterEach(() => jest.resetAllMocks()); - - describe('SingleConnectionDatabaseManager.fromConfig', () => { - it('accesses the backend.database key', () => { - const config = defaultConfig(); - const getConfig = jest.spyOn(config, 'getConfig'); - - SingleConnectionDatabaseManager.fromConfig(config); - - expect(getConfig.mock.calls[0][0]).toEqual('backend.database'); - }); - }); - - describe('SingleConnectionDatabaseManager.forPlugin', () => { - const manager = SingleConnectionDatabaseManager.fromConfig(defaultConfig()); - - it('connects to a database scoped to the plugin', async () => { - const pluginId = 'test1'; - await manager.forPlugin(pluginId).getClient(); - - expect(mocked(createDatabaseClient)).toHaveBeenCalledTimes(1); - - const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); - const callArgs = mockCalls[0]; - expect(callArgs[1].connection.database).toEqual( - `backstage_plugin_${pluginId}`, - ); - }); - - it('provides different plugins different databases', async () => { - const plugin1Id = 'test1'; - const plugin2Id = 'test2'; - await manager.forPlugin(plugin1Id).getClient(); - await manager.forPlugin(plugin2Id).getClient(); - - expect(mocked(createDatabaseClient)).toHaveBeenCalledTimes(2); - - const mockCalls = mocked(createDatabaseClient).mock.calls; - const plugin1CallArgs = mockCalls[0]; - const plugin2CallArgs = mockCalls[1]; - expect(plugin1CallArgs[1].connection.database).not.toEqual( - plugin2CallArgs[1].connection.database, - ); - }); - - it('ensure plugin database is created', async () => { - await manager.forPlugin('test').getClient(); - const mockCalls = mocked(ensureDatabaseExists).mock.calls.splice(-1); - const [_, database] = mockCalls[0]; - - expect(database).toEqual('backstage_plugin_test'); - }); - }); -}); diff --git a/packages/backend-common/src/database/SingleConnection.ts b/packages/backend-common/src/database/SingleConnection.ts deleted file mode 100644 index a81d60fe83..0000000000 --- a/packages/backend-common/src/database/SingleConnection.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { DatabaseManager } from './DatabaseManager'; - -/** - * Implements a Database Manager which will automatically create new databases - * for plugins when requested. All requested databases are created with the - * credentials provided; if the database already exists no attempt to create - * the database will be made. - * - * @public - * @deprecated Use `DatabaseManager` from `@backend-common` instead. - */ -export const SingleConnectionDatabaseManager = DatabaseManager; diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index 7fc9df060d..fd0769b8a2 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -57,14 +57,6 @@ export function createDatabaseClient( ); } -/** - * Alias for {@link createDatabaseClient} - * - * @public - * @deprecated Use createDatabaseClient instead - */ -export const createDatabase = createDatabaseClient; - /** * Ensures that the given databases all exist, creating them if they do not. * diff --git a/packages/backend-common/src/database/index.ts b/packages/backend-common/src/database/index.ts index 2a820f7367..0dde1ec239 100644 --- a/packages/backend-common/src/database/index.ts +++ b/packages/backend-common/src/database/index.ts @@ -14,18 +14,13 @@ * limitations under the License. */ -export * from './SingleConnection'; export * from './DatabaseManager'; /* * Undocumented API surface from connection is being reduced for future deprecation. * Avoid exporting additional symbols. */ -export { - createDatabaseClient, - createDatabase, - ensureDatabaseExists, -} from './connection'; +export { createDatabaseClient, ensureDatabaseExists } from './connection'; export type { PluginDatabaseManager } from './types'; export { isDatabaseConflictError } from './util'; From dc98ba469d550b51194e3681f94f7b1f24feeac0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 4 Mar 2022 10:30:11 +0100 Subject: [PATCH 193/353] cli: fix release stage entry point resolution Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/pack.ts | 11 ++++++++--- packages/cli/src/lib/packager/copyPackageDist.ts | 11 ++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/commands/pack.ts b/packages/cli/src/commands/pack.ts index e646568980..6c88ac055f 100644 --- a/packages/cli/src/commands/pack.ts +++ b/packages/cli/src/commands/pack.ts @@ -23,6 +23,11 @@ const SKIPPED_KEYS = ['access', 'registry', 'tag', 'alphaTypes', 'betaTypes']; const PKG_PATH = 'package.json'; const PKG_BACKUP_PATH = 'package.json-prepack'; +function resolveEntrypoint(pkg: any, name: string) { + const targetEntry = pkg.publishConfig[name] || pkg[name]; + return targetEntry && joinPath('..', targetEntry); +} + // Writes e.g. alpha/package.json async function writeReleaseStageEntrypoint(pkg: any, stage: 'alpha' | 'beta') { await fs.ensureDir(paths.resolveTarget(stage)); @@ -31,9 +36,9 @@ async function writeReleaseStageEntrypoint(pkg: any, stage: 'alpha' | 'beta') { { name: pkg.name, version: pkg.version, - main: (pkg.publishConfig.main || pkg.main) && '..', - module: (pkg.publishConfig.module || pkg.module) && '..', - browser: (pkg.publishConfig.browser || pkg.browser) && '..', + main: resolveEntrypoint(pkg, 'main'), + module: resolveEntrypoint(pkg, 'module'), + browser: resolveEntrypoint(pkg, 'browser'), types: joinPath('..', pkg.publishConfig[`${stage}Types`]), }, { encoding: 'utf8', spaces: 2 }, diff --git a/packages/cli/src/lib/packager/copyPackageDist.ts b/packages/cli/src/lib/packager/copyPackageDist.ts index 03ac361006..439b087fd7 100644 --- a/packages/cli/src/lib/packager/copyPackageDist.ts +++ b/packages/cli/src/lib/packager/copyPackageDist.ts @@ -20,6 +20,11 @@ import { join as joinPath, resolve as resolvePath } from 'path'; const SKIPPED_KEYS = ['access', 'registry', 'tag', 'alphaTypes', 'betaTypes']; +function resolveEntrypoint(pkg: any, name: string) { + const targetEntry = pkg.publishConfig[name] || pkg[name]; + return targetEntry && joinPath('..', targetEntry); +} + // Writes e.g. alpha/package.json async function writeReleaseStageEntrypoint( pkg: any, @@ -32,9 +37,9 @@ async function writeReleaseStageEntrypoint( { name: pkg.name, version: pkg.version, - main: (pkg.publishConfig.main || pkg.main) && '..', - module: (pkg.publishConfig.module || pkg.module) && '..', - browser: (pkg.publishConfig.browser || pkg.browser) && '..', + main: resolveEntrypoint(pkg, 'main'), + module: resolveEntrypoint(pkg, 'module'), + browser: resolveEntrypoint(pkg, 'browser'), types: joinPath('..', pkg.publishConfig[`${stage}Types`]), }, { encoding: 'utf8', spaces: 2 }, From f833dd823c1c59e4cdd2fb61e48f4d3299b8e253 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 4 Mar 2022 10:31:33 +0100 Subject: [PATCH 194/353] catalog-backend: mark github multi org reader as stable Signed-off-by: Patrik Oldsberg --- plugins/catalog-backend/api-report.md | 2 +- .../src/modules/github/GithubMultiOrgReaderProcessor.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 0d9f306009..fcbd124e3a 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -706,7 +706,7 @@ export type GithubMultiOrgConfig = Array<{ userNamespace: string | undefined; }>; -// @alpha +// @public export class GithubMultiOrgReaderProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrationRegistry; diff --git a/plugins/catalog-backend/src/modules/github/GithubMultiOrgReaderProcessor.ts b/plugins/catalog-backend/src/modules/github/GithubMultiOrgReaderProcessor.ts index 339119137a..dbd858fc0d 100644 --- a/plugins/catalog-backend/src/modules/github/GithubMultiOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/github/GithubMultiOrgReaderProcessor.ts @@ -40,10 +40,11 @@ import { import { buildOrgHierarchy } from '../util/org'; /** - * @alpha * Extracts teams and users out of a multiple GitHub orgs namespaced per org. * * Be aware that this processor may not be compatible with future org structures in the catalog. + * + * @public */ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { private readonly integrations: ScmIntegrationRegistry; From b1aacbf96a8064442ac049743ab4da3b3a317610 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 4 Mar 2022 12:55:49 +0100 Subject: [PATCH 195/353] Applied patches from 0.70.1 release Signed-off-by: Patrik Oldsberg --- .changeset/patched.json | 9 ++++++++- .changeset/popular-boxes-develop.md | 5 +++++ .changeset/popular-rats-return.md | 9 +++++++++ packages/backend-common/CHANGELOG.md | 6 ++++++ packages/catalog-model/CHANGELOG.md | 6 ++++++ packages/cli/CHANGELOG.md | 6 ++++++ plugins/catalog-backend/CHANGELOG.md | 11 +++++++++++ plugins/catalog-common/CHANGELOG.md | 6 ++++++ plugins/catalog-react/CHANGELOG.md | 8 ++++++++ 9 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 .changeset/popular-boxes-develop.md create mode 100644 .changeset/popular-rats-return.md diff --git a/.changeset/patched.json b/.changeset/patched.json index 1029c8f106..dd63e86169 100644 --- a/.changeset/patched.json +++ b/.changeset/patched.json @@ -1,3 +1,10 @@ { - "currentReleaseVersion": {} + "currentReleaseVersion": { + "@backstage/backend-common": "0.12.1", + "@backstage/catalog-model": "0.12.1", + "@backstage/cli": "0.15.1", + "@backstage/plugin-catalog-backend": "0.23.1", + "@backstage/plugin-catalog-common": "0.2.1", + "@backstage/plugin-catalog-react": "0.8.1" + } } diff --git a/.changeset/popular-boxes-develop.md b/.changeset/popular-boxes-develop.md new file mode 100644 index 0000000000..65aae2c7df --- /dev/null +++ b/.changeset/popular-boxes-develop.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Applied the fix from version `0.15.1` of this package, which was part of the `v0.70.1` release of Backstage. diff --git a/.changeset/popular-rats-return.md b/.changeset/popular-rats-return.md new file mode 100644 index 0000000000..3a91e6d707 --- /dev/null +++ b/.changeset/popular-rats-return.md @@ -0,0 +1,9 @@ +--- +'@backstage/backend-common': patch +'@backstage/catalog-model': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-common': patch +'@backstage/plugin-catalog-react': patch +--- + +Applied the fix for the `/alpha` entry point resolution that was part of the `v0.70.1` release of Backstage. diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 8a7b5ef528..70b2455a55 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/backend-common +## 0.12.1 + +### Patch Changes + +- Fixed runtime resolution of the `/alpha` entry point. + ## 0.12.0 ### Minor Changes diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index 72d4f8b04a..bea5f18f09 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/catalog-model +## 0.12.1 + +### Patch Changes + +- Fixed runtime resolution of the `/alpha` entry point. + ## 0.12.0 ### Minor Changes diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index cfae3cab06..10f7ce1d69 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/cli +## 0.15.1 + +### Patch Changes + +- Fixed an issue where the release stage entry point of packages were not resolved correctly. + ## 0.15.0 ### Minor Changes diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index fb197a65f5..d4a7d8b8d6 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-backend +## 0.23.1 + +### Patch Changes + +- Marked `GithubMultiOrgReaderProcessor` as stable, as it was moved to `/alpha` by mistake. +- Fixed runtime resolution of the `/alpha` entry point. +- Updated dependencies + - @backstage/backend-common@0.12.1 + - @backstage/catalog-model@0.12.1 + - @backstage/plugin-catalog-common@0.2.1 + ## 0.23.0 ### Minor Changes diff --git a/plugins/catalog-common/CHANGELOG.md b/plugins/catalog-common/CHANGELOG.md index 707207eda4..badffd5f0a 100644 --- a/plugins/catalog-common/CHANGELOG.md +++ b/plugins/catalog-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-catalog-common +## 0.2.1 + +### Patch Changes + +- Fixed runtime resolution of the `/alpha` entry point. + ## 0.2.0 ### Minor Changes diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 89e03aa445..b93a2f7451 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-catalog-react +## 0.8.1 + +### Patch Changes + +- Fixed runtime resolution of the `/alpha` entry point. +- Updated dependencies + - @backstage/catalog-model@0.12.1 + ## 0.8.0 ### Minor Changes From b0af81726d17a005c12a860a1861be150d57e1a9 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 4 Mar 2022 13:39:26 +0100 Subject: [PATCH 196/353] catalog-react: Remove reduceCatalogFilters & reduceEntityFilters Signed-off-by: Johan Haals --- .changeset/chilly-comics-jam.md | 5 +++++ plugins/catalog-react/api-report.md | 10 ---------- plugins/catalog-react/src/index.ts | 9 ++++++++- plugins/catalog-react/src/utils/filters.ts | 8 -------- 4 files changed, 13 insertions(+), 19 deletions(-) create mode 100644 .changeset/chilly-comics-jam.md diff --git a/.changeset/chilly-comics-jam.md b/.changeset/chilly-comics-jam.md new file mode 100644 index 0000000000..92d7f9014e --- /dev/null +++ b/.changeset/chilly-comics-jam.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +**BREAKING**: Removed `reduceCatalogFilters` and `reduceEntityFilters` due to low external utility value. diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index ca5de26048..6d6d4ecc27 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -477,16 +477,6 @@ export class MockStarredEntitiesApi implements StarredEntitiesApi { toggleStarred(entityRef: string): Promise; } -// @public @deprecated (undocumented) -export function reduceCatalogFilters( - filters: EntityFilter[], -): Record; - -// @public @deprecated (undocumented) -export function reduceEntityFilters( - filters: EntityFilter[], -): (entity: Entity) => boolean; - // @public export interface StarredEntitiesApi { starredEntitie$(): Observable>; diff --git a/plugins/catalog-react/src/index.ts b/plugins/catalog-react/src/index.ts index 2279d49a2c..6d5a5ad204 100644 --- a/plugins/catalog-react/src/index.ts +++ b/plugins/catalog-react/src/index.ts @@ -30,5 +30,12 @@ export * from './filters'; export { entityRouteParams, entityRouteRef } from './routes'; export * from './testUtils'; export * from './types'; -export * from './utils'; export * from './overridableComponents'; +export { + getEntityMetadataEditUrl, + getEntityMetadataViewUrl, + getEntityRelations, + getEntitySourceLocation, + isOwnerOf, +} from './utils'; +export type { EntitySourceLocation } from './utils'; diff --git a/plugins/catalog-react/src/utils/filters.ts b/plugins/catalog-react/src/utils/filters.ts index 68cd94fd6e..109e864c52 100644 --- a/plugins/catalog-react/src/utils/filters.ts +++ b/plugins/catalog-react/src/utils/filters.ts @@ -17,10 +17,6 @@ import { Entity } from '@backstage/catalog-model'; import { EntityFilter } from '../types'; -/** - * @public - * @deprecated will be made private. - */ export function reduceCatalogFilters( filters: EntityFilter[], ): Record { @@ -32,10 +28,6 @@ export function reduceCatalogFilters( }, {} as Record); } -/** - * @public - * @deprecated will be made private. - */ export function reduceEntityFilters( filters: EntityFilter[], ): (entity: Entity) => boolean { From a3eb3d2afaf85259607a969810009222eaa78b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 4 Mar 2022 13:25:23 +0100 Subject: [PATCH 197/353] remove getXByEntity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/two-mails-boil.md | 5 +++ packages/catalog-client/api-report.md | 10 ----- packages/catalog-client/src/CatalogClient.ts | 45 -------------------- 3 files changed, 5 insertions(+), 55 deletions(-) create mode 100644 .changeset/two-mails-boil.md diff --git a/.changeset/two-mails-boil.md b/.changeset/two-mails-boil.md new file mode 100644 index 0000000000..22651f1bd4 --- /dev/null +++ b/.changeset/two-mails-boil.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': minor +--- + +**BREAKING**: Removed `CatalogClient.getLocationByEntity` and `CatalogClient.getOriginLocationByEntity` which had previously been deprecated. Please use `CatalogApi.getLocationByRef` instead. Note that this only affects you if you were using `CatalogClient` (the class) directly, rather than `CatalogApi` (the interface), since it has been removed from the interface in an earlier release. diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index 9bbb24463b..d85b9c4d0d 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -107,11 +107,6 @@ export class CatalogClient implements CatalogApi { request: GetEntityFacetsRequest, options?: CatalogRequestOptions, ): Promise; - // @deprecated (undocumented) - getLocationByEntity( - entity: Entity, - options?: CatalogRequestOptions, - ): Promise; getLocationById( id: string, options?: CatalogRequestOptions, @@ -120,11 +115,6 @@ export class CatalogClient implements CatalogApi { locationRef: string, options?: CatalogRequestOptions, ): Promise; - // @deprecated (undocumented) - getOriginLocationByEntity( - entity: Entity, - options?: CatalogRequestOptions, - ): Promise; refreshEntity( entityRef: string, options?: CatalogRequestOptions, diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index f2bc3ed8f6..53397d061e 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -15,8 +15,6 @@ */ import { - ANNOTATION_LOCATION, - ANNOTATION_ORIGIN_LOCATION, Entity, CompoundEntityRef, parseEntityRef, @@ -308,49 +306,6 @@ export class CatalogClient implements CatalogApi { }; } - /** - * @deprecated please use getLocationByRef instead - */ - async getOriginLocationByEntity( - entity: Entity, - options?: CatalogRequestOptions, - ): Promise { - const locationCompound = - entity.metadata.annotations?.[ANNOTATION_ORIGIN_LOCATION]; - if (!locationCompound) { - return undefined; - } - const all: { data: Location }[] = await this.requestRequired( - 'GET', - '/locations', - options, - ); - return all - .map(r => r.data) - .find(l => locationCompound === stringifyLocationRef(l)); - } - - /** - * @deprecated please use getLocationByRef instead - */ - async getLocationByEntity( - entity: Entity, - options?: CatalogRequestOptions, - ): Promise { - const locationCompound = entity.metadata.annotations?.[ANNOTATION_LOCATION]; - if (!locationCompound) { - return undefined; - } - const all: { data: Location }[] = await this.requestRequired( - 'GET', - '/locations', - options, - ); - return all - .map(r => r.data) - .find(l => locationCompound === stringifyLocationRef(l)); - } - /** * {@inheritdoc CatalogApi.getLocationByRef} */ From c382100444c8a0018f7890ca6c67afa2b85e66cb Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 4 Mar 2022 13:53:49 +0100 Subject: [PATCH 198/353] catalog-react: Remove useEntityCompoundName Signed-off-by: Johan Haals --- plugins/catalog-react/src/hooks/index.ts | 1 - plugins/catalog-react/src/hooks/useEntity.tsx | 10 ++++--- .../src/hooks/useEntityCompoundName.ts | 27 ------------------- .../components/EntityLayout/EntityLayout.tsx | 5 ++-- plugins/rollbar/src/hooks/useCatalogEntity.ts | 9 +++---- 5 files changed, 13 insertions(+), 39 deletions(-) delete mode 100644 plugins/catalog-react/src/hooks/useEntityCompoundName.ts diff --git a/plugins/catalog-react/src/hooks/index.ts b/plugins/catalog-react/src/hooks/index.ts index da23d54ad7..907dad973b 100644 --- a/plugins/catalog-react/src/hooks/index.ts +++ b/plugins/catalog-react/src/hooks/index.ts @@ -25,7 +25,6 @@ export type { EntityProviderProps, AsyncEntityProviderProps, } from './useEntity'; -export { useEntityCompoundName } from './useEntityCompoundName'; export { EntityListContext, EntityListProvider, diff --git a/plugins/catalog-react/src/hooks/useEntity.tsx b/plugins/catalog-react/src/hooks/useEntity.tsx index f80f858dec..152c1ebe61 100644 --- a/plugins/catalog-react/src/hooks/useEntity.tsx +++ b/plugins/catalog-react/src/hooks/useEntity.tsx @@ -14,7 +14,11 @@ * limitations under the License. */ import { Entity } from '@backstage/catalog-model'; -import { errorApiRef, useApi } from '@backstage/core-plugin-api'; +import { + errorApiRef, + useApi, + useRouteRefParams, +} from '@backstage/core-plugin-api'; import { createVersionedContext, createVersionedValueMap, @@ -24,7 +28,7 @@ import React, { ReactNode, useEffect } from 'react'; import { useNavigate } from 'react-router'; import useAsyncRetry from 'react-use/lib/useAsyncRetry'; import { catalogApiRef } from '../api'; -import { useEntityCompoundName } from './useEntityCompoundName'; +import { entityRouteRef } from '../routes'; /** @public */ export type EntityLoadingStatus = { @@ -104,7 +108,7 @@ export const EntityProvider = ({ entity, children }: EntityProviderProps) => ( * @deprecated will be deleted shortly due to low external usage, re-implement if needed. */ export const useEntityFromUrl = (): EntityLoadingStatus => { - const { kind, namespace, name } = useEntityCompoundName(); + const { kind, namespace, name } = useRouteRefParams(entityRouteRef); const navigate = useNavigate(); const errorApi = useApi(errorApiRef); const catalogApi = useApi(catalogApiRef); diff --git a/plugins/catalog-react/src/hooks/useEntityCompoundName.ts b/plugins/catalog-react/src/hooks/useEntityCompoundName.ts deleted file mode 100644 index 481c18f8a0..0000000000 --- a/plugins/catalog-react/src/hooks/useEntityCompoundName.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { entityRouteRef } from '../routes'; -import { useRouteRefParams } from '@backstage/core-plugin-api'; - -/** - * Grabs entity kind, namespace, and name from the location - * @public - * @deprecated use {@link @backstage/core-plugin-api#useRouteRefParams} instead - */ -export const useEntityCompoundName = () => { - const { kind, namespace, name } = useRouteRefParams(entityRouteRef); - return { kind, namespace, name }; -}; diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index 40c4f9e873..d4244419cb 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -33,15 +33,16 @@ import { attachComponentData, IconComponent, useElementFilter, + useRouteRefParams, } from '@backstage/core-plugin-api'; import { EntityRefLinks, + entityRouteRef, FavoriteEntity, getEntityRelations, InspectEntityDialog, UnregisterEntityDialog, useAsyncEntity, - useEntityCompoundName, } from '@backstage/plugin-catalog-react'; import { Box, TabProps } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; @@ -176,7 +177,7 @@ export const EntityLayout = (props: EntityLayoutProps) => { UNSTABLE_contextMenuOptions, children, } = props; - const { kind, namespace, name } = useEntityCompoundName(); + const { kind, namespace, name } = useRouteRefParams(entityRouteRef); const { entity, loading, error } = useAsyncEntity(); const location = useLocation(); const routes = useElementFilter( diff --git a/plugins/rollbar/src/hooks/useCatalogEntity.ts b/plugins/rollbar/src/hooks/useCatalogEntity.ts index 21d22dd13c..4845a275be 100644 --- a/plugins/rollbar/src/hooks/useCatalogEntity.ts +++ b/plugins/rollbar/src/hooks/useCatalogEntity.ts @@ -14,16 +14,13 @@ * limitations under the License. */ -import { - catalogApiRef, - useEntityCompoundName, -} from '@backstage/plugin-catalog-react'; +import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; import useAsync from 'react-use/lib/useAsync'; -import { useApi } from '@backstage/core-plugin-api'; +import { useApi, useRouteRefParams } from '@backstage/core-plugin-api'; export function useCatalogEntity() { const catalogApi = useApi(catalogApiRef); - const { namespace, name } = useEntityCompoundName(); + const { namespace, name } = useRouteRefParams(entityRouteRef); const { value: entity, From 9844d4d2bd739324cac8c98445b8ea3700febbd6 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 4 Mar 2022 13:56:44 +0100 Subject: [PATCH 199/353] changesets Signed-off-by: Johan Haals --- .changeset/nasty-seahorses-bathe.md | 6 ++++++ .changeset/nice-windows-push.md | 5 +++++ 2 files changed, 11 insertions(+) create mode 100644 .changeset/nasty-seahorses-bathe.md create mode 100644 .changeset/nice-windows-push.md diff --git a/.changeset/nasty-seahorses-bathe.md b/.changeset/nasty-seahorses-bathe.md new file mode 100644 index 0000000000..884313e422 --- /dev/null +++ b/.changeset/nasty-seahorses-bathe.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog': patch +'@backstage/plugin-rollbar': patch +--- + +Removed usage of removed hook. diff --git a/.changeset/nice-windows-push.md b/.changeset/nice-windows-push.md new file mode 100644 index 0000000000..2d04141cf1 --- /dev/null +++ b/.changeset/nice-windows-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +**BREAKING**: Removed `useEntityCompoundName`, use `useRouteRefParams(entityRouteRef)` instead. From 5247f613ca9881157384c08a53c7bb29cd54a497 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 4 Mar 2022 13:57:35 +0100 Subject: [PATCH 200/353] update api report Signed-off-by: Johan Haals --- plugins/catalog-react/api-report.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index ca5de26048..42ceb29571 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -522,13 +522,6 @@ export function useEntity(): { refresh?: VoidFunction; }; -// @public @deprecated -export const useEntityCompoundName: () => { - kind: string; - namespace: string; - name: string; -}; - // @public @deprecated (undocumented) export const useEntityFromUrl: () => EntityLoadingStatus; From 2b8c986ce0a84c4cfc33001d318a97dfc714874b Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 4 Mar 2022 14:06:34 +0100 Subject: [PATCH 201/353] catalog-react: Remove useEntityListProvider Signed-off-by: Johan Haals --- .changeset/quiet-pens-wait.md | 5 +++++ plugins/catalog-react/api-report.md | 5 ----- plugins/catalog-react/src/hooks/index.ts | 1 - .../src/hooks/useEntityListProvider.tsx | 14 -------------- 4 files changed, 5 insertions(+), 20 deletions(-) create mode 100644 .changeset/quiet-pens-wait.md diff --git a/.changeset/quiet-pens-wait.md b/.changeset/quiet-pens-wait.md new file mode 100644 index 0000000000..b7510eab3f --- /dev/null +++ b/.changeset/quiet-pens-wait.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +**BREAKING**: Removed `useEntityListProvider` use `useEntityList` instead. diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index ca5de26048..ceedf9fe1a 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -544,11 +544,6 @@ export function useEntityList< EntityFilters extends DefaultEntityFilters = DefaultEntityFilters, >(): EntityListContextProps; -// @public @deprecated -export function useEntityListProvider< - EntityFilters extends DefaultEntityFilters = DefaultEntityFilters, ->(): EntityListContextProps; - // @public export function useEntityOwnership(): { loading: boolean; diff --git a/plugins/catalog-react/src/hooks/index.ts b/plugins/catalog-react/src/hooks/index.ts index da23d54ad7..9b4955fc91 100644 --- a/plugins/catalog-react/src/hooks/index.ts +++ b/plugins/catalog-react/src/hooks/index.ts @@ -29,7 +29,6 @@ export { useEntityCompoundName } from './useEntityCompoundName'; export { EntityListContext, EntityListProvider, - useEntityListProvider, useEntityList, } from './useEntityListProvider'; export type { diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index 72dfb7d1c3..ccac475ae8 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -249,20 +249,6 @@ export const EntityListProvider = ({ ); }; -/** - * Hook for interacting with the entity list context provided by the {@link EntityListProvider}. - * @public - * @deprecated use {@link useEntityList} instead. - */ -export function useEntityListProvider< - EntityFilters extends DefaultEntityFilters = DefaultEntityFilters, ->(): EntityListContextProps { - const context = useContext(EntityListContext); - if (!context) - throw new Error('useEntityList must be used within EntityListProvider'); - return context; -} - /** * Hook for interacting with the entity list context provided by the {@link EntityListProvider}. * @public From e421d775364ebb9a234c55259b862b786513f2fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 4 Mar 2022 14:09:50 +0100 Subject: [PATCH 202/353] remove deprecations in catalog-backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/many-swans-sort.md | 10 ++++ plugins/catalog-backend/api-report.md | 24 +------- .../src/api/deprecatedResult.ts | 4 +- .../src/api/processingResult.ts | 7 +-- plugins/catalog-backend/src/api/processor.ts | 2 - .../modules/core/StaticLocationProcessor.ts | 59 ------------------- .../catalog-backend/src/modules/core/index.ts | 1 - .../processing/ProcessorOutputCollector.ts | 3 +- plugins/catalog-backend/src/util/index.ts | 1 - .../src/util/runPeriodically.ts | 56 ------------------ 10 files changed, 17 insertions(+), 150 deletions(-) create mode 100644 .changeset/many-swans-sort.md delete mode 100644 plugins/catalog-backend/src/modules/core/StaticLocationProcessor.ts delete mode 100644 plugins/catalog-backend/src/util/runPeriodically.ts diff --git a/.changeset/many-swans-sort.md b/.changeset/many-swans-sort.md new file mode 100644 index 0000000000..fec613cf00 --- /dev/null +++ b/.changeset/many-swans-sort.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +**BREAKING**: + +- Removed the previously deprecated `runPeriodically` export. Please use the `@backstage/backend-tasks` package instead, or copy [the actual implementation](https://github.com/backstage/backstage/blob/02875d4d56708c60f86f6b0a5b3da82e24988354/plugins/catalog-backend/src/util/runPeriodically.ts#L29) into your own code if you explicitly do not want coordination of task runs across your worker nodes. +- Removed the previously deprecated `CatalogProcessorLocationResult.optional` field. Please set the corresponding `LocationSpec.presence` field to `'optional'` instead. +- Related to the previous point, the `processingResult.location` function no longer has a second boolean `optional` argument. Please set the corresponding `LocationSpec.presence` field to `'optional'` instead. +- Removed the previously deprecated `StaticLocationProcessor`. It has not been in use for some time; its functionality is covered by `ConfigLocationEntityProvider` instead. diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 0d9f306009..687f1029c5 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -333,7 +333,6 @@ export type CatalogProcessorErrorResult = { export type CatalogProcessorLocationResult = { type: 'location'; location: LocationSpec; - optional?: boolean; }; // @public @@ -812,7 +811,7 @@ function inputError( // @public @deprecated (undocumented) function location_2( newLocation: LocationSpec, - optional?: boolean, + _optional?: boolean, ): CatalogProcessorResult; // @public (undocumented) @@ -1007,10 +1006,7 @@ export const processingResult: Readonly<{ atLocation: LocationSpec, message: string, ) => CatalogProcessorResult; - readonly location: ( - newLocation: LocationSpec, - optional?: boolean | undefined, - ) => CatalogProcessorResult; + readonly location: (newLocation: LocationSpec) => CatalogProcessorResult; readonly entity: ( atLocation: LocationSpec, newEntity: Entity, @@ -1074,22 +1070,6 @@ export interface RouterOptions { refreshService?: RefreshService; } -// @public @deprecated -export function runPeriodically(fn: () => any, delayMs: number): () => void; - -// @public @deprecated (undocumented) -export class StaticLocationProcessor implements StaticLocationProcessor { - constructor(staticLocations: LocationSpec[]); - // (undocumented) - static fromConfig(config: Config): StaticLocationProcessor; - // (undocumented) - readLocation( - location: LocationSpec, - _optional: boolean, - emit: CatalogProcessorEmit, - ): Promise; -} - // @public (undocumented) export class UrlReaderProcessor implements CatalogProcessor { constructor(options: { reader: UrlReader; logger: Logger_2 }); diff --git a/plugins/catalog-backend/src/api/deprecatedResult.ts b/plugins/catalog-backend/src/api/deprecatedResult.ts index ecacc62c65..e616f3cf7d 100644 --- a/plugins/catalog-backend/src/api/deprecatedResult.ts +++ b/plugins/catalog-backend/src/api/deprecatedResult.ts @@ -68,9 +68,9 @@ export function generalError( */ export function location( newLocation: LocationSpec, - optional?: boolean, + _optional?: boolean, ): CatalogProcessorResult { - return { type: 'location', location: newLocation, optional }; + return { type: 'location', location: newLocation }; } /** diff --git a/plugins/catalog-backend/src/api/processingResult.ts b/plugins/catalog-backend/src/api/processingResult.ts index 5ad908dc8b..2fc2c9eac7 100644 --- a/plugins/catalog-backend/src/api/processingResult.ts +++ b/plugins/catalog-backend/src/api/processingResult.ts @@ -54,11 +54,8 @@ export const processingResult = Object.freeze({ return { type: 'error', location: atLocation, error: new Error(message) }; }, - location( - newLocation: LocationSpec, - optional?: boolean, - ): CatalogProcessorResult { - return { type: 'location', location: newLocation, optional }; + location(newLocation: LocationSpec): CatalogProcessorResult { + return { type: 'location', location: newLocation }; }, entity(atLocation: LocationSpec, newEntity: Entity): CatalogProcessorResult { diff --git a/plugins/catalog-backend/src/api/processor.ts b/plugins/catalog-backend/src/api/processor.ts index 12a008edc7..3f274d13ea 100644 --- a/plugins/catalog-backend/src/api/processor.ts +++ b/plugins/catalog-backend/src/api/processor.ts @@ -147,8 +147,6 @@ export type CatalogProcessorEmit = (generated: CatalogProcessorResult) => void; export type CatalogProcessorLocationResult = { type: 'location'; location: LocationSpec; - /** @deprecated Set `location.presence = 'optional'` instead */ - optional?: boolean; }; /** @public */ diff --git a/plugins/catalog-backend/src/modules/core/StaticLocationProcessor.ts b/plugins/catalog-backend/src/modules/core/StaticLocationProcessor.ts deleted file mode 100644 index e22d011695..0000000000 --- a/plugins/catalog-backend/src/modules/core/StaticLocationProcessor.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Config } from '@backstage/config'; -import { - processingResult, - CatalogProcessorEmit, - LocationSpec, -} from '../../api'; - -/** - * @deprecated no longer in use, replaced by the ConfigLocationEntityProvider. - * @public - */ -export class StaticLocationProcessor implements StaticLocationProcessor { - static fromConfig(config: Config): StaticLocationProcessor { - const locations: LocationSpec[] = []; - - const lConfigs = config.getOptionalConfigArray('catalog.locations') ?? []; - for (const lConfig of lConfigs) { - const type = lConfig.getString('type'); - const target = lConfig.getString('target'); - locations.push({ type, target }); - } - - return new StaticLocationProcessor(locations); - } - - constructor(private readonly staticLocations: LocationSpec[]) {} - - async readLocation( - location: LocationSpec, - _optional: boolean, - emit: CatalogProcessorEmit, - ): Promise { - if (location.type !== 'bootstrap') { - return false; - } - - for (const staticLocation of this.staticLocations) { - emit(processingResult.location(staticLocation)); - } - - return true; - } -} diff --git a/plugins/catalog-backend/src/modules/core/index.ts b/plugins/catalog-backend/src/modules/core/index.ts index 9d582bfb34..364cede069 100644 --- a/plugins/catalog-backend/src/modules/core/index.ts +++ b/plugins/catalog-backend/src/modules/core/index.ts @@ -28,6 +28,5 @@ export type { PlaceholderResolverRead, PlaceholderResolverResolveUrl, } from './PlaceholderProcessor'; -export { StaticLocationProcessor } from './StaticLocationProcessor'; export { UrlReaderProcessor } from './UrlReaderProcessor'; export { parseEntityYaml } from '../util/parse'; diff --git a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts index c3cc6e288b..ff12f2f3db 100644 --- a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts +++ b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts @@ -106,9 +106,8 @@ export class ProcessorOutputCollector { this.deferredEntities.push({ entity, locationKey: location }); } else if (i.type === 'location') { - const presence = i.optional ? 'optional' : 'required'; const entity = locationSpecToLocationEntity( - { presence, ...i.location }, + i.location, this.parentEntity, ); const locationKey = getEntityLocationRef(entity); diff --git a/plugins/catalog-backend/src/util/index.ts b/plugins/catalog-backend/src/util/index.ts index 13a9c84b0f..0b5a942e0c 100644 --- a/plugins/catalog-backend/src/util/index.ts +++ b/plugins/catalog-backend/src/util/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export * from './runPeriodically'; export * from './RecursivePartial'; diff --git a/plugins/catalog-backend/src/util/runPeriodically.ts b/plugins/catalog-backend/src/util/runPeriodically.ts deleted file mode 100644 index 361fb33b2f..0000000000 --- a/plugins/catalog-backend/src/util/runPeriodically.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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. - */ - -/** - * Runs a function repeatedly, with a fixed wait between invocations. - * - * Supports async functions, and silently ignores exceptions and rejections. - * - * @param fn - The function to run. May return a Promise. - * @param delayMs - The delay between a completed function invocation and the - * next. - * @returns A function that, when called, stops the invocation loop. - * @deprecated use \@backstage/backend-tasks package instead. - * @public - */ -export function runPeriodically(fn: () => any, delayMs: number): () => void { - let cancel: () => void; - let cancelled = false; - const cancellationPromise = new Promise(resolve => { - cancel = () => { - resolve(); - cancelled = true; - }; - }); - - const startRefresh = async () => { - while (!cancelled) { - try { - await fn(); - } catch { - // ignore intentionally - } - - await Promise.race([ - new Promise(resolve => setTimeout(resolve, delayMs)), - cancellationPromise, - ]); - } - }; - startRefresh(); - - return cancel!; -} From 1360f7d73a2ed1bda6496b6a40a04cbb7e833557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 4 Mar 2022 14:38:52 +0100 Subject: [PATCH 203/353] remove old ScaffolderTaskOutput link outputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/lazy-points-explain.md | 5 +++ plugins/scaffolder/api-report.md | 16 ++++---- .../src/components/TaskPage/TaskPage.tsx | 8 +--- .../TaskPage/TaskPageLinks.test.tsx | 37 ------------------- .../src/components/TaskPage/TaskPageLinks.tsx | 18 +-------- plugins/scaffolder/src/index.ts | 1 + plugins/scaffolder/src/types.ts | 8 ++-- 7 files changed, 20 insertions(+), 73 deletions(-) create mode 100644 .changeset/lazy-points-explain.md diff --git a/.changeset/lazy-points-explain.md b/.changeset/lazy-points-explain.md new file mode 100644 index 0000000000..ced31c2195 --- /dev/null +++ b/.changeset/lazy-points-explain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +**BREAKING**: Removed `ScaffolderTaskOutput.entityRef` and `ScaffolderTaskOutput.remoteUrl`, which both have been deprecated for over a year. Please use the `links` output instead. diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index b51f18fcc9..9326a61fd5 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -314,6 +314,14 @@ export interface ScaffolderGetIntegrationsListResponse { }[]; } +// @public (undocumented) +export type ScaffolderOutputLink = { + title?: string; + icon?: string; + url?: string; + entityRef?: string; +}; + // Warning: (ae-missing-release-tag) "ScaffolderPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -372,12 +380,8 @@ export type ScaffolderTask = { createdAt: string; }; -// Warning: (ae-missing-release-tag) "ScaffolderTaskOutput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type ScaffolderTaskOutput = { - entityRef?: string; - remoteUrl?: string; links?: ScaffolderOutputLink[]; } & { [key: string]: unknown; @@ -451,8 +455,4 @@ export const TemplateTypePicker: () => JSX.Element | null; // @public export const useTemplateSecrets: () => ScaffolderUseTemplateSecrets; - -// Warnings were encountered during analysis: -// -// src/types.d.ts:32:5 - (ae-forgotten-export) The symbol "ScaffolderOutputLink" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index e0445ed641..aab5e8348a 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -217,12 +217,8 @@ export const TaskStatusStepper = memo( }, ); -const hasLinks = ({ - entityRef, - remoteUrl, - links = [], -}: ScaffolderTaskOutput): boolean => - !!(entityRef || remoteUrl || links.length > 0); +const hasLinks = ({ links = [] }: ScaffolderTaskOutput): boolean => + links.length > 0; /** * TaskPageProps for constructing a TaskPage diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.test.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.test.tsx index 8316b67ff5..2f5d015f8d 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.test.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.test.tsx @@ -26,43 +26,6 @@ describe('TaskPageLinks', () => { jest.resetAllMocks(); }); - it('renders the entityRef link', async () => { - const output = { entityRef: 'Component:default/my-app' }; - const { findByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name/*': entityRouteRef, - }, - }, - ); - - const element = await findByText('Open in catalog'); - - expect(element).toBeInTheDocument(); - expect(element).toHaveAttribute( - 'href', - '/catalog/default/Component/my-app', - ); - }); - - it('renders the remoteUrl link', async () => { - const output = { remoteUrl: 'https://remote.url' }; - const { findByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name/*': entityRouteRef, - }, - }, - ); - - const element = await findByText('Repo'); - - expect(element).toBeInTheDocument(); - expect(element).toHaveAttribute('href', 'https://remote.url'); - }); - it('renders further links', async () => { const output = { links: [ diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx index 5451b4e725..cb77689b6e 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx @@ -28,29 +28,13 @@ type TaskPageLinksProps = { }; export const TaskPageLinks = ({ output }: TaskPageLinksProps) => { - const { entityRef: entityRefOutput, remoteUrl } = output; - let { links = [] } = output; + const { links = [] } = output; const app = useApp(); const entityRoute = useRouteRef(entityRouteRef); const iconResolver = (key?: string): IconComponent => key ? app.getSystemIcon(key) ?? LanguageIcon : LanguageIcon; - if (remoteUrl) { - links = [{ url: remoteUrl, title: 'Repo' }, ...links]; - } - - if (entityRefOutput) { - links = [ - { - entityRef: entityRefOutput, - title: 'Open in catalog', - icon: 'catalog', - }, - ...links, - ]; - } - return ( {links diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index f33cf289f4..34b9155d66 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -28,6 +28,7 @@ export type { ScaffolderApi, ScaffolderGetIntegrationsListOptions, ScaffolderGetIntegrationsListResponse, + ScaffolderOutputLink, ScaffolderScaffoldOptions, ScaffolderScaffoldResponse, ScaffolderStreamLogsOptions, diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index 0ffbd601a8..069c28d85c 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -43,18 +43,16 @@ export type ListActionsResponse = Array<{ }; }>; -type ScaffolderOutputLink = { +/** @public */ +export type ScaffolderOutputLink = { title?: string; icon?: string; url?: string; entityRef?: string; }; +/** @public */ export type ScaffolderTaskOutput = { - /** @deprecated use the `links` property to link out to relevant resources */ - entityRef?: string; - /** @deprecated use the `links` property to link out to relevant resources */ - remoteUrl?: string; links?: ScaffolderOutputLink[]; } & { [key: string]: unknown; From 6a1fe077adddb347db12c4c31bafa1ba18b97938 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 4 Mar 2022 13:20:32 +0100 Subject: [PATCH 204/353] cli: switch rollup config to consider all package modules external Signed-off-by: Patrik Oldsberg --- .changeset/tender-berries-lie.md | 5 ++ docs/local-dev/cli-build-system.md | 10 ++-- packages/cli/package.json | 1 - packages/cli/src/lib/builder/config.test.ts | 59 +++++++++++++++++++++ packages/cli/src/lib/builder/config.ts | 22 +++++--- yarn.lock | 5 -- 6 files changed, 84 insertions(+), 18 deletions(-) create mode 100644 .changeset/tender-berries-lie.md create mode 100644 packages/cli/src/lib/builder/config.test.ts diff --git a/.changeset/tender-berries-lie.md b/.changeset/tender-berries-lie.md new file mode 100644 index 0000000000..74a3eb4707 --- /dev/null +++ b/.changeset/tender-berries-lie.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Changed the logic for how modules are marked as external in the Rollup build of packages. Rather than only marking dependencies and build-in Node.js modules as external, all non-relative imports are now considered external. diff --git a/docs/local-dev/cli-build-system.md b/docs/local-dev/cli-build-system.md index f57efc1255..aed3ba70bc 100644 --- a/docs/local-dev/cli-build-system.md +++ b/docs/local-dev/cli-build-system.md @@ -181,12 +181,10 @@ files like stylesheets or images. For more details on what syntax and file formats are supported by the build process, see the [loaders section](#loaders). When building CommonJS or ESM output, the build commands will always use -`src/index.ts` as the entrypoint. All dependencies of the package will be marked -as external, meaning that in general it is only the contents of the `src` folder -that ends up being compiled and output to `dist`. All import statements of -external dependencies, even within the same monorepo, will stay intact. The -externalized dependencies are based on dependency information in `package.json`, -which means it's important to keep it up to date. +`src/index.ts` as the entrypoint. All non-relative modules imports are considered +external, meaning the Rollup build will only compile the source code of the package +itself. All import statements of external dependencies, even within the same +monorepo, will stay intact. The build of the type definitions works quite differently. The entrypoint of the type definition build is the relative location of the package within the diff --git a/packages/cli/package.json b/packages/cli/package.json index af2c3afb2d..ec158fa5c1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -103,7 +103,6 @@ "rollup": "^2.60.2", "rollup-plugin-dts": "^4.0.1", "rollup-plugin-esbuild": "^4.7.2", - "rollup-plugin-peer-deps-external": "^2.2.2", "rollup-plugin-postcss": "^4.0.0", "rollup-pluginutils": "^2.8.2", "run-script-webpack-plugin": "^0.0.11", diff --git a/packages/cli/src/lib/builder/config.test.ts b/packages/cli/src/lib/builder/config.test.ts new file mode 100644 index 0000000000..a2e99da73b --- /dev/null +++ b/packages/cli/src/lib/builder/config.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { ExternalOption } from 'rollup'; +import { makeRollupConfigs } from './config'; +import { Output } from './types'; + +describe('makeRollupConfigs', () => { + it('should mark external modules correctly', async () => { + const importerPath = '/some/path.ts'; // when specified we don't care about the path + + const [config] = await makeRollupConfigs({ + outputs: new Set([Output.cjs]), + }); + const external = config.external as Exclude< + ExternalOption, + string | RegExp | (string | RegExp)[] + >; + + expect(external('foo', importerPath, false)).toBe(true); + expect(external('./foo', importerPath, false)).toBe(false); + expect(external('/foo', importerPath, false)).toBe(false); + expect(external('.\\foo', importerPath, false)).toBe(false); + expect(external('c:\\foo', importerPath, false)).toBe(false); + expect(external('@foo/bar', importerPath, false)).toBe(true); + expect(external('../foo', importerPath, false)).toBe(false); + + // Modules without an importer are entry points, i.e. not external + expect(external('foo', undefined, false)).toBe(false); + expect(external('./foo', undefined, false)).toBe(false); + expect(external('/foo', undefined, false)).toBe(false); + expect(external('.\\foo', undefined, false)).toBe(false); + expect(external('c:\\foo', undefined, false)).toBe(false); + expect(external('@foo/bar', undefined, false)).toBe(false); + expect(external('../foo', undefined, false)).toBe(false); + + // After modules have been resolved they're never marked as external + expect(external('foo', importerPath, true)).toBe(false); + expect(external('./foo', importerPath, true)).toBe(false); + expect(external('/foo', importerPath, true)).toBe(false); + expect(external('.\\foo', importerPath, true)).toBe(false); + expect(external('c:\\foo', importerPath, true)).toBe(false); + expect(external('@foo/bar', importerPath, true)).toBe(false); + expect(external('../foo', importerPath, true)).toBe(false); + }); +}); diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts index 0a214f1a7a..fe91cedbb0 100644 --- a/packages/cli/src/lib/builder/config.ts +++ b/packages/cli/src/lib/builder/config.ts @@ -17,7 +17,6 @@ import chalk from 'chalk'; import fs from 'fs-extra'; import { relative as relativePath, resolve as resolvePath } from 'path'; -import peerDepsExternal from 'rollup-plugin-peer-deps-external'; import commonjs from '@rollup/plugin-commonjs'; import resolve from '@rollup/plugin-node-resolve'; import postcss from 'rollup-plugin-postcss'; @@ -33,6 +32,19 @@ import { BuildOptions, Output } from './types'; import { paths } from '../paths'; import { svgrTemplate } from '../svgrTemplate'; +function isFileImport(source: string) { + if (source.startsWith('.')) { + return true; + } + if (source.startsWith('/')) { + return true; + } + if (source.match(/[a-z]:/i)) { + return true; + } + return false; +} + export async function makeRollupConfigs( options: BuildOptions, ): Promise { @@ -81,12 +93,10 @@ export async function makeRollupConfigs( output, onwarn, preserveEntrySignatures: 'strict', - external: require('module').builtinModules, + // All module imports are always marked as external + external: (source, importer, isResolved) => + Boolean(importer && !isResolved && !isFileImport(source)), plugins: [ - peerDepsExternal({ - packageJsonPath: resolvePath(targetDir, 'package.json'), - includeDependencies: true, - }), resolve({ mainFields }), commonjs({ include: /node_modules/, diff --git a/yarn.lock b/yarn.lock index 5d8b1c24d7..02532c5286 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22048,11 +22048,6 @@ rollup-plugin-esbuild@^4.7.2: joycon "^3.0.1" jsonc-parser "^3.0.0" -rollup-plugin-peer-deps-external@^2.2.2: - version "2.2.4" - resolved "https://registry.npmjs.org/rollup-plugin-peer-deps-external/-/rollup-plugin-peer-deps-external-2.2.4.tgz#8a420bbfd6dccc30aeb68c9bf57011f2f109570d" - integrity sha512-AWdukIM1+k5JDdAqV/Cxd+nejvno2FVLVeZ74NKggm3Q5s9cbbcOgUPGdbxPi4BXu7xGaZ8HG12F+thImYu/0g== - rollup-plugin-postcss@*, rollup-plugin-postcss@^4.0.0: version "4.0.2" resolved "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-4.0.2.tgz#15e9462f39475059b368ce0e49c800fa4b1f7050" From 1bcf139fca64ea281a41e9f11cb649f75897dc6b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 4 Mar 2022 14:58:51 +0100 Subject: [PATCH 205/353] SECURITY.md: add section with coding practices Signed-off-by: Patrik Oldsberg --- SECURITY.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index ba96c694ee..caf90669c1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -36,3 +36,34 @@ There are many situations where a vulnerability does not affect a particular dep To work around this and other similar issues, Snyk provides a method to ignore vulnerabilities. In order to provide the best visibility and most utility to adopters of Backstage, we store these ignore rules in `.snyk` policy files. This allows adopters to rely on our ignore policies if they wish to do so. Adding a new ignore policy is done by creating or modifying an existing `.snyk` file within a package root. See the [Snyk Documentation](https://support.snyk.io/hc/en-us/articles/360007487097-The-snyk-file) for details on the syntax. Always include a description, full path, and time limit of the ignore policy. + +## Coding Practices + +In this section we highlight patterns where particular care needs to be taken in order to avoid security vulnerabilities, as well as the best practices to follow. + +### Local file path resolution + +A common pitfall in backend packages is to resolve local file paths based on user input, without sufficiently protecting against input that traverses outside the intended directory. + +For example, consider the following code: + +```ts +import { join } from 'path'; +import fs from 'fs-extra'; + +function writeTemporaryFile(tmpDir: string, name: string, content: string) { + // WARNING: DO NOT DO THIS + const filePath = join(tmpDir, name); + await fs.writeFile(filePath, content); +} +``` + +If the `name` of the file is controlled by the user, they can for example enter `../../../../../../../../etc/hosts` as the name of the file. This can lead to a file being written outside the intended directory, which in turn can be used to inject malicious code or other form of attacks. + +The recommended solution to this is to use `resolveSafeChildPath` from `@backstage/backend-common` to resolve the file path instead. It makes sure that the resolved path does not fall outside the provided directory. If you simply what to validate whether a file path is safe, you can use `isChildPath` instead. + +### Express responses + +When returning a response from an Express route, always use `.json(...)` or `.end()` without any arguments. This ensures that the response can not be interpreted as an HTML document with embedded JavaScript by the browser. Never use `.send(...)` unless you want to send other forms of content, and be sure to always set an explicit content type. If you need to return HTML or other content that may be executed by the browser, be very careful how you handle user input. + +If you want to return an error response, simply throw the error or pass it on to the `next(...)` callback. There is a middleware installed that will transform the error into a JSON response. Many of the common HTTP errors are available from `@backstage/errors`, for example `NotFoundError`, which will also set the correct status code. From dd88d1e3ac7aa7a9c33d4e3009cab64a876727e1 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 4 Mar 2022 13:47:01 +0100 Subject: [PATCH 206/353] catalog-react: Remove `useEntityFromUrl` Signed-off-by: Johan Haals --- .changeset/late-pianos-attend.md | 5 +++ plugins/catalog-react/api-report.md | 3 -- plugins/catalog-react/src/hooks/index.ts | 1 - plugins/catalog-react/src/hooks/useEntity.tsx | 40 +------------------ 4 files changed, 6 insertions(+), 43 deletions(-) create mode 100644 .changeset/late-pianos-attend.md diff --git a/.changeset/late-pianos-attend.md b/.changeset/late-pianos-attend.md new file mode 100644 index 0000000000..e1e0aac392 --- /dev/null +++ b/.changeset/late-pianos-attend.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +**BREAKING**: Removed `useEntityFromUrl`. diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 40764b8844..f0d267d397 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -512,9 +512,6 @@ export function useEntity(): { refresh?: VoidFunction; }; -// @public @deprecated (undocumented) -export const useEntityFromUrl: () => EntityLoadingStatus; - // @public @deprecated export function useEntityKinds(): { error: Error | undefined; diff --git a/plugins/catalog-react/src/hooks/index.ts b/plugins/catalog-react/src/hooks/index.ts index e26f01928d..879ead8809 100644 --- a/plugins/catalog-react/src/hooks/index.ts +++ b/plugins/catalog-react/src/hooks/index.ts @@ -15,7 +15,6 @@ */ export { useEntity, - useEntityFromUrl, EntityProvider, AsyncEntityProvider, useAsyncEntity, diff --git a/plugins/catalog-react/src/hooks/useEntity.tsx b/plugins/catalog-react/src/hooks/useEntity.tsx index 152c1ebe61..75a623aaed 100644 --- a/plugins/catalog-react/src/hooks/useEntity.tsx +++ b/plugins/catalog-react/src/hooks/useEntity.tsx @@ -14,21 +14,12 @@ * limitations under the License. */ import { Entity } from '@backstage/catalog-model'; -import { - errorApiRef, - useApi, - useRouteRefParams, -} from '@backstage/core-plugin-api'; import { createVersionedContext, createVersionedValueMap, useVersionedContext, } from '@backstage/version-bridge'; -import React, { ReactNode, useEffect } from 'react'; -import { useNavigate } from 'react-router'; -import useAsyncRetry from 'react-use/lib/useAsyncRetry'; -import { catalogApiRef } from '../api'; -import { entityRouteRef } from '../routes'; +import React, { ReactNode } from 'react'; /** @public */ export type EntityLoadingStatus = { @@ -104,35 +95,6 @@ export const EntityProvider = ({ entity, children }: EntityProviderProps) => ( /> ); -/** @public - * @deprecated will be deleted shortly due to low external usage, re-implement if needed. - */ -export const useEntityFromUrl = (): EntityLoadingStatus => { - const { kind, namespace, name } = useRouteRefParams(entityRouteRef); - const navigate = useNavigate(); - const errorApi = useApi(errorApiRef); - const catalogApi = useApi(catalogApiRef); - - const { - value: entity, - error, - loading, - retry: refresh, - } = useAsyncRetry( - () => catalogApi.getEntityByRef({ kind, namespace, name }), - [catalogApi, kind, namespace, name], - ); - - useEffect(() => { - if (!name) { - errorApi.post(new Error('No name provided!')); - navigate('/'); - } - }, [errorApi, navigate, error, loading, entity, name]); - - return { entity, loading, error, refresh }; -}; - /** * Grab the current entity from the context, throws if the entity has not yet been loaded * or is not available. From f9c7bdd89994970edc1b8158073becbba2665135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 4 Mar 2022 15:07:00 +0100 Subject: [PATCH 207/353] remove the last remnants of cookiecutter from scaffolder-backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/brave-brooms-tie.md | 33 +++++++++++++++++++ .changeset/many-coins-drive.md | 8 +++++ packages/backend/src/plugins/scaffolder.ts | 7 ---- .../backend/src/plugins/scaffolder.ts | 6 ---- plugins/scaffolder-backend/api-report.md | 8 ----- plugins/scaffolder-backend/package.json | 1 - .../actions/builtin/createBuiltinActions.ts | 22 +------------ .../src/scaffolder/actions/builtin/index.ts | 3 -- .../src/service/router.test.ts | 2 -- .../scaffolder-backend/src/service/router.ts | 9 +---- 10 files changed, 43 insertions(+), 56 deletions(-) create mode 100644 .changeset/brave-brooms-tie.md create mode 100644 .changeset/many-coins-drive.md diff --git a/.changeset/brave-brooms-tie.md b/.changeset/brave-brooms-tie.md new file mode 100644 index 0000000000..a4b986c9d5 --- /dev/null +++ b/.changeset/brave-brooms-tie.md @@ -0,0 +1,33 @@ +--- +'@backstage/create-app': patch +--- + +Builtin support for cookiecutter based templates has been removed from `@backstage/plugin-scaffolder-backend`. Due to this, the `containerRunner` argument to its `createRouter` has also been removed. + +If you do not use cookiecutter templates and are fine with removing support from it in your own installation, update your `packages/backend/src/plugins/scaffolder.ts` file as follows: + +```diff +-import { DockerContainerRunner } from '@backstage/backend-common'; + import { CatalogClient } from '@backstage/catalog-client'; + import { createRouter } from '@backstage/plugin-scaffolder-backend'; +-import Docker from 'dockerode'; + import { Router } from 'express'; + import type { PluginEnvironment } from '../types'; + + export default async function createPlugin({ + reader, + discovery, + }: PluginEnvironment): Promise { +- const dockerClient = new Docker(); +- const containerRunner = new DockerContainerRunner({ dockerClient }); +- + const catalogClient = new CatalogClient({ discoveryApi: discovery }); +- + return await createRouter({ +- containerRunner, + logger, + config, + // ... +``` + +If you want to retain cookiecutter support, please use the `@backstage/plugin-scaffolder-backend-module-cookiecutter` package explicitly (see [its README](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend-module-cookiecutter) for installation instructions). diff --git a/.changeset/many-coins-drive.md b/.changeset/many-coins-drive.md new file mode 100644 index 0000000000..b28a0b0519 --- /dev/null +++ b/.changeset/many-coins-drive.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +**BREAKING**: + +- Removed the `createFetchCookiecutterAction` export, please use the `@backstage/plugin-scaffolder-backend-module-cookiecutter` package explicitly (see [its README](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend-module-cookiecutter) for installation instructions). +- Removed the `containerRunner` argument from the types `RouterOptions` (as used by `createRouter`) and `CreateBuiltInActionsOptions` (as used by `createBuiltinActions`). diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index 619134a990..e0f3a4b1ca 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -14,10 +14,8 @@ * limitations under the License. */ -import { DockerContainerRunner } from '@backstage/backend-common'; import { CatalogClient } from '@backstage/catalog-client'; import { createRouter } from '@backstage/plugin-scaffolder-backend'; -import Docker from 'dockerode'; import { Router } from 'express'; import type { PluginEnvironment } from '../types'; @@ -28,13 +26,8 @@ export default async function createPlugin({ reader, discovery, }: PluginEnvironment): Promise { - const dockerClient = new Docker(); - const containerRunner = new DockerContainerRunner({ dockerClient }); - const catalogClient = new CatalogClient({ discoveryApi: discovery }); - return await createRouter({ - containerRunner, logger, config, database, 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 6be2e9712d..a460fd8a6d 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 @@ -1,7 +1,5 @@ -import { DockerContainerRunner } from '@backstage/backend-common'; import { CatalogClient } from '@backstage/catalog-client'; import { createRouter } from '@backstage/plugin-scaffolder-backend'; -import Docker from 'dockerode'; import { Router } from 'express'; import type { PluginEnvironment } from '../types'; @@ -12,12 +10,8 @@ export default async function createPlugin({ reader, discovery, }: PluginEnvironment): Promise { - const dockerClient = new Docker(); - const containerRunner = new DockerContainerRunner({ dockerClient }); const catalogClient = new CatalogClient({ discoveryApi: discovery }); - return await createRouter({ - containerRunner, logger, config, database, diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index b18e199f40..a5e8ede8bb 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -9,8 +9,6 @@ import { CatalogApi } from '@backstage/catalog-client'; import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; import { Config } from '@backstage/config'; -import { ContainerRunner } from '@backstage/backend-common'; -import { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-backend-module-cookiecutter'; import { createPullRequest } from 'octokit-plugin-create-pull-request'; import { Entity } from '@backstage/catalog-model'; import express from 'express'; @@ -63,8 +61,6 @@ export interface CreateBuiltInActionsOptions { catalogClient: CatalogApi; // (undocumented) config: Config; - // @deprecated (undocumented) - containerRunner?: ContainerRunner; // (undocumented) integrations: ScmIntegrations; // (undocumented) @@ -99,8 +95,6 @@ export function createDebugLogAction(): TemplateAction<{ listWorkspace?: boolean | undefined; }>; -export { createFetchCookiecutterAction }; - // @public export function createFetchPlainAction(options: { reader: UrlReader; @@ -428,8 +422,6 @@ export interface RouterOptions { // (undocumented) config: Config; // (undocumented) - containerRunner?: ContainerRunner; - // (undocumented) database: PluginDatabaseManager; // (undocumented) logger: Logger_2; diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 09df1818b4..f0822c27bb 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -42,7 +42,6 @@ "@backstage/integration": "^0.8.0", "@backstage/plugin-catalog-backend": "^0.23.0", "@backstage/plugin-scaffolder-common": "^0.2.3", - "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.2.3", "@backstage/types": "^0.1.3", "@gitbeaker/core": "^34.6.0", "@gitbeaker/node": "^35.1.0", diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index c03ba150c2..7405dc4de8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ContainerRunner, UrlReader } from '@backstage/backend-common'; +import { UrlReader } from '@backstage/backend-common'; import { JsonObject } from '@backstage/types'; import { CatalogApi } from '@backstage/catalog-client'; import { @@ -30,7 +30,6 @@ import { import { createDebugLogAction } from './debug'; import { createFetchPlainAction, createFetchTemplateAction } from './fetch'; -import { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-backend-module-cookiecutter'; import { createFilesystemDeleteAction, createFilesystemRenameAction, @@ -49,7 +48,6 @@ import { } from './github'; import { TemplateFilter } from '../../../lib'; import { TemplateAction } from '../types'; -import { getRootLogger } from '@backstage/backend-common'; /** * The options passed to {@link createBuiltinActions} @@ -59,8 +57,6 @@ export interface CreateBuiltInActionsOptions { reader: UrlReader; integrations: ScmIntegrations; catalogClient: CatalogApi; - /** @deprecated when the cookiecutter action is removed this won't be necessary */ - containerRunner?: ContainerRunner; config: Config; additionalTemplateFilters?: Record; } @@ -78,7 +74,6 @@ export const createBuiltinActions = ( const { reader, integrations, - containerRunner, catalogClient, config, additionalTemplateFilters, @@ -135,20 +130,5 @@ export const createBuiltinActions = ( }), ]; - if (containerRunner) { - getRootLogger().warn( - `[DEPRECATED] The fetch:cookiecutter action will be removed part of the default scaffolder actions in later versions. -You can install the package seperately and remove the containerRunner from the createBuiltInActions to remove this warning, -or you can migrate to using fetch:template https://backstage.io/docs/features/software-templates/builtin-actions#migrating-from-fetchcookiecutter-to-fetchtemplate`, - ); - actions.push( - createFetchCookiecutterAction({ - reader, - integrations, - containerRunner, - }), - ); - } - return actions as TemplateAction[]; }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts index 8be1b62eaa..553481ba40 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts @@ -22,8 +22,5 @@ export * from './filesystem'; export * from './publish'; export * from './github'; -/** @deprecated please add this package to your own installation manually */ -export { createFetchCookiecutterAction } from '@backstage/plugin-scaffolder-backend-module-cookiecutter'; - export { runCommand, executeShellCommand } from './helpers'; export type { RunCommandOptions } from './helpers'; diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 4041e41cf8..1c57153fb0 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -30,7 +30,6 @@ jest.doMock('fs-extra', () => ({ import { DatabaseManager, - DockerContainerRunner, getVoidLogger, PluginDatabaseManager, UrlReaders, @@ -122,7 +121,6 @@ describe('createRouter', () => { config: new ConfigReader({}), database: createDatabase(), catalogClient: createCatalogClient(template), - containerRunner: new DockerContainerRunner({} as any), reader: mockUrlReader, taskBroker, }); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 8f793a34d8..f7e375b0b2 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -14,11 +14,7 @@ * limitations under the License. */ -import { - ContainerRunner, - PluginDatabaseManager, - UrlReader, -} from '@backstage/backend-common'; +import { PluginDatabaseManager, UrlReader } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; @@ -61,7 +57,6 @@ export interface RouterOptions { catalogClient: CatalogApi; actions?: TemplateAction[]; taskWorkers?: number; - containerRunner?: ContainerRunner; taskBroker?: TaskBroker; additionalTemplateFilters?: Record; } @@ -89,7 +84,6 @@ export async function createRouter( database, catalogClient, actions, - containerRunner, taskWorkers, additionalTemplateFilters, } = options; @@ -128,7 +122,6 @@ export async function createRouter( : createBuiltinActions({ integrations, catalogClient, - containerRunner, reader, config, additionalTemplateFilters, From 358fc79ff01f4a52b1e55b618869dfa7126ce85d Mon Sep 17 00:00:00 2001 From: Karan Shah Date: Fri, 4 Mar 2022 14:30:52 +0000 Subject: [PATCH 208/353] Remove any changes with ADOPTERS.md Signed-off-by: Karan Shah --- ADOPTERS.md | 202 ++++++++++++++++++++++++++-------------------------- 1 file changed, 101 insertions(+), 101 deletions(-) diff --git a/ADOPTERS.md b/ADOPTERS.md index d22978cca9..5aad5dcf72 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -2,104 +2,104 @@ _If you're using Backstage in your organization, please try to add your company name to this list. This really helps the project to gain momentum and credibility. It's a small contribution back to the project with a big impact._ -| Organization | Contact | Description of Use | -| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | Main interface towards all of Spotify's infrastructure and technical documentation. | -| [bol.com](https://www.bol.com) | [@sagacity](https://github.com/sagacity) | Initial work being done to unify platform tooling. | -| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | -| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | -| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | -| [SDA SE](https://sda.se) | [@dschwank](https://github.com/dschwank), [@iammnils](https://github.com/iammnils) | Central place for developing and sharing services in our insurance ecosystem. | -| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | -| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | -| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. | -| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | -| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D | -| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling | -| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling | -| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks | -| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. | -| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. | -| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. | -| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo), [Erik Lindgren](https://github.com/lindgren) | EG Common Developer Toolkit | -| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go | -| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | -| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. | -| [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | -| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. | -| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. | -| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a team’s engineering dependencies. | -| [Oriflame](https://www.oriflame.com/) | [Oriflame](https://github.com/oriflame) | Internal developer portal for services, single page apps and packages overview, API documentation, technical guides, tech-radar and more. | -| [Booz Allen Hamilton](https://www.boozallen.com/) | [Jason Miller](https://github.com/JasonMiller-BAH) | Developer portal for a full-stack software development ecosystem that accelerates consistent and repeatable Modern Software Development practices for internal innovation and investments. | -| [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. | -| [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. | -| [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. | -| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 | -| [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes | -| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | -| [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. | -| [FactSet](https://www.factset.com/) | [@kuangp](https://github.com/kuangp) | Developer portal to provide discoverability to all internal components, APIs, documentation, and scaffold templates with integrations to our internal infrastructure tools. | -| [Workrise](https://www.workrise.com/) | [Michael Rode](https://github.com/michaelrode) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | -| [RedVentures](https://www.redventures.com/) | [Chris Diaz](https://github.com/codingdiaz) | Developer portal that brings everything an engineer needs to provide value into a single pane of glass. | -| [MavTek](https://www.mavtek.com/) | [@fgascon](https://github.com/fgascon) | Developer portal focused on standardizing practices, centralizing documentation and streamlining developer practices. | -| [QuintoAndar](https://www.quintoandar.com.br/) | [@quintoandar](https://github.com/quintoandar) | Developer portal, services catalog and centralization of service metrics. | -| [empathy.co](https://empathy.co/) | [@guillermotti](https://github.com/guillermotti) | Developer portal for tech docs, service catalog, plugin discovery and much more. | -| [creditas.com](https://creditas.com/) | [@aureliosaraiva](https://github.com/aureliosaraiva) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. | -| [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration | -| [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. | -| [2U](https://2u.com) | [Andrew Thal](https://github.com/athal7) | Development team home-base, promoting service discoverability, resource dependencies, and tech radar | -| [Taxfix](https://taxfix.de/) | [Sami Ur Rehman](https://github.com/samiurrehman92) | Developer's portal with software catalog at it's core. Hosts API Specs, Tech Docs, Tech Radar and some custom plugins. | -| [Busuu](https://busuu.com/) | [Adam Tester](https://github.com/adamtester) | Developer portal with service catalog, API docs, Event docs, service templating, and cost insights. | -| [Loadsmart](https://loadsmart.com/) | [Loadsmart](https://github.com/loadsmart) | Improve services visibility and operations for service owners and developers. | -| [Monzo](https://monzo.com/) | [@WillSewell](https://github.com/WillSewell), [@joechrisellis](https://github.com/joechrisellis) | Developer portal showing metadata and docs for over 2000 microservices. We have built a number of plugins such as a UI for our system to measure [software excellence](https://monzo.com/blog/2021/09/15/how-we-measure-software-excellence), and a UI to show deployment and config change events. | -| [Vaimo](https://www.vaimo.com) | [@vaimo-magnus](https://github.com/vaimo-magnus) | Developer Portal for our developers at Vaimo, currently docs and self-service towards our internal PaaS based on k8s. Plans to extend the catalog into Projects, Environments etc | -| [Wayfair](https://www.wayfair.com) | [@fransan](https://github.com/fransan), [@errskipower](https://github.com/errskipower), [@hrrs](https://github.com/hrrs) | Developer portal for service catalog, technical documentation, and APIs. | -| [CircleHD](https://www.circlehd.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | -| [CastDesk](https://castdesk.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | -| [Santagostino](https://santagostino.it) | [@santagostino](https://github.com/santagostino) | Developer portal, gateway to our infrastructure, documentation, service catalog and internal tooling. | -| [Peak](https://peak.ai) | [Luke Beamish](https://github.com/lukebeamish-peak) | Developer portal for all internal engineers to access documentation and tooling. | -| [Gelato](https://gelato.com/) | [Dmitry Makarenko](https://github.com/dmitry-makarenko-gelato) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal and third-party systems🚀. | -| [GoCardless](https://gocardless.com/) | [James Turley](https://github.com/tragiclifestories) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal systems. | -| [Box](https://www.box.com) | [@kielosz](https://github.com/kielosz), [@jluk-box](https://github.com/jluk-box), [@ptychu](https://github.com/ptychu), [@alexrybch](https://github.com/alexrybch), [@szubster](https://github.com/szubster) | Developer portal for service catalog, integration with internal systems, new service onboarding. | -| [Bazaarvoice](https://www.bazaarvoice.com) | [@niallmccullagh](https://github.com/niallmccullagh) | Developer portal for service catalog and scaffolds, publishing Github docs and API documentation, visualising our internal tech radar and our product engineering org structure. | -| [Krateo PlatformOps](https://www.krateo.io) | [@projectkerberus](https://github.com/projectkerberus) | A multi-cloud control plane to create, manage and deploy any kind of resource easily and centrally via a Developer Portal that centralizes via a self-service catalog the templating and ownership of services, the available documentation, the overview of the components that compose an entire domain and all the data of the service lifecycle. | -| [Adevinta](https://www.adevinta.com) | [Ray Sinnema](https://github.com/RemonSinnema) | Showcase shared services to our internal customers. | -| [Splunk](https://www.splunk.com) | [@tonytamsf](https://github.com/tonytamsf) | Developer portal as a centralized place to find people, services, documentation, escalation policies and give bravos. This portal is also being used as a centralized search engine for engineering specific documentation. | -| [SoundCloud](https://www.soundcloud.com) | [Julio Zynger](https://github.com/julioz) | Developer portal as a [humane registry](https://martinfowler.com/bliki/HumaneRegistry.html) for the organization: catalog of people, services, documentation, feature toggles, escalation policies, etc. | -| [Volvofinans Bank](https://www.volvofinans.se) | [Johan Hammar](https://github.com/johanhammar) | Developer portal enabling engineers to manage and explore software and documentation. | -| [Palo Alto Networks](https://www.paloaltonetworks.com) | [Jeremy Guarini](https://github.com/jeremyguarini), [Brian Lomeland](https://github.com/bbbmmmlll), [Palo Alto Networks](https://github.com/PaloAltoNetworks) | Developer portal, service catalog, documentation and tooling | -| [Signal Iduna Group](https://www.signal-iduna.de/) | [Jonas Thomsen](https://github.com/JoThomsen) | Developer Portal, documentation, monitoring, service catalog for our insurance ecosystem | -| [Tradeshift](https://www.tradeshift.com/) | [Soren Mathiasen](https://github.com/sorenmat) | Developer Portal: documentation, monitoring, service templates, service catalog for our micro services | -| [Unity](https://unity.com) | [Ted Cordery](https://github.com/TeddyBallGame) | A centralized service catalog with documentation for our service engineers. | -| [PicPay](https://www.picpay.com) | [Luis Baroni](https://github.com/lcsbaroni), [Renata Poluceno](https://github.com/renatapoluceno), [PicPay](https://github.com/picpay) | Developer portal for building services throught templates, service catalog with ownership of services, documentation and metrics providing autonomy and visibility for all. | -| [Epic Games](https://www.epicgames.com) | [Brian Jung](https://github.com/brian-at-epic), [Jeff Goldian](https://github.com/jeffgoldian-Epic) | Developer Portal: Service Catalog, Documentation, Software Templates and more making our internal teams' lives easier! | -| [Globo](https://globo.com) | [Carlos Gusmão](https://github.com/caeugusmao), [Guilherme Vierno](https://github.com/vierno), [Denis Aoki](https://github.com/dnsaoki2), [Maycon Dionisio](https://github.com/MayconDionisio), | Reduce the friction of accessing the information engineers need about Globo's digital services through a coherent and centralized experience. | -| [QBE](https://www.qbe.com/) | [Daniel Steel](https://github.com/danielsteelqbe), [Pete Jespers](https://github.com/petejespersqbe) | Developer portal allowing our global teams to explore and create applications, documentation and cloud infrastructure easily and quickly 🚀 | -| [GoTo](https://www.goto.com) | [Lorenzo Orsatti](https://github.com/lorsatti) | Improve onboarding experience of new developers. Discover faster and painlessly developer documentation, API definitions and team information. Provide useful dev metrics in a central place. Provide easy-to-use templates for new services. | -| [Telstra](https://www.telstra.com.au) | [@kiranpatel11](https://github.com/kiranpatel11), [JasonC](https://github.com/JasonC17) | Primary usage: software catalog and templates
Emerging usage : TechDocs, Explore Ecosystem, TechRadar, etc | -| [Mosaico](https://www.mosaico.com.br/) | [Wédney Yuri](https://github.com/wedneyyuri),[@tino.milton](https://github.com/miltonjacomini) | A centralized service catalog of our documentation for our service engineers. | -| [Mox Bank](https://www.mox.com/) | [Nick Laqua](https://github.com/nick-laqua-dragon) | "Single pane of glass" developer portal for providing a best-in-class developer experience to our product teams and making Mox the best tech environment in Hongkong 🥰🚀 | -| [Keyloop](https://www.keyloop.com/) | [Andre Wanlin](https://github.com/awanlin) | Future-motive Developer Portal to help our teams create technology to make everything about buying and owning a car better. 🚗 | -| [Simply Business](https://sbtech.simplybusiness.co.uk/) | [@addersuk](https://github.com/addersuk), [@LightningStairs](https://github.com/LightningStairs), [@punitcse](https://github.com/punitcse), [@moltenice](https://github.com/moltenice) | Central developer portal to access everything a developer needs such as docs, internal service catalog, and the ability to quickly create a new service from a template. Internally developed Backstage plugins allow us to customise the experience to how we work. | -| [Overwolf](https://www.overwolf.com) | [@tomwolfgang](https://github.com/tomwolfgang) | Dev portal - software catalog, tech-docs, scaffolding | -| [Hotmart](https://www.hotmart.com) | [@fabioviana-hotmart](https://github.com/fabioviana-hotmart) | The main Developers Portal to centralize docs, applications and technical metrics. | -| [EF Education First](https://www.ef.com) | [Daan Boerlage](https://github.com/runebaas), [Rafał Nowosielski](https://github.com/rnowosielski) | Our developer portal - primarily used for cataloging and scaffolding with the ambition to expand with more feature adoptions over time | -| [Power Home Remodeling](https://www.techatpower.com) | [Ben Langfeld](https://github.com/benlangfeld) | Developer portal to our internal services, build on open-source software (including Kubernetes) in our own datacenters. Our Portal allows our team members to navigate inherant complexity and standardise. | -| [Livspace](https://www.livspace.com) | [Praveen Kumar](https://github.com/praveen-livspace) | Developer portal, service catalog, tech docs, API docs and plugins | -| [Just Eat Takeaway](https://www.justeattakeaway.com) | [Kim Wilson](https://github.com/kwilson541) | Our developer portal which centralises applications, reduces cognitive load and provides teams insights. | -| [Hopin](https://hopin.com) | [Vladimir Glafirov](https://github.com/vglafirov), [Chloe Lee](https://github.com/msfuko) | Developer portal to streamline the development practices. Integrated with service catalog, software templates, application monitoring, tech docs and plugins. | -| [HBO Max](https://hbomax.com) | [@mdb](https://github.com/mdb), [@nesta219](https://github.com/nesta219), [@nmische](https://github.com/nmische), [@hbomark](https://github.com/hbomark) | Developer portal hosting service catalog and API documentation, as well as cloud infrastructure details, operational visibility tools, and a custom plugin for browsing notable platform change events, such as deployments and configuration updates. | -| [RCHLO](https://www.riachuelo.com.br) & [MIDWAY](https://www.midway.com.br) | [@marcosborges](https://github.com/marcosborges), [@defaultbr](https://github.com/defaultbr) | Self-Service Platform | -| [HP Inc](https://www.hp.com) | [Damon Kaswell](https://github.com/dekoding) | DevEx engagement hub (dev portal: docs, standards, Q&A) and extensive assets catalog (APIs, services, code, data, etc.) for the pan-HP internal developer community. | -| [VMware](https://www.vmware.com) | [@mpriamo](https://github.com/mpriamo), [@krisapplegate](https://github.com/krisapplegate) | Part of [Tanzu Application Platform](https://docs.vmware.com/en/VMware-Tanzu-Application-Platform/index.html) offering; internal developer portal | -| [Ualá](https://www.uala.com.ar/) | [Santiago Bernal](https://github.com/sabernal) | Initial work being done to centralize documentation for all our microservices and APIs, as well as scaffolding new services and tracking code quality | -| [IKEA IT AB](https://www.ingka.com) | [@bjornramberg](https://github.com/bjornramberg), [@supriyachitale](https://github.com/supriyachitale) | Supporting engineers at scale with self serve access and connecting the dots of our engineering platform and services, enabling product teams to move faster and go further, and unleashing innovation, reuse and co-creation across the organisation. | -| [Invitae](https://www.invitae.com/en) | [@ryan-hanchett](https://github.com/ryan-hanchett), [@gmandler42](https://github.com/gmandler42) | Centralized Developer Experience portal, putting all of our tooling behind a single pane of glass and creating a living service catalog. | -| [Fortum](https://www.fortum.com/) | [@brunoamaroalmeida](https://github.com/brunoamaroalmeida), [@dhaval-vithalani](https://github.com/dhaval-vithalani), [@sunilkumarmohanty](https://github.com/sunilkumarmohanty) | A central portal containing information about our applications, services, processes and other software assets to be used by Fortum software engineering community. | -| [Procore](https://www.procore.com/jobs/engineering) | [@shayon](https://github.com/Shayon), [@jamesdabbs-procore](https://github.com/jamesdabbs-procore) | Developer portal - centralized software catalog and templates to enable self serve and reduce cognitive load. | -| [Bradesco](https://banco.bradesco/html/classic/sobre/index.shtm) | [@danilosoarescardoso](https://github.com/danilosoarescardoso) | Centralized developer portal with software catalog, software templates, techdocs and some plugins from community and by our own. | -| [SeatGeek](https://seatgeek.com) | [@zhammer](https://github.com/zhammer) | Software catalog and documentation platform | -| [Grupo Boticário](https://www.grupoboticario.com.br/) | [@chicoribas](https://github.com/chicoribas), [@fndiaz](https://github.com/fndiaz), [@digogid](https://github.com/digogid), [@manumbs](https://github.com/manumbs), [@haooliveira84](https://github.com/haooliveira84), [@Rhullyam](https://github.com/Rhullyam) | Centralized developer portal with catalog, documentation, and software templates to build a ready to go application, including pipelines (CI/CD) and cloud services provisioning | -| [Snyk](https://snyk.io/) | [@robcresswell](https://github.com/robcresswell) | Developer portal for software discovery, API documentation and templating | -| [Stilingue](https://www.stilingue.com.br/) | [@stilingue-inteligencia-artificial](https://github.com/Stilingue-IA), [@bbviana](https://github.com/bbviana) | Developer portal, services catalog and centralization of metrics from Grafana, Sentry and GCP. Furthermore, centralization of documentation and infra details like DNS, Network services, SSL and so on. | -| [TUI Group](https://www.tuigroup.com/) | [Simon Stamm](https://github.com/simonstamm), [Christian Rudolph](https://github.com/ChrisRu82) | Developer portal for all engineer to provide discoverability to all internal components, APIs, documentation and to scaffold templates with integrations to our internal tools extended by plugins from our community. | +| Organization | Contact | Description of Use | +| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | Main interface towards all of Spotify's infrastructure and technical documentation. | +| [bol.com](https://www.bol.com) | [@sagacity](https://github.com/sagacity) | Initial work being done to unify platform tooling. | +| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | +| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | +| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | +| [SDA SE](https://sda.se) | [@dschwank](https://github.com/dschwank), [@iammnils](https://github.com/iammnils) | Central place for developing and sharing services in our insurance ecosystem. | +| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | +| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | +| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. | +| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | +| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D | +| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling | +| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling | +| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks | +| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. | +| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. | +| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. | +| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo), [Erik Lindgren](https://github.com/lindgren) | EG Common Developer Toolkit | +| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go | +| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | +| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. | +| [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | +| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. | +| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. | +| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a team’s engineering dependencies. | +| [Oriflame](https://www.oriflame.com/) | [Oriflame](https://github.com/oriflame) | Internal developer portal for services, single page apps and packages overview, API documentation, technical guides, tech-radar and more. | +| [Booz Allen Hamilton](https://www.boozallen.com/) | [Jason Miller](https://github.com/JasonMiller-BAH) | Developer portal for a full-stack software development ecosystem that accelerates consistent and repeatable Modern Software Development practices for internal innovation and investments. | +| [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. | +| [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. | +| [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. | +| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 | +| [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes | +| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | +| [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. | +| [FactSet](https://www.factset.com/) | [@kuangp](https://github.com/kuangp) | Developer portal to provide discoverability to all internal components, APIs, documentation, and scaffold templates with integrations to our internal infrastructure tools. | +| [Workrise](https://www.workrise.com/) | [Michael Rode](https://github.com/michaelrode) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | +| [RedVentures](https://www.redventures.com/) | [Chris Diaz](https://github.com/codingdiaz) | Developer portal that brings everything an engineer needs to provide value into a single pane of glass. | +| [MavTek](https://www.mavtek.com/) | [@fgascon](https://github.com/fgascon) | Developer portal focused on standardizing practices, centralizing documentation and streamlining developer practices. | +| [QuintoAndar](https://www.quintoandar.com.br/) | [@quintoandar](https://github.com/quintoandar) | Developer portal, services catalog and centralization of service metrics. | +| [empathy.co](https://empathy.co/) | [@guillermotti](https://github.com/guillermotti) | Developer portal for tech docs, service catalog, plugin discovery and much more. | +| [creditas.com](https://creditas.com/) | [@aureliosaraiva](https://github.com/aureliosaraiva) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. | +| [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration | +| [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. | +| [2U](https://2u.com) | [Andrew Thal](https://github.com/athal7) | Development team home-base, promoting service discoverability, resource dependencies, and tech radar | +| [Taxfix](https://taxfix.de/) | [Sami Ur Rehman](https://github.com/samiurrehman92) | Developer's portal with software catalog at it's core. Hosts API Specs, Tech Docs, Tech Radar and some custom plugins. | +| [Busuu](https://busuu.com/) | [Adam Tester](https://github.com/adamtester) | Developer portal with service catalog, API docs, Event docs, service templating, and cost insights. | +| [Loadsmart](https://loadsmart.com/) | [Loadsmart](https://github.com/loadsmart) | Improve services visibility and operations for service owners and developers. | +| [Monzo](https://monzo.com/) | [@WillSewell](https://github.com/WillSewell), [@joechrisellis](https://github.com/joechrisellis) | Developer portal showing metadata and docs for over 2000 microservices. We have built a number of plugins such as a UI for our system to measure [software excellence](https://monzo.com/blog/2021/09/15/how-we-measure-software-excellence), and a UI to show deployment and config change events. | +| [Vaimo](https://www.vaimo.com) | [@vaimo-magnus](https://github.com/vaimo-magnus) | Developer Portal for our developers at Vaimo, currently docs and self-service towards our internal PaaS based on k8s. Plans to extend the catalog into Projects, Environments etc | +| [Wayfair](https://www.wayfair.com) | [@fransan](https://github.com/fransan), [@errskipower](https://github.com/errskipower), [@hrrs](https://github.com/hrrs) | Developer portal for service catalog, technical documentation, and APIs. | +| [CircleHD](https://www.circlehd.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | +| [CastDesk](https://castdesk.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | +| [Santagostino](https://santagostino.it) | [@santagostino](https://github.com/santagostino) | Developer portal, gateway to our infrastructure, documentation, service catalog and internal tooling. | +| [Peak](https://peak.ai) | [Luke Beamish](https://github.com/lukebeamish-peak) | Developer portal for all internal engineers to access documentation and tooling. | +| [Gelato](https://gelato.com/) | [Dmitry Makarenko](https://github.com/dmitry-makarenko-gelato) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal and third-party systems🚀. | +| [GoCardless](https://gocardless.com/) | [James Turley](https://github.com/tragiclifestories) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal systems. | +| [Box](https://www.box.com) | [@kielosz](https://github.com/kielosz), [@jluk-box](https://github.com/jluk-box), [@ptychu](https://github.com/ptychu), [@alexrybch](https://github.com/alexrybch), [@szubster](https://github.com/szubster) | Developer portal for service catalog, integration with internal systems, new service onboarding. | +| [Bazaarvoice](https://www.bazaarvoice.com) | [@niallmccullagh](https://github.com/niallmccullagh) | Developer portal for service catalog and scaffolds, publishing Github docs and API documentation, visualising our internal tech radar and our product engineering org structure. | +| [Krateo PlatformOps](https://www.krateo.io) | [@projectkerberus](https://github.com/projectkerberus) | A multi-cloud control plane to create, manage and deploy any kind of resource easily and centrally via a Developer Portal that centralizes via a self-service catalog the templating and ownership of services, the available documentation, the overview of the components that compose an entire domain and all the data of the service lifecycle. | +| [Adevinta](https://www.adevinta.com) | [Ray Sinnema](https://github.com/RemonSinnema) | Showcase shared services to our internal customers. | +| [Splunk](https://www.splunk.com) | [@tonytamsf](https://github.com/tonytamsf) | Developer portal as a centralized place to find people, services, documentation, escalation policies and give bravos. This portal is also being used as a centralized search engine for engineering specific documentation. | +| [SoundCloud](https://www.soundcloud.com) | [Julio Zynger](https://github.com/julioz) | Developer portal as a [humane registry](https://martinfowler.com/bliki/HumaneRegistry.html) for the organization: catalog of people, services, documentation, feature toggles, escalation policies, etc. | +| [Volvofinans Bank](https://www.volvofinans.se) | [Johan Hammar](https://github.com/johanhammar) | Developer portal enabling engineers to manage and explore software and documentation. | +| [Palo Alto Networks](https://www.paloaltonetworks.com) | [Jeremy Guarini](https://github.com/jeremyguarini), [Brian Lomeland](https://github.com/bbbmmmlll), [Palo Alto Networks](https://github.com/PaloAltoNetworks) | Developer portal, service catalog, documentation and tooling | +| [Signal Iduna Group](https://www.signal-iduna.de/) | [Jonas Thomsen](https://github.com/JoThomsen) | Developer Portal, documentation, monitoring, service catalog for our insurance ecosystem | +| [Tradeshift](https://www.tradeshift.com/) | [Soren Mathiasen](https://github.com/sorenmat) | Developer Portal: documentation, monitoring, service templates, service catalog for our micro services | +| [Unity](https://unity.com) | [Ted Cordery](https://github.com/TeddyBallGame) | A centralized service catalog with documentation for our service engineers. | +| [PicPay](https://www.picpay.com) | [Luis Baroni](https://github.com/lcsbaroni), [Renata Poluceno](https://github.com/renatapoluceno), [PicPay](https://github.com/picpay) | Developer portal for building services throught templates, service catalog with ownership of services, documentation and metrics providing autonomy and visibility for all. | +| [Epic Games](https://www.epicgames.com) | [Brian Jung](https://github.com/brian-at-epic), [Jeff Goldian](https://github.com/jeffgoldian-Epic) | Developer Portal: Service Catalog, Documentation, Software Templates and more making our internal teams' lives easier! | +| [Globo](https://globo.com) | [Carlos Gusmão](https://github.com/caeugusmao), [Guilherme Vierno](https://github.com/vierno), [Denis Aoki](https://github.com/dnsaoki2), [Maycon Dionisio](https://github.com/MayconDionisio), | Reduce the friction of accessing the information engineers need about Globo's digital services through a coherent and centralized experience. | +| [QBE](https://www.qbe.com/) | [Daniel Steel](https://github.com/danielsteelqbe), [Pete Jespers](https://github.com/petejespersqbe) | Developer portal allowing our global teams to explore and create applications, documentation and cloud infrastructure easily and quickly 🚀 | +| [GoTo](https://www.goto.com) | [Lorenzo Orsatti](https://github.com/lorsatti) | Improve onboarding experience of new developers. Discover faster and painlessly developer documentation, API definitions and team information. Provide useful dev metrics in a central place. Provide easy-to-use templates for new services. | +| [Telstra](https://www.telstra.com.au) | [@kiranpatel11](https://github.com/kiranpatel11), [JasonC](https://github.com/JasonC17) | Primary usage: software catalog and templates
Emerging usage : TechDocs, Explore Ecosystem, TechRadar, etc | +| [Mosaico](https://www.mosaico.com.br/) | [Wédney Yuri](https://github.com/wedneyyuri),[@tino.milton](https://github.com/miltonjacomini) | A centralized service catalog of our documentation for our service engineers. | +| [Mox Bank](https://www.mox.com/) | [Nick Laqua](https://github.com/nick-laqua-dragon) | "Single pane of glass" developer portal for providing a best-in-class developer experience to our product teams and making Mox the best tech environment in Hongkong 🥰🚀 | +| [Keyloop](https://www.keyloop.com/) | [Andre Wanlin](https://github.com/awanlin) | Future-motive Developer Portal to help our teams create technology to make everything about buying and owning a car better. 🚗 | +| [Simply Business](https://sbtech.simplybusiness.co.uk/) | [@addersuk](https://github.com/addersuk), [@LightningStairs](https://github.com/LightningStairs), [@punitcse](https://github.com/punitcse), [@moltenice](https://github.com/moltenice) | Central developer portal to access everything a developer needs such as docs, internal service catalog, and the ability to quickly create a new service from a template. Internally developed Backstage plugins allow us to customise the experience to how we work. | +| [Overwolf](https://www.overwolf.com) | [@tomwolfgang](https://github.com/tomwolfgang) | Dev portal - software catalog, tech-docs, scaffolding | +| [Hotmart](https://www.hotmart.com) | [@fabioviana-hotmart](https://github.com/fabioviana-hotmart) | The main Developers Portal to centralize docs, applications and technical metrics. | +| [EF Education First](https://www.ef.com) | [Daan Boerlage](https://github.com/runebaas), [Rafał Nowosielski](https://github.com/rnowosielski) | Our developer portal - primarily used for cataloging and scaffolding with the ambition to expand with more feature adoptions over time | +| [Power Home Remodeling](https://www.techatpower.com) | [Ben Langfeld](https://github.com/benlangfeld) | Developer portal to our internal services, build on open-source software (including Kubernetes) in our own datacenters. Our Portal allows our team members to navigate inherant complexity and standardise. | +| [Livspace](https://www.livspace.com) | [Praveen Kumar](https://github.com/praveen-livspace) | Developer portal, service catalog, tech docs, API docs and plugins | +| [Just Eat Takeaway](https://www.justeattakeaway.com) | [Kim Wilson](https://github.com/kwilson541) | Our developer portal which centralises applications, reduces cognitive load and provides teams insights. | +| [Hopin](https://hopin.com) | [Vladimir Glafirov](https://github.com/vglafirov), [Chloe Lee](https://github.com/msfuko) | Developer portal to streamline the development practices. Integrated with service catalog, software templates, application monitoring, tech docs and plugins. | +| [HBO Max](https://hbomax.com) | [@mdb](https://github.com/mdb), [@nesta219](https://github.com/nesta219), [@nmische](https://github.com/nmische), [@hbomark](https://github.com/hbomark) | Developer portal hosting service catalog and API documentation, as well as cloud infrastructure details, operational visibility tools, and a custom plugin for browsing notable platform change events, such as deployments and configuration updates. | +| [RCHLO](https://www.riachuelo.com.br) & [MIDWAY](https://www.midway.com.br) | [@marcosborges](https://github.com/marcosborges), [@defaultbr](https://github.com/defaultbr) | Self-Service Platform | +| [HP Inc](https://www.hp.com) | [Damon Kaswell](https://github.com/dekoding) | DevEx engagement hub (dev portal: docs, standards, Q&A) and extensive assets catalog (APIs, services, code, data, etc.) for the pan-HP internal developer community. | +| [VMware](https://www.vmware.com) | [@mpriamo](https://github.com/mpriamo), [@krisapplegate](https://github.com/krisapplegate) | Part of [Tanzu Application Platform](https://docs.vmware.com/en/VMware-Tanzu-Application-Platform/index.html) offering; internal developer portal | +| [Ualá](https://www.uala.com.ar/) | [Santiago Bernal](https://github.com/sabernal) | Initial work being done to centralize documentation for all our microservices and APIs, as well as scaffolding new services and tracking code quality | +| [IKEA IT AB](https://www.ingka.com) | [@bjornramberg](https://github.com/bjornramberg), [@supriyachitale](https://github.com/supriyachitale) | Supporting engineers at scale with self serve access and connecting the dots of our engineering platform and services, enabling product teams to move faster and go further, and unleashing innovation, reuse and co-creation across the organisation. | +| [Invitae](https://www.invitae.com/en) | [@ryan-hanchett](https://github.com/ryan-hanchett), [@gmandler42](https://github.com/gmandler42) | Centralized Developer Experience portal, putting all of our tooling behind a single pane of glass and creating a living service catalog. | +| [Fortum](https://www.fortum.com/) | [@brunoamaroalmeida](https://github.com/brunoamaroalmeida), [@dhaval-vithalani](https://github.com/dhaval-vithalani), [@sunilkumarmohanty](https://github.com/sunilkumarmohanty) | A central portal containing information about our applications, services, processes and other software assets to be used by Fortum software engineering community. | +| [Procore](https://www.procore.com/jobs/engineering) | [@shayon](https://github.com/Shayon), [@jamesdabbs-procore](https://github.com/jamesdabbs-procore) | Developer portal - centralized software catalog and templates to enable self serve and reduce cognitive load. | +| [Bradesco](https://banco.bradesco/html/classic/sobre/index.shtm) | [@danilosoarescardoso](https://github.com/danilosoarescardoso) | Centralized developer portal with software catalog, software templates, techdocs and some plugins from community and by our own. | +| [SeatGeek](https://seatgeek.com) | [@zhammer](https://github.com/zhammer) | Software catalog and documentation platform | +| [Grupo Boticário](https://www.grupoboticario.com.br/) | [@chicoribas](https://github.com/chicoribas), [@fndiaz](https://github.com/fndiaz), [@digogid](https://github.com/digogid), [@manumbs](https://github.com/manumbs), [@haooliveira84](https://github.com/haooliveira84), [@Rhullyam](https://github.com/Rhullyam) | Centralized developer portal with catalog, documentation, and software templates to build a ready to go application, including pipelines (CI/CD) and cloud services provisioning | +| [Snyk](https://snyk.io/) | [@robcresswell](https://github.com/robcresswell) | Developer portal for software discovery, API documentation and templating | +| [Stilingue](https://www.stilingue.com.br/) | [@stilingue-inteligencia-artificial](https://github.com/Stilingue-IA), [@bbviana](https://github.com/bbviana) | Developer portal, services catalog and centralization of metrics from Grafana, Sentry and GCP. Furthermore, centralization of documentation and infra details like DNS, Network services, SSL and so on. | +| [TUI Group](https://www.tuigroup.com/) | [Simon Stamm](https://github.com/simonstamm), [Christian Rudolph](https://github.com/ChrisRu82) | Developer portal for all engineer to provide discoverability to all internal components, APIs, documentation and to scaffold templates with integrations to our internal tools extended by plugins from our community. | From 3d31da90c98d5cedb7507da624b894f48d6cf225 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 4 Mar 2022 15:40:35 +0100 Subject: [PATCH 209/353] Upgrade helper blog post (#9953) * Upgrade helper blog post Signed-off-by: Vincenzo Scamporlino * Whitelist ecco Signed-off-by: Vincenzo Scamporlino --- .github/styles/vocab.txt | 1 + .../2022-03-04-backstage-upgrade-helper.md | 56 ++++++++++++++++++ .../backstage-upgrade-helper-post-header.png | Bin 0 -> 86768 bytes .../22-03-04/backstage-upgrade-helper.gif | Bin 0 -> 618633 bytes 4 files changed, 57 insertions(+) create mode 100644 microsite/blog/2022-03-04-backstage-upgrade-helper.md create mode 100644 microsite/blog/assets/22-03-04/backstage-upgrade-helper-post-header.png create mode 100644 microsite/blog/assets/22-03-04/backstage-upgrade-helper.gif diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index c7971f6c52..6ed539530d 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -84,6 +84,7 @@ dockerfiles Dockerize dockerode Docusaurus +ecco env Env elasticsearch diff --git a/microsite/blog/2022-03-04-backstage-upgrade-helper.md b/microsite/blog/2022-03-04-backstage-upgrade-helper.md new file mode 100644 index 0000000000..a189e28cd5 --- /dev/null +++ b/microsite/blog/2022-03-04-backstage-upgrade-helper.md @@ -0,0 +1,56 @@ +--- +title: Avoid upgrade surprises with Backstage Upgrade Helper +author: Vincenzo Scamporlino, Spotify +authorURL: https://github.com/vinzscam +authorImageURL: https://avatars.githubusercontent.com/u/8433119?v=4 +--- + +![backstage header](assets/22-03-04/backstage-upgrade-helper-post-header.png) + +_TLDR;_ The Spotify team recently announced [Backstage Upgrade Helper](https://backstage.github.io/upgrade-helper): a tool that helps adopters keep their Backstage application up-to-date. **Spoiler alert**: this is also a hidden and heartwarming tale about the beauty of open source innovation. + + + +## Upgrading Backstage was a common pain point + +Without a doubt, one of the most exciting things about Backstage is our community. Step into our daily [Discord conversations](https://discord.gg/sBEF5VkG) and the monthly [Community Sessions](https://www.youtube.com/watch?v=0QMQYSTKAx0) and you’ll find a ton of great ideas, problem-solving, and support for one another. + +During one of these discussions, we realized we all shared the same pain point — upgrading a Backstage application. + +The Backstage open source project is in hyper-growth mode, where dozens of pull requests are merged each day. This means adopters need to go through the upgrade process more often in order to get the latest and greatest features. The Spotify team heard a lot of feedback about how painful and manual this process was — so we decided to tackle this problem during one of our internal Hack Days. + +At Spotify, we run Hack Days every month and a lot of ideas come from those sessions (in fact, the idea to open source Backstage itself [was born on a Hack Day](https://open.spotify.com/episode/332yTwGiILGKTS7dsHCj2P)!). During a brainstorming session, we noticed that another open source community faced a similar upgrading problem years back and came up with a really good solution. + +If you have ever worked on a React Native project, you might immediately recognize what we are referring to: The [React Native Upgrade Helper](https://github.com/react-native-community/upgrade-helper) — a web tool to support React Native developers in upgrading their apps — might have saved hours of your time. It certainly saved us hours of ours. So we looked into the project details and discovered it could fulfill our Backstage needs. + +We started by creating a fork from React Native’s open source project, applying a few changes on top of it to introduce Backstage support, and — _ecco_ — we had a new Backstage Upgrade Helper! In just a few days with pretty minimal effort, we created a product that would save a lot of time for the entire Backstage community. And we had fun at the same time (happy developers make happy code 😁 ). + +## So what is Backstage Upgrade Helper? + +The Backstage Upgrade Helper tool enables adopters to easily upgrade their Backstage app by using the power of git to create a diff between different versions. + +Whenever a new version of Backstage is released, the Helper scaffolds a new dummy Backstage app using the `backstage-create-app` cli utility and checks all the generated files in a specific git branch. After the branch is generated, it gets compared with all the existing ones, which results in generating specific git patches stored in specific files. By selecting the version of the current Backstage release together with the version you want to upgrade to, the UI knows which patch file needs to be picked up. + +So, now you can update your Backstage application in three steps rather than manually reading the changelogs of all the Backstage modules in reverse order. + +### To use the Upgrade Helper, follow these simple steps + +1. Go to the [Backstage Upgrade Helper](https://backstage.github.io/upgrade-helper) and enter your current release and the release you would like to upgrade to. + +2. Press **Show me how to upgrade!** After that, the feature spits out the changes between the two Backstage versions in a split-screen view for easy comparison. + + ![Backstage Upgrade Helper in action](assets/22-03-04/backstage-upgrade-helper.gif) + +3. Apply the suggested changes to your source code to correctly upgrade your app to the selected Backstage version. This will let you directly migrate from the version you’re currently using to the target version and skip all the intermediate steps you would have otherwise gone through manually reading all the changelogs. + +[Watch the Upgrade Helper demo](https://youtu.be/nYjI2j-lWEM?t=410). + +## Open source inspires + +Thus far, the Backstage Upgrade Helper has gotten a lot of good feedback from the community and we’re sure to see awesome contributions in the future. But all the credit behind this idea goes to the React Native community. + +With React Native’s open source contributions, we were able to quickly solve a tough problem for the entire Backstage community. Not only did React Native’s project save us time creating a new product, but it has also saved our adopters time upgrading Backstage. + +This is why we love working in open source. The hard work done for one community has the power to influence and inspire another community. We hope Backstage can do the same and pay it forward to other open source projects. + +For more Backstage Upgrade Helper resources, check out the [UI code](https://github.com/backstage/upgrade-helper) and the [git patches](https://github.com/backstage/upgrade-helper-diff). diff --git a/microsite/blog/assets/22-03-04/backstage-upgrade-helper-post-header.png b/microsite/blog/assets/22-03-04/backstage-upgrade-helper-post-header.png new file mode 100644 index 0000000000000000000000000000000000000000..c4bc4fdd25b4a30b4bcf4032175fa8ed4399bc9a GIT binary patch literal 86768 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Ⓢ@u10Q&)Q#I(v20|As%G6svI<_h>^WC) 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�uJs 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 Date: Fri, 4 Mar 2022 15:40:33 +0100 Subject: [PATCH 210/353] remove octokit stuff from the scaffolder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/red-kiwis-divide.md | 5 + plugins/scaffolder-backend/api-report.md | 20 ---- .../builtin/github/OctokitProvider.test.ts | 88 -------------- .../actions/builtin/github/OctokitProvider.ts | 111 ------------------ .../actions/builtin/github/index.ts | 1 - 5 files changed, 5 insertions(+), 220 deletions(-) create mode 100644 .changeset/red-kiwis-divide.md delete mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts diff --git a/.changeset/red-kiwis-divide.md b/.changeset/red-kiwis-divide.md new file mode 100644 index 0000000000..0bfb247093 --- /dev/null +++ b/.changeset/red-kiwis-divide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +**BREAKING**: Removed the previously deprecated `OctokitProvider` class. diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index b18e199f40..80a106bf27 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -21,7 +21,6 @@ import { Knex } from 'knex'; import { LocationSpec } from '@backstage/plugin-catalog-backend'; import { Logger as Logger_2 } from 'winston'; import { Observable } from '@backstage/types'; -import { Octokit } from 'octokit'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { Schema } from 'jsonschema'; import { ScmIntegrationRegistry } from '@backstage/integration'; @@ -388,25 +387,6 @@ export function fetchContents({ outputPath: string; }): Promise; -// Warning: (ae-missing-release-tag) "OctokitProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public @deprecated -export class OctokitProvider { - constructor( - integrations: ScmIntegrationRegistry, - githubCredentialsProvider?: GithubCredentialsProvider, - ); - // Warning: (ae-forgotten-export) The symbol "OctokitIntegration" needs to be exported by the entry point index.d.ts - // - // @deprecated - getOctokit( - repoUrl: string, - options?: { - token?: string; - }, - ): Promise; -} - // @public (undocumented) export interface OctokitWithPullRequestPluginClient { // (undocumented) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.test.ts deleted file mode 100644 index e2449917ab..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * 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 { OctokitProvider } from './OctokitProvider'; -import { - ScmIntegrations, - DefaultGithubCredentialsProvider, -} from '@backstage/integration'; -import { ConfigReader } from '@backstage/config'; - -describe('getOctokit', () => { - const config = new ConfigReader({ - integrations: { - github: [ - { host: 'github.com', token: 'tokenlols' }, - { host: 'ghe.github.com' }, - ], - }, - }); - - const integrations = ScmIntegrations.fromConfig(config); - const githubCredentialsProvider = - DefaultGithubCredentialsProvider.fromIntegrations(integrations); - const octokitProvider = new OctokitProvider( - integrations, - githubCredentialsProvider, - ); - - beforeEach(() => { - jest.resetAllMocks(); - }); - - it('should throw an error when the repoUrl is not well formed', async () => { - await expect( - octokitProvider.getOctokit('github.com?repo=bob'), - ).rejects.toThrow(/missing owner/); - - await expect( - octokitProvider.getOctokit('github.com?owner=owner'), - ).rejects.toThrow(/missing repo/); - }); - - it('should throw if there is no integration config provided', async () => { - await expect( - octokitProvider.getOctokit('missing.com?repo=bob&owner=owner'), - ).rejects.toThrow(/No matching integration configuration/); - }); - - it('should throw if there is no token in the integration config that is returned', async () => { - await expect( - octokitProvider.getOctokit('ghe.github.com?repo=bob&owner=owner'), - ).rejects.toThrow(/No token available for host/); - }); - - it('should return proper Octokit', async () => { - const { client, token, owner, repo } = await octokitProvider.getOctokit( - 'github.com?repo=bob&owner=owner', - ); - expect(client).toBeDefined(); - expect(token).toBe('tokenlols'); - expect(owner).toBe('owner'); - expect(repo).toBe('bob'); - }); - - it('should return an octokit client with the passed in token if it is provided', async () => { - const { client, token, owner, repo } = await octokitProvider.getOctokit( - 'github.com?repo=bob&owner=owner', - { token: 'tokenlols2' }, - ); - expect(client).toBeDefined(); - expect(token).toBe('tokenlols2'); - expect(owner).toBe('owner'); - expect(repo).toBe('bob'); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts deleted file mode 100644 index 2f3c43a6b8..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * 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 { InputError } from '@backstage/errors'; -import { - DefaultGithubCredentialsProvider, - GithubCredentialsProvider, - ScmIntegrationRegistry, -} from '@backstage/integration'; -import { Octokit } from 'octokit'; -import { parseRepoUrl } from '../publish/util'; - -export type OctokitIntegration = { - client: Octokit; - token: string; - owner: string; - repo: string; -}; -/** - * OctokitProvider provides Octokit client based on ScmIntegrationsRegistry configuration. - * OctokitProvider supports GitHub credentials caching out of the box. - * - * @deprecated we are no longer providing a way from the scaffolder to generate octokit instances. - * Implement your own if you're using this method from an external package, or use the internal `getOctokitOptions` function instead - */ -export class OctokitProvider { - private readonly integrations: ScmIntegrationRegistry; - private readonly githubCredentialsProvider: GithubCredentialsProvider; - - constructor( - integrations: ScmIntegrationRegistry, - githubCredentialsProvider?: GithubCredentialsProvider, - ) { - this.integrations = integrations; - this.githubCredentialsProvider = - githubCredentialsProvider || - DefaultGithubCredentialsProvider.fromIntegrations(this.integrations); - } - - /** - * gets standard Octokit client based on repository URL. - * - * @param repoUrl - Repository URL - * - * @deprecated we are no longer providing a way from the scaffolder to generate octokit instances. - * Implement your own if you're using this method from an external package, or use the internal `getOctokitOptions` function instead - */ - async getOctokit( - repoUrl: string, - options?: { token?: string }, - ): Promise { - const { owner, repo, host } = parseRepoUrl(repoUrl, this.integrations); - - if (!owner) { - throw new InputError(`No owner provided for repo ${repoUrl}`); - } - - const integrationConfig = this.integrations.github.byHost(host)?.config; - - if (!integrationConfig) { - throw new InputError(`No integration for host ${host}`); - } - - // Short circuit the internal Github Token provider the token provided - // by the action or the caller. - if (options?.token) { - const client = new Octokit({ - auth: options.token, - baseUrl: integrationConfig.apiBaseUrl, - previews: ['nebula-preview'], - }); - - return { client, token: options.token, owner, repo }; - } - - // TODO(blam): Consider changing this API to have owner, repoo interface instead of URL as the it's - // needless to create URL and then parse again the other side. - const { token } = await this.githubCredentialsProvider.getCredentials({ - url: `https://${host}/${encodeURIComponent(owner)}/${encodeURIComponent( - repo, - )}`, - }); - - if (!token) { - throw new InputError( - `No token available for host: ${host}, with owner ${owner}, and repo ${repo}`, - ); - } - - const client = new Octokit({ - auth: token, - baseUrl: integrationConfig.apiBaseUrl, - previews: ['nebula-preview'], - }); - - return { client, token, owner, repo }; - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts index 865c433c16..07614e8a03 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts @@ -16,4 +16,3 @@ export { createGithubActionsDispatchAction } from './githubActionsDispatch'; export { createGithubWebhookAction } from './githubWebhook'; -export { OctokitProvider } from './OctokitProvider'; From f3a7a9de6de729733a10e34b157aab0523567c00 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 4 Mar 2022 15:51:50 +0100 Subject: [PATCH 211/353] catalog-react: Remove useOwnedEntities hook Signed-off-by: Johan Haals --- .changeset/spotty-seals-press.md | 7 ++ .changeset/young-feet-flow.md | 5 ++ plugins/catalog-react/api-report.md | 16 ----- plugins/catalog-react/src/hooks/index.ts | 2 - .../src/hooks/useEntityTypeFilter.tsx | 11 --- .../src/hooks/useOwnedEntities.ts | 69 ------------------- .../OwnedEntityPicker/OwnedEntityPicker.tsx | 50 +++++++++++++- 7 files changed, 60 insertions(+), 100 deletions(-) create mode 100644 .changeset/spotty-seals-press.md create mode 100644 .changeset/young-feet-flow.md delete mode 100644 plugins/catalog-react/src/hooks/useOwnedEntities.ts diff --git a/.changeset/spotty-seals-press.md b/.changeset/spotty-seals-press.md new file mode 100644 index 0000000000..a0a69b2efc --- /dev/null +++ b/.changeset/spotty-seals-press.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +**BREAKING**: Removed `useOwnedEntities` and moved its usage internally to the scaffolder-backend where it's used. + +**BREAKING**: Removed `EntityTypeReturn` type which is now inlined. diff --git a/.changeset/young-feet-flow.md b/.changeset/young-feet-flow.md new file mode 100644 index 0000000000..91e55da1d8 --- /dev/null +++ b/.changeset/young-feet-flow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Internalized usage of `useOwnedEntities` hook. diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 40764b8844..0ace24142b 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -13,7 +13,6 @@ import { ComponentEntity } from '@backstage/catalog-model'; import { ComponentProps } from 'react'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; -import { GetEntitiesResponse } from '@backstage/catalog-client'; import { IconButton } from '@material-ui/core'; import { LinkProps } from '@backstage/core-components'; import { Observable } from '@backstage/types'; @@ -388,15 +387,6 @@ export interface EntityTypePickerProps { initialFilter?: string; } -// @public @deprecated (undocumented) -export type EntityTypeReturn = { - loading: boolean; - error?: Error; - availableTypes: string[]; - selectedTypes: string[]; - setSelectedTypes: (types: string[]) => void; -}; - // @public export const FavoriteEntity: (props: FavoriteEntityProps) => JSX.Element; @@ -549,12 +539,6 @@ export function useEntityTypeFilter(): { setSelectedTypes: (types: string[]) => void; }; -// @public @deprecated -export function useOwnedEntities(allowedKinds?: string[]): { - loading: boolean; - ownedEntities: GetEntitiesResponse | undefined; -}; - // @public @deprecated export function useOwnUser(): AsyncState; diff --git a/plugins/catalog-react/src/hooks/index.ts b/plugins/catalog-react/src/hooks/index.ts index e26f01928d..9e2830a89c 100644 --- a/plugins/catalog-react/src/hooks/index.ts +++ b/plugins/catalog-react/src/hooks/index.ts @@ -35,12 +35,10 @@ export type { EntityListContextProps, } from './useEntityListProvider'; export { useEntityTypeFilter } from './useEntityTypeFilter'; -export type { EntityTypeReturn } from './useEntityTypeFilter'; export { useEntityKinds } from './useEntityKinds'; export { useOwnUser } from './useOwnUser'; export { useRelatedEntities } from './useRelatedEntities'; export { useStarredEntities } from './useStarredEntities'; export { useStarredEntity } from './useStarredEntity'; export { loadCatalogOwnerRefs, useEntityOwnership } from './useEntityOwnership'; -export { useOwnedEntities } from './useOwnedEntities'; export { useEntityPermission } from './useEntityPermission'; diff --git a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx index 2159c30edc..9971536138 100644 --- a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx +++ b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx @@ -23,17 +23,6 @@ import { catalogApiRef } from '../api'; import { useEntityList } from './useEntityListProvider'; import { EntityTypeFilter } from '../filters'; -/** @public - * @deprecated type inlined with {@link useEntityTypeFilter}. - */ -export type EntityTypeReturn = { - loading: boolean; - error?: Error; - availableTypes: string[]; - selectedTypes: string[]; - setSelectedTypes: (types: string[]) => void; -}; - /** * A hook built on top of `useEntityList` for enabling selection of valid `spec.type` values * based on the selected EntityKindFilter. diff --git a/plugins/catalog-react/src/hooks/useOwnedEntities.ts b/plugins/catalog-react/src/hooks/useOwnedEntities.ts deleted file mode 100644 index a2258cf138..0000000000 --- a/plugins/catalog-react/src/hooks/useOwnedEntities.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * 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 { catalogApiRef } from './../api'; -import { loadCatalogOwnerRefs } from './useEntityOwnership'; -import { identityApiRef, useApi } from '@backstage/core-plugin-api'; -import { RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { GetEntitiesResponse } from '@backstage/catalog-client'; -import useAsync from 'react-use/lib/useAsync'; -import { useMemo } from 'react'; - -/** - * Takes the relevant parts of the Backstage identity, and translates them into - * a list of entities which are owned by the user. Takes an optional parameter - * to filter the entities based on allowedKinds - * - * @public - * - * @param allowedKinds - Array of allowed kinds to filter the entities - * @deprecated Use `ownershipEntityRefs` from `identityApi.getBackstageIdentity()` instead. - */ -export function useOwnedEntities(allowedKinds?: string[]): { - loading: boolean; - ownedEntities: GetEntitiesResponse | undefined; -} { - const identityApi = useApi(identityApiRef); - const catalogApi = useApi(catalogApiRef); - - const { loading, value: refs } = useAsync(async () => { - const identity = await identityApi.getBackstageIdentity(); - const identityRefs = identity.ownershipEntityRefs; - const catalogRefs = await loadCatalogOwnerRefs(catalogApi, identityRefs); - const catalogs = await catalogApi.getEntities( - allowedKinds - ? { - filter: { - kind: allowedKinds, - [`relations.${RELATION_OWNED_BY}`]: - [...identityRefs, ...catalogRefs] || [], - }, - } - : { - filter: { - [`relations.${RELATION_OWNED_BY}`]: - [...identityRefs, ...catalogRefs] || [], - }, - }, - ); - return catalogs; - }, []); - - const ownedEntities = useMemo(() => { - return refs; - }, [refs]); - - return useMemo(() => ({ loading, ownedEntities }), [loading, ownedEntities]); -} diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx index aded9acffa..fb4a7a7234 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx @@ -13,14 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { GetEntitiesResponse } from '@backstage/catalog-client'; +import { RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { identityApiRef, useApi } from '@backstage/core-plugin-api'; import { + catalogApiRef, humanizeEntityRef, - useOwnedEntities, } from '@backstage/plugin-catalog-react'; import { TextField } from '@material-ui/core'; import FormControl from '@material-ui/core/FormControl'; import Autocomplete from '@material-ui/lab/Autocomplete'; -import React from 'react'; +import React, { useMemo } from 'react'; +import useAsync from 'react-use/lib/useAsync'; import { FieldExtensionComponentProps } from '../../../extensions'; @@ -86,3 +90,45 @@ export const OwnedEntityPicker = ( ); }; + +/** + * Takes the relevant parts of the Backstage identity, and translates them into + * a list of entities which are owned by the user. Takes an optional parameter + * to filter the entities based on allowedKinds + * + * + * @param allowedKinds - Array of allowed kinds to filter the entities + */ +function useOwnedEntities(allowedKinds?: string[]): { + loading: boolean; + ownedEntities: GetEntitiesResponse | undefined; +} { + const identityApi = useApi(identityApiRef); + const catalogApi = useApi(catalogApiRef); + + const { loading, value: refs } = useAsync(async () => { + const identity = await identityApi.getBackstageIdentity(); + const identityRefs = identity.ownershipEntityRefs; + const catalogs = await catalogApi.getEntities( + allowedKinds + ? { + filter: { + kind: allowedKinds, + [`relations.${RELATION_OWNED_BY}`]: identityRefs || [], + }, + } + : { + filter: { + [`relations.${RELATION_OWNED_BY}`]: identityRefs || [], + }, + }, + ); + return catalogs; + }, []); + + const ownedEntities = useMemo(() => { + return refs; + }, [refs]); + + return useMemo(() => ({ loading, ownedEntities }), [loading, ownedEntities]); +} From cf1ff5b438120a497f82ac759eba0bb1dfb089d6 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 4 Mar 2022 14:01:23 +0100 Subject: [PATCH 212/353] catalog-react: Remove useEntityKinds Signed-off-by: Johan Haals --- .changeset/tasty-carpets-fold.md | 5 ++ plugins/catalog-react/api-report.md | 7 --- plugins/catalog-react/src/hooks/index.ts | 1 - .../src/hooks/useEntityKinds.test.tsx | 53 ------------------- .../catalog-react/src/hooks/useEntityKinds.ts | 39 -------------- 5 files changed, 5 insertions(+), 100 deletions(-) create mode 100644 .changeset/tasty-carpets-fold.md delete mode 100644 plugins/catalog-react/src/hooks/useEntityKinds.test.tsx delete mode 100644 plugins/catalog-react/src/hooks/useEntityKinds.ts diff --git a/.changeset/tasty-carpets-fold.md b/.changeset/tasty-carpets-fold.md new file mode 100644 index 0000000000..38f7099f9e --- /dev/null +++ b/.changeset/tasty-carpets-fold.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +**BREAKING**: Removed `useEntityKinds` hook. diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index f0d267d397..1fdaeb8524 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -512,13 +512,6 @@ export function useEntity(): { refresh?: VoidFunction; }; -// @public @deprecated -export function useEntityKinds(): { - error: Error | undefined; - loading: boolean; - kinds: string[] | undefined; -}; - // @public export function useEntityList< EntityFilters extends DefaultEntityFilters = DefaultEntityFilters, diff --git a/plugins/catalog-react/src/hooks/index.ts b/plugins/catalog-react/src/hooks/index.ts index 879ead8809..8c11d9fb9d 100644 --- a/plugins/catalog-react/src/hooks/index.ts +++ b/plugins/catalog-react/src/hooks/index.ts @@ -35,7 +35,6 @@ export type { } from './useEntityListProvider'; export { useEntityTypeFilter } from './useEntityTypeFilter'; export type { EntityTypeReturn } from './useEntityTypeFilter'; -export { useEntityKinds } from './useEntityKinds'; export { useOwnUser } from './useOwnUser'; export { useRelatedEntities } from './useRelatedEntities'; export { useStarredEntities } from './useStarredEntities'; diff --git a/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx b/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx deleted file mode 100644 index c96301b3c4..0000000000 --- a/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * 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, { PropsWithChildren } from 'react'; -import { CatalogApi } from '@backstage/catalog-client'; -import { catalogApiRef } from '../api'; -import { renderHook } from '@testing-library/react-hooks'; -import { useEntityKinds } from './useEntityKinds'; -import { TestApiProvider } from '@backstage/test-utils'; - -const mockCatalogApi: Partial = { - getEntityFacets: jest.fn().mockResolvedValue({ - facets: { - kind: [ - { value: 'Template', count: 2 }, - { value: 'System', count: 1 }, - { value: 'Component', count: 3 }, - ], - }, - }), -}; - -const wrapper = ({ children }: PropsWithChildren<{}>) => { - return ( - - {children} - - ); -}; - -describe('useEntityKinds', () => { - it('gets entity kinds', async () => { - const { result, waitForValueToChange } = renderHook( - () => useEntityKinds(), - { wrapper }, - ); - await waitForValueToChange(() => result.current); - expect(result.current.kinds).toEqual(['Component', 'System', 'Template']); - }); -}); diff --git a/plugins/catalog-react/src/hooks/useEntityKinds.ts b/plugins/catalog-react/src/hooks/useEntityKinds.ts deleted file mode 100644 index 3dfc31eb88..0000000000 --- a/plugins/catalog-react/src/hooks/useEntityKinds.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * 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 useAsync from 'react-use/lib/useAsync'; -import { useApi } from '@backstage/core-plugin-api'; -import { catalogApiRef } from '../api'; - -/** - * Retrieve a list of unique entity kinds present in the catalog - * @public - * @deprecated and will be removed due to low utility value. - */ -export function useEntityKinds() { - const catalogApi = useApi(catalogApiRef); - - const { - error, - loading, - value: kinds, - } = useAsync(async () => { - return await catalogApi - .getEntityFacets({ facets: ['kind'] }) - .then(response => response.facets.kind?.map(f => f.value).sort() || []); - }); - return { error, loading, kinds }; -} From ffd784bf80615af4efab618a95767f26601e5179 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 4 Mar 2022 15:10:12 +0100 Subject: [PATCH 213/353] update changeset Signed-off-by: Johan Haals --- .changeset/tasty-carpets-fold.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tasty-carpets-fold.md b/.changeset/tasty-carpets-fold.md index 38f7099f9e..a1e62413bc 100644 --- a/.changeset/tasty-carpets-fold.md +++ b/.changeset/tasty-carpets-fold.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-react': minor --- -**BREAKING**: Removed `useEntityKinds` hook. +**BREAKING**: Removed the `useEntityKinds` hook, use `catalogApi.getEntityFacets({ facets: ['kind'] })` instead. From fc6290a76d291035372e5e88565183284bde06cd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 4 Mar 2022 16:03:51 +0100 Subject: [PATCH 214/353] catalog-react: removed useOwnUser Signed-off-by: Patrik Oldsberg --- .changeset/weak-schools-wash.md | 5 ++ plugins/catalog-react/api-report.md | 5 -- plugins/catalog-react/src/hooks/index.ts | 1 - plugins/catalog-react/src/hooks/useOwnUser.ts | 49 ------------------- .../components/DefaultTechDocsHome.test.tsx | 8 --- .../components/LegacyTechDocsHome.test.tsx | 8 --- .../components/TechDocsCustomHome.test.tsx | 8 --- 7 files changed, 5 insertions(+), 79 deletions(-) create mode 100644 .changeset/weak-schools-wash.md delete mode 100644 plugins/catalog-react/src/hooks/useOwnUser.ts diff --git a/.changeset/weak-schools-wash.md b/.changeset/weak-schools-wash.md new file mode 100644 index 0000000000..86f3111d93 --- /dev/null +++ b/.changeset/weak-schools-wash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +**BREAKING**: Removed the deprecated `useOwnUser` hook. Existing usage can be replaced with `identityApi.getBackstageIdentity()`, followed by a call to `catalogClient.getEntityByRef(identity.userEntityRef)`. diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 40764b8844..85bbc51328 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -6,7 +6,6 @@ /// import { ApiRef } from '@backstage/core-plugin-api'; -import { AsyncState } from 'react-use/lib/useAsync'; import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client'; import { CatalogApi } from '@backstage/catalog-client'; import { ComponentEntity } from '@backstage/catalog-model'; @@ -27,7 +26,6 @@ import { ScmIntegrationRegistry } from '@backstage/integration'; import { StyleRules } from '@material-ui/core/styles/withStyles'; import { SystemEntity } from '@backstage/catalog-model'; import { TableColumn } from '@backstage/core-components'; -import { UserEntity } from '@backstage/catalog-model'; // @public export const AsyncEntityProvider: ({ @@ -555,9 +553,6 @@ export function useOwnedEntities(allowedKinds?: string[]): { ownedEntities: GetEntitiesResponse | undefined; }; -// @public @deprecated -export function useOwnUser(): AsyncState; - // @public (undocumented) export function useRelatedEntities( entity: Entity, diff --git a/plugins/catalog-react/src/hooks/index.ts b/plugins/catalog-react/src/hooks/index.ts index e26f01928d..322f1cf31f 100644 --- a/plugins/catalog-react/src/hooks/index.ts +++ b/plugins/catalog-react/src/hooks/index.ts @@ -37,7 +37,6 @@ export type { export { useEntityTypeFilter } from './useEntityTypeFilter'; export type { EntityTypeReturn } from './useEntityTypeFilter'; export { useEntityKinds } from './useEntityKinds'; -export { useOwnUser } from './useOwnUser'; export { useRelatedEntities } from './useRelatedEntities'; export { useStarredEntities } from './useStarredEntities'; export { useStarredEntity } from './useStarredEntity'; diff --git a/plugins/catalog-react/src/hooks/useOwnUser.ts b/plugins/catalog-react/src/hooks/useOwnUser.ts deleted file mode 100644 index 211321afdf..0000000000 --- a/plugins/catalog-react/src/hooks/useOwnUser.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { - DEFAULT_NAMESPACE, - parseEntityRef, - UserEntity, -} from '@backstage/catalog-model'; -import useAsync, { AsyncState } from 'react-use/lib/useAsync'; -import { catalogApiRef } from '../api'; -import { identityApiRef, useApi } from '@backstage/core-plugin-api'; - -/** - * Get the catalog User entity (if any) that matches the logged-in user. - * @public - * @deprecated due to low external usage. - */ -export function useOwnUser(): AsyncState { - const catalogApi = useApi(catalogApiRef); - const identityApi = useApi(identityApiRef); - - return useAsync(async () => { - const identity = await identityApi.getBackstageIdentity(); - // TODO(freben): Defensively parse with defaults even though getEntityByRef - // supports the string form, since some auth resolvers have been known to - // return incomplete refs (just the name part) historically. This can be - // simplified in the future to just pass the ref immediately to - // getEntityByRef. - return catalogApi.getEntityByRef( - parseEntityRef(identity.userEntityRef, { - defaultKind: 'User', - defaultNamespace: DEFAULT_NAMESPACE, - }), - ) as Promise; - }, [catalogApi, identityApi]); -} diff --git a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx index 14fdb88395..48c1fa248c 100644 --- a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx +++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx @@ -36,14 +36,6 @@ import React from 'react'; import { rootDocsRouteRef } from '../../routes'; import { DefaultTechDocsHome } from './DefaultTechDocsHome'; -jest.mock('@backstage/plugin-catalog-react', () => { - const actual = jest.requireActual('@backstage/plugin-catalog-react'); - return { - ...actual, - useOwnUser: () => 'test-user', - }; -}); - const mockCatalogApi = { getEntityByRef: () => Promise.resolve(), getEntities: async () => ({ diff --git a/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx index 16d8d48bc3..4c33f69fb7 100644 --- a/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx +++ b/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx @@ -24,14 +24,6 @@ import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { ConfigApi, configApiRef } from '@backstage/core-plugin-api'; import { rootDocsRouteRef } from '../../routes'; -jest.mock('@backstage/plugin-catalog-react', () => { - const actual = jest.requireActual('@backstage/plugin-catalog-react'); - return { - ...actual, - useOwnUser: () => 'test-user', - }; -}); - const mockCatalogApi = { getEntityByRef: jest.fn(), getEntities: async () => ({ diff --git a/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx b/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx index 2088dbf4fc..6f09a70b5e 100644 --- a/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx +++ b/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx @@ -22,14 +22,6 @@ import { TechDocsCustomHome, PanelType } from './TechDocsCustomHome'; import { ApiProvider } from '@backstage/core-app-api'; import { rootDocsRouteRef } from '../../routes'; -jest.mock('@backstage/plugin-catalog-react', () => { - const actual = jest.requireActual('@backstage/plugin-catalog-react'); - return { - ...actual, - useOwnUser: () => 'test-user', - }; -}); - const mockCatalogApi = { getEntityByRef: jest.fn(), getEntities: async () => ({ From 7ffb2c73c9785a566a1b24c8b8e83609760e7e27 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 4 Mar 2022 15:43:51 +0100 Subject: [PATCH 215/353] catalog-react: removed loadCatalogOwnerRefs Signed-off-by: Patrik Oldsberg --- .changeset/kind-experts-lay.md | 7 ++ plugins/catalog-react/api-report.md | 13 ---- plugins/catalog-react/src/hooks/index.ts | 3 +- .../src/hooks/useEntityOwnership.test.tsx | 54 +-------------- .../src/hooks/useEntityOwnership.ts | 47 +------------ .../src/hooks/useOwnedEntities.ts | 69 ------------------- 6 files changed, 11 insertions(+), 182 deletions(-) create mode 100644 .changeset/kind-experts-lay.md delete mode 100644 plugins/catalog-react/src/hooks/useOwnedEntities.ts diff --git a/.changeset/kind-experts-lay.md b/.changeset/kind-experts-lay.md new file mode 100644 index 0000000000..00524bfe3a --- /dev/null +++ b/.changeset/kind-experts-lay.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +**BREAKING**: Removed the deprecated `loadCatalogOwnerRefs` function. Usages of this function can be directly replaced with `ownershipEntityRefs` from `identityApi.getBackstageIdentity()`. + +This also affects the `useEntityOwnership` hook in that it no longer uses `loadCatalogOwnerRefs`, meaning it will no longer load in additional relations and instead only rely on the `ownershipEntityRefs` from the `IdentityApi`. diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index ca5de26048..a55c53ed2e 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -13,7 +13,6 @@ import { ComponentEntity } from '@backstage/catalog-model'; import { ComponentProps } from 'react'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; -import { GetEntitiesResponse } from '@backstage/catalog-client'; import { IconButton } from '@material-ui/core'; import { LinkProps } from '@backstage/core-components'; import { Observable } from '@backstage/types'; @@ -455,12 +454,6 @@ export function InspectEntityDialog(props: { // @alpha export function isOwnerOf(owner: Entity, entity: Entity): boolean; -// @public @deprecated -export function loadCatalogOwnerRefs( - catalogApi: CatalogApi, - identityOwnerRefs: string[], -): Promise; - // @public (undocumented) export const MockEntityListContextProvider: ({ children, @@ -571,12 +564,6 @@ export function useEntityTypeFilter(): { setSelectedTypes: (types: string[]) => void; }; -// @public @deprecated -export function useOwnedEntities(allowedKinds?: string[]): { - loading: boolean; - ownedEntities: GetEntitiesResponse | undefined; -}; - // @public @deprecated export function useOwnUser(): AsyncState; diff --git a/plugins/catalog-react/src/hooks/index.ts b/plugins/catalog-react/src/hooks/index.ts index da23d54ad7..f0407f746c 100644 --- a/plugins/catalog-react/src/hooks/index.ts +++ b/plugins/catalog-react/src/hooks/index.ts @@ -43,6 +43,5 @@ export { useOwnUser } from './useOwnUser'; export { useRelatedEntities } from './useRelatedEntities'; export { useStarredEntities } from './useStarredEntities'; export { useStarredEntity } from './useStarredEntity'; -export { loadCatalogOwnerRefs, useEntityOwnership } from './useEntityOwnership'; -export { useOwnedEntities } from './useOwnedEntities'; +export { useEntityOwnership } from './useEntityOwnership'; export { useEntityPermission } from './useEntityPermission'; diff --git a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx index 4730f71a7d..d4d80203d3 100644 --- a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx @@ -15,18 +15,13 @@ */ import { CatalogApi } from '@backstage/catalog-client'; -import { - ComponentEntity, - RELATION_MEMBER_OF, - RELATION_OWNED_BY, - UserEntity, -} from '@backstage/catalog-model'; +import { ComponentEntity, RELATION_OWNED_BY } from '@backstage/catalog-model'; import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; import { TestApiProvider } from '@backstage/test-utils'; import { renderHook } from '@testing-library/react-hooks'; import React from 'react'; import { catalogApiRef } from '../api'; -import { loadCatalogOwnerRefs, useEntityOwnership } from './useEntityOwnership'; +import { useEntityOwnership } from './useEntityOwnership'; describe('useEntityOwnership', () => { type MockIdentityApi = jest.Mocked>; @@ -77,55 +72,10 @@ describe('useEntityOwnership', () => { ], }; - const user2Entity: UserEntity = { - apiVersion: 'backstage.io/v1beta1', - kind: 'User', - metadata: { - name: 'user2', - namespace: 'default', - }, - spec: { - /* should not be accessed */ - } as any, - relations: [ - { - type: RELATION_MEMBER_OF, - targetRef: 'group:default/group1', - target: { kind: 'Group', namespace: 'default', name: 'group1' }, - }, - ], - }; - afterEach(() => { jest.resetAllMocks(); }); - describe('loadCatalogOwnerRefs', () => { - it('loads the first user from the catalog', async () => { - mockCatalogApi.getEntityByRef.mockResolvedValueOnce(user2Entity); - await expect( - loadCatalogOwnerRefs(catalogApi, ['user:default/user2']), - ).resolves.toEqual(['group:default/group1']); - expect(mockCatalogApi.getEntityByRef).toBeCalledWith({ - kind: 'user', - namespace: 'default', - name: 'user2', - }); - }); - - it('gracefully handles missing user', async () => { - mockCatalogApi.getEntityByRef.mockResolvedValueOnce(undefined); - await expect( - loadCatalogOwnerRefs(catalogApi, ['user:default/user2']), - ).resolves.toEqual([]); - expect(mockCatalogApi.getEntityByRef).toBeCalledWith({ - kind: 'user', - namespace: 'default', - name: 'user2', - }); - }); - }); - describe('useEntityOwnership', () => { it('matches ownership via ownership entity refs', async () => { mockIdentityApi.getBackstageIdentity.mockResolvedValue({ diff --git a/plugins/catalog-react/src/hooks/useEntityOwnership.ts b/plugins/catalog-react/src/hooks/useEntityOwnership.ts index 55b4e75ace..f8a37a2c83 100644 --- a/plugins/catalog-react/src/hooks/useEntityOwnership.ts +++ b/plugins/catalog-react/src/hooks/useEntityOwnership.ts @@ -14,56 +14,16 @@ * limitations under the License. */ -import { CatalogApi } from '@backstage/catalog-client'; import { Entity, - parseEntityRef, - RELATION_MEMBER_OF, RELATION_OWNED_BY, stringifyEntityRef, } from '@backstage/catalog-model'; import { identityApiRef, useApi } from '@backstage/core-plugin-api'; import { useMemo } from 'react'; import useAsync from 'react-use/lib/useAsync'; -import { catalogApiRef } from '../api'; import { getEntityRelations } from '../utils/getEntityRelations'; -/** - * Takes the relevant parts of the User entity corresponding to the Backstage - * identity, and translates them into a list of entity refs on string form that - * represent the user's ownership connections. - * - * @public - * - * @param catalogApi - The Catalog API implementation - * @param identityOwnerRefs - List of identity owner refs as strings - * @returns OwnerRefs as a string array - * @deprecated Use `ownershipEntityRefs` from `identityApi.getBackstageIdentity()` instead. - */ -export async function loadCatalogOwnerRefs( - catalogApi: CatalogApi, - identityOwnerRefs: string[], -): Promise { - const result = new Array(); - - const primaryUserRef = identityOwnerRefs.find(ref => ref.startsWith('user:')); - if (primaryUserRef) { - const entity = await catalogApi.getEntityByRef( - parseEntityRef(primaryUserRef), - ); - if (entity) { - const memberOf = getEntityRelations(entity, RELATION_MEMBER_OF, { - kind: 'Group', - }); - for (const group of memberOf) { - result.push(stringifyEntityRef(group)); - } - } - } - - return result; -} - /** * Returns a function that checks whether the currently signed-in user is an * owner of a given entity. When the hook is initially mounted, the loading @@ -79,16 +39,11 @@ export function useEntityOwnership(): { isOwnedEntity: (entity: Entity) => boolean; } { const identityApi = useApi(identityApiRef); - const catalogApi = useApi(catalogApiRef); // Trigger load only on mount const { loading, value: refs } = useAsync(async () => { const { ownershipEntityRefs } = await identityApi.getBackstageIdentity(); - const catalogRefs = await loadCatalogOwnerRefs( - catalogApi, - ownershipEntityRefs, - ); - return new Set([...ownershipEntityRefs, ...catalogRefs]); + return ownershipEntityRefs; }, []); const isOwnedEntity = useMemo(() => { diff --git a/plugins/catalog-react/src/hooks/useOwnedEntities.ts b/plugins/catalog-react/src/hooks/useOwnedEntities.ts deleted file mode 100644 index a2258cf138..0000000000 --- a/plugins/catalog-react/src/hooks/useOwnedEntities.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * 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 { catalogApiRef } from './../api'; -import { loadCatalogOwnerRefs } from './useEntityOwnership'; -import { identityApiRef, useApi } from '@backstage/core-plugin-api'; -import { RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { GetEntitiesResponse } from '@backstage/catalog-client'; -import useAsync from 'react-use/lib/useAsync'; -import { useMemo } from 'react'; - -/** - * Takes the relevant parts of the Backstage identity, and translates them into - * a list of entities which are owned by the user. Takes an optional parameter - * to filter the entities based on allowedKinds - * - * @public - * - * @param allowedKinds - Array of allowed kinds to filter the entities - * @deprecated Use `ownershipEntityRefs` from `identityApi.getBackstageIdentity()` instead. - */ -export function useOwnedEntities(allowedKinds?: string[]): { - loading: boolean; - ownedEntities: GetEntitiesResponse | undefined; -} { - const identityApi = useApi(identityApiRef); - const catalogApi = useApi(catalogApiRef); - - const { loading, value: refs } = useAsync(async () => { - const identity = await identityApi.getBackstageIdentity(); - const identityRefs = identity.ownershipEntityRefs; - const catalogRefs = await loadCatalogOwnerRefs(catalogApi, identityRefs); - const catalogs = await catalogApi.getEntities( - allowedKinds - ? { - filter: { - kind: allowedKinds, - [`relations.${RELATION_OWNED_BY}`]: - [...identityRefs, ...catalogRefs] || [], - }, - } - : { - filter: { - [`relations.${RELATION_OWNED_BY}`]: - [...identityRefs, ...catalogRefs] || [], - }, - }, - ); - return catalogs; - }, []); - - const ownedEntities = useMemo(() => { - return refs; - }, [refs]); - - return useMemo(() => ({ loading, ownedEntities }), [loading, ownedEntities]); -} From 7a1dbe6ce966c56389c5efb388619b994988661b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 4 Mar 2022 16:23:14 +0100 Subject: [PATCH 216/353] techdocs: remove custom ownership handling Signed-off-by: Patrik Oldsberg --- .changeset/swift-mails-sing.md | 5 +++ .../home/components/TechDocsCustomHome.tsx | 40 ++++--------------- 2 files changed, 12 insertions(+), 33 deletions(-) create mode 100644 .changeset/swift-mails-sing.md diff --git a/.changeset/swift-mails-sing.md b/.changeset/swift-mails-sing.md new file mode 100644 index 0000000000..3df53e0340 --- /dev/null +++ b/.changeset/swift-mails-sing.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +The panels of `TechDocsCustomHome` now use the `useEntityOwnership` hook to resolve ownership when the `'ownedByUser'` filter predicate is used. diff --git a/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx b/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx index d2f0266af6..7023560e83 100644 --- a/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx +++ b/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx @@ -15,21 +15,16 @@ */ import React, { useState } from 'react'; -import useAsync, { AsyncState } from 'react-use/lib/useAsync'; +import useAsync from 'react-use/lib/useAsync'; import { makeStyles } from '@material-ui/core'; import { CSSProperties } from '@material-ui/styles'; import { CATALOG_FILTER_EXISTS, catalogApiRef, CatalogApi, - isOwnerOf, + useEntityOwnership, } from '@backstage/plugin-catalog-react'; -import { - DEFAULT_NAMESPACE, - Entity, - parseEntityRef, - UserEntity, -} from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; import { DocsTable } from './Tables'; import { DocsCardGrid } from './Grids'; import { TechDocsPageWrapper } from './TechDocsPageWrapper'; @@ -43,8 +38,7 @@ import { SupportButton, ContentHeader, } from '@backstage/core-components'; - -import { identityApiRef, useApi } from '@backstage/core-plugin-api'; +import { useApi } from '@backstage/core-plugin-api'; const panels = { DocsTable: DocsTable, @@ -104,16 +98,16 @@ const CustomPanel = ({ }, }); const classes = useStyles(); - const { value: user } = useOwnUser(); + const { loading: loadingOwnership, isOwnedEntity } = useEntityOwnership(); const Panel = panels[config.panelType]; const shownEntities = entities.filter(entity => { if (config.filterPredicate === 'ownedByUser') { - if (!user) { + if (loadingOwnership) { return false; } - return isOwnerOf(user, entity); + return isOwnedEntity(entity); } return ( @@ -225,23 +219,3 @@ export const TechDocsCustomHome = (props: TechDocsCustomHomeProps) => { ); }; - -function useOwnUser(): AsyncState { - const catalogApi = useApi(catalogApiRef); - const identityApi = useApi(identityApiRef); - - return useAsync(async () => { - const identity = await identityApi.getBackstageIdentity(); - // TODO(freben): Defensively parse with defaults even though getEntityByRef - // supports the string form, since some auth resolvers have been known to - // return incomplete refs (just the name part) historically. This can be - // simplified in the future to just pass the ref immediately to - // getEntityByRef. - return catalogApi.getEntityByRef( - parseEntityRef(identity.userEntityRef, { - defaultKind: 'User', - defaultNamespace: DEFAULT_NAMESPACE, - }), - ) as Promise; - }, [catalogApi, identityApi]); -} From 07400fd84bb094c5cc5779b933e96b9917e26213 Mon Sep 17 00:00:00 2001 From: Andrew Shirley Date: Fri, 4 Mar 2022 15:29:44 +0000 Subject: [PATCH 217/353] Correct log message Signed-off-by: Andrew Shirley --- .../src/processing/ProcessorOutputCollector.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts index c3cc6e288b..0a11f7191d 100644 --- a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts +++ b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts @@ -72,17 +72,17 @@ export class ProcessorOutputCollector { if (i.type === 'entity') { let entity: Entity; + const location = stringifyLocationRef(i.location); + try { entity = validateEntityEnvelope(i.entity); } catch (e) { assertError(e); - this.logger.debug(`Envelope validation failed at ${i.location}, ${e}`); + this.logger.debug(`Envelope validation failed at ${location}, ${e}`); this.errors.push(e); return; } - const location = stringifyLocationRef(i.location); - // Note that at this point, we have only validated the envelope part of // the entity data. Annotations are not part of that, so we have to be // defensive. If the annotations were malformed (e.g. were not a valid From 81e0166c5de152b77d660338e986271f700e3817 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 4 Mar 2022 16:43:33 +0100 Subject: [PATCH 218/353] SECURITY.md: add more examples to the coding practices Signed-off-by: Patrik Oldsberg --- SECURITY.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index caf90669c1..1ff2460d19 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -48,22 +48,67 @@ A common pitfall in backend packages is to resolve local file paths based on use For example, consider the following code: ```ts +// WARNING: DO NOT DO THIS + import { join } from 'path'; import fs from 'fs-extra'; function writeTemporaryFile(tmpDir: string, name: string, content: string) { - // WARNING: DO NOT DO THIS const filePath = join(tmpDir, name); await fs.writeFile(filePath, content); } ``` -If the `name` of the file is controlled by the user, they can for example enter `../../../../../../../../etc/hosts` as the name of the file. This can lead to a file being written outside the intended directory, which in turn can be used to inject malicious code or other form of attacks. +If the `name` of the file is controlled by the user, they can for example enter `../../../../etc/hosts` as the name of the file. This can lead to a file being written outside the intended directory, which in turn can be used to inject malicious code or other form of attacks. The recommended solution to this is to use `resolveSafeChildPath` from `@backstage/backend-common` to resolve the file path instead. It makes sure that the resolved path does not fall outside the provided directory. If you simply what to validate whether a file path is safe, you can use `isChildPath` instead. +The insecure example above should instead be written like this: + +```ts +// THIS IS GOOD, DO THIS + +import { resolveSafeChildPath } from '@backstaghe/backend-common'; +import fs from 'fs-extra'; + +function writeTemporaryFile(tmpDir: string, name: string, content: string) { + const filePath = resolveSafeChildPath(tmpDir, name); + await fs.writeFile(filePath, content); +} +``` + ### Express responses When returning a response from an Express route, always use `.json(...)` or `.end()` without any arguments. This ensures that the response can not be interpreted as an HTML document with embedded JavaScript by the browser. Never use `.send(...)` unless you want to send other forms of content, and be sure to always set an explicit content type. If you need to return HTML or other content that may be executed by the browser, be very careful how you handle user input. If you want to return an error response, simply throw the error or pass it on to the `next(...)` callback. There is a middleware installed that will transform the error into a JSON response. Many of the common HTTP errors are available from `@backstage/errors`, for example `NotFoundError`, which will also set the correct status code. + +The following example show how to return an error that contains user input: + +```ts +res.send(`Invalid id: '${req.params.id}'`); // BAD + +// import { InputError } from '@backstage/errors'; +throw new InputError(`Invalid id: '${req.params.id}'`); // GOOD + +// OR, in case a custom response is needed +res.json({ message: `Invalid id: '${req.params.id}'` }); // NOT BAD +``` + +No matter how trivial it may seem, always use `.json(...)`. It reduces the risk that a future refactoring introduces vulnerabilities: + +```ts +res.send({ ok: true }); // BAD + +res.json({ ok: true }); // GOOD +``` + +If you absolute must return a string with `.send(...)`, use an explicit and secure `Content-Type`: + +```ts +res.send(`message=${message}`); // BAD + +res.contentType('text/plain').send(`message=${message}`); // GOOD +``` + +An example of how to return dynamic HTML is not provided here. If you need to do so, proceed with extreme caution and be very sure that you know what you are doing. From 51a9f8f1228e849bc599b791ca8c35ac5d9bbd1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 4 Mar 2022 16:37:21 +0100 Subject: [PATCH 219/353] more cleanup in catalog-model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/modern-windows-brush.md | 9 + packages/catalog-model/api-report.md | 31 ---- packages/catalog-model/src/entity/index.ts | 1 - packages/catalog-model/src/entity/ref.test.ts | 164 +----------------- packages/catalog-model/src/entity/ref.ts | 39 ----- packages/catalog-model/src/index.ts | 2 +- .../catalog-model/src/location/annotation.ts | 18 +- packages/catalog-model/src/location/index.ts | 1 - packages/catalog-model/src/location/types.ts | 33 ---- packages/catalog-model/src/types.ts | 22 --- 10 files changed, 23 insertions(+), 297 deletions(-) create mode 100644 .changeset/modern-windows-brush.md delete mode 100644 packages/catalog-model/src/location/types.ts diff --git a/.changeset/modern-windows-brush.md b/.changeset/modern-windows-brush.md new file mode 100644 index 0000000000..49c45eb41e --- /dev/null +++ b/.changeset/modern-windows-brush.md @@ -0,0 +1,9 @@ +--- +'@backstage/catalog-model': minor +--- + +**BREAKING**: + +- Removed the previously deprecated type `EntityRef`. Please use `string` for stringified entity refs, `CompoundEntityRef` for compound kind-namespace-name triplet objects, or custom objects like `{ kind?: string; namespace?: string; name: string }` and similar if you have need for partial types. +- Removed the previously deprecated type `LocationSpec` type, which has been moved to `@backstage/plugin-catalog-backend`. +- Removed the previously deprecated function `parseEntityName`. Please use `parseEntityRef` instead. diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 9a65401d55..90242f9293 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -190,15 +190,6 @@ export type EntityPolicy = { enforce(entity: Entity): Promise; }; -// @public @deprecated -export type EntityRef = - | string - | { - kind?: string; - namespace?: string; - name: string; - }; - // @public export type EntityRelation = { type: string; @@ -316,13 +307,6 @@ export { LocationEntityV1alpha1 }; // @public export const locationEntityV1alpha1Validator: KindValidator; -// @public @deprecated -export type LocationSpec = { - type: string; - target: string; - presence?: 'optional' | 'required'; -}; - // @public export function makeValidator(overrides?: Partial): Validators; @@ -333,21 +317,6 @@ export class NoForeignRootFieldsEntityPolicy implements EntityPolicy { enforce(entity: Entity): Promise; } -// @public @deprecated -export function parseEntityName( - ref: - | string - | { - kind?: string; - namespace?: string; - name: string; - }, - context?: { - defaultKind?: string; - defaultNamespace?: string; - }, -): CompoundEntityRef; - // @public export function parseEntityRef( ref: diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts index fb9eee0353..f46f53945f 100644 --- a/packages/catalog-model/src/entity/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -36,7 +36,6 @@ export * from './policies'; export { getCompoundEntityRef, getEntityName, - parseEntityName, parseEntityRef, stringifyEntityRef, } from './ref'; diff --git a/packages/catalog-model/src/entity/ref.test.ts b/packages/catalog-model/src/entity/ref.test.ts index 045c4710dc..6cb7612439 100644 --- a/packages/catalog-model/src/entity/ref.test.ts +++ b/packages/catalog-model/src/entity/ref.test.ts @@ -14,171 +14,9 @@ * limitations under the License. */ -import { DEFAULT_NAMESPACE } from './constants'; -import { parseEntityName, parseEntityRef } from './ref'; +import { parseEntityRef } from './ref'; describe('ref', () => { - describe('parseEntityName', () => { - it('handles some omissions', () => { - expect(parseEntityName('a:b/c')).toEqual({ - kind: 'a', - namespace: 'b', - name: 'c', - }); - expect(() => parseEntityName('b/c')).toThrow(/kind/); - expect(parseEntityName('a:c')).toEqual({ - kind: 'a', - namespace: DEFAULT_NAMESPACE, - name: 'c', - }); - expect(() => parseEntityName('c')).toThrow(/kind/); - }); - - it('rejects bad inputs', () => { - expect(() => parseEntityName(null as any)).toThrow(); - expect(() => parseEntityName(7 as any)).toThrow(); - expect(() => parseEntityName('a:b:c')).toThrow(); - expect(() => parseEntityName('a/b/c')).toThrow(); - expect(() => parseEntityName('a/b:c')).toThrow(); - expect(() => parseEntityName('a:b/c/d')).toThrow(); - expect(() => parseEntityName('a:b/c:d')).toThrow(); - }); - - it('rejects empty parts in strings', () => { - // one is empty - expect(() => parseEntityName(':b/c')).toThrow(); - expect(() => parseEntityName('a:/c')).toThrow(); - expect(() => parseEntityName('a:b/')).toThrow(); - // two are empty - expect(() => parseEntityName('a:/')).toThrow(); - expect(() => parseEntityName(':b/')).toThrow(); - expect(() => parseEntityName(':/c')).toThrow(); - // three are empty - expect(() => parseEntityName(':/')).toThrow(); - // one is left out, one empty - expect(() => parseEntityName('/c')).toThrow(); - expect(() => parseEntityName('b/')).toThrow(); - expect(() => parseEntityName(':c')).toThrow(); - expect(() => parseEntityName('a:')).toThrow(); - // nothing at all - expect(() => parseEntityName('')).toThrow(); - }); - - it('rejects empty parts in compounds', () => { - // one is empty - expect(() => - parseEntityName({ kind: '', namespace: 'b', name: 'c' }), - ).toThrow(); - expect(() => parseEntityName({ namespace: 'b', name: 'c' })).toThrow(); - expect(() => - parseEntityName({ kind: 'a', namespace: '', name: 'c' }), - ).toThrow(); - expect(() => parseEntityName({ kind: 'a', name: 'c' })).not.toThrow(); - expect(() => - parseEntityName({ kind: 'a', namespace: 'b', name: '' }), - ).toThrow(); - expect(() => - parseEntityName({ kind: 'a', namespace: 'b' } as any), - ).toThrow(); - // two are empty - expect(() => - parseEntityName({ kind: '', namespace: '', name: 'c' }), - ).toThrow(); - expect(() => parseEntityName({ name: 'c' })).toThrow(); - expect(() => - parseEntityName({ kind: '', namespace: 'b', name: '' }), - ).toThrow(); - expect(() => parseEntityName({ namespace: 'b' } as any)).toThrow(); - expect(() => - parseEntityName({ kind: 'a', namespace: '', name: '' }), - ).toThrow(); - expect(() => parseEntityName({ kind: 'a' } as any)).toThrow(); - // three are empty - expect(() => - parseEntityName({ kind: '', namespace: '', name: '' }), - ).toThrow(); - expect(() => parseEntityName({} as any)).toThrow(); - // one is left out, one empty - expect(() => parseEntityName({ namespace: '', name: 'c' })).toThrow(); - expect(() => parseEntityName({ namespace: 'b', name: '' })).toThrow(); - expect(() => parseEntityName({ kind: '', name: 'c' })).toThrow(); - expect(() => parseEntityName({ kind: 'a', name: '' })).toThrow(); - }); - - it('adds defaults where necessary to strings', () => { - expect( - parseEntityName('a:b/c', { defaultKind: 'x', defaultNamespace: 'y' }), - ).toEqual({ kind: 'a', namespace: 'b', name: 'c' }); - expect( - parseEntityName('b/c', { defaultKind: 'x', defaultNamespace: 'y' }), - ).toEqual({ kind: 'x', namespace: 'b', name: 'c' }); - expect( - parseEntityName('a:c', { defaultKind: 'x', defaultNamespace: 'y' }), - ).toEqual({ kind: 'a', namespace: 'y', name: 'c' }); - expect(parseEntityName('a:c', { defaultKind: 'x' })).toEqual({ - kind: 'a', - namespace: DEFAULT_NAMESPACE, - name: 'c', - }); - expect( - parseEntityName('c', { defaultKind: 'x', defaultNamespace: 'y' }), - ).toEqual({ kind: 'x', namespace: 'y', name: 'c' }); - expect(parseEntityName('c', { defaultKind: 'x' })).toEqual({ - kind: 'x', - namespace: DEFAULT_NAMESPACE, - name: 'c', - }); - }); - - it('adds defaults where necessary to compounds', () => { - expect( - parseEntityName( - { kind: 'a', namespace: 'b', name: 'c' }, - { defaultKind: 'x', defaultNamespace: 'y' }, - ), - ).toEqual({ kind: 'a', namespace: 'b', name: 'c' }); - expect( - parseEntityName( - { namespace: 'b', name: 'c' }, - { defaultKind: 'x', defaultNamespace: 'y' }, - ), - ).toEqual({ kind: 'x', namespace: 'b', name: 'c' }); - expect( - parseEntityName( - { kind: 'a', name: 'c' }, - { defaultKind: 'x', defaultNamespace: 'y' }, - ), - ).toEqual({ kind: 'a', namespace: 'y', name: 'c' }); - expect( - parseEntityName({ kind: 'a', name: 'c' }, { defaultKind: 'x' }), - ).toEqual({ kind: 'a', namespace: DEFAULT_NAMESPACE, name: 'c' }); - expect( - parseEntityName( - { name: 'c' }, - { defaultKind: 'x', defaultNamespace: 'y' }, - ), - ).toEqual({ kind: 'x', namespace: 'y', name: 'c' }); - expect(parseEntityName({ name: 'c' }, { defaultKind: 'x' })).toEqual({ - kind: 'x', - namespace: DEFAULT_NAMESPACE, - name: 'c', - }); - // empty strings are errors, not defaults - expect(() => - parseEntityName( - { kind: '', namespace: 'b', name: 'c' }, - { defaultKind: 'x', defaultNamespace: 'y' }, - ), - ).toThrow(/kind/); - expect(() => - parseEntityName( - { kind: 'a', namespace: '', name: 'c' }, - { defaultKind: 'x', defaultNamespace: 'y' }, - ), - ).toThrow(/namespace/); - }); - }); - describe('parseEntityRef', () => { it('handles some omissions', () => { expect(parseEntityRef('a:b/c')).toEqual({ diff --git a/packages/catalog-model/src/entity/ref.ts b/packages/catalog-model/src/entity/ref.ts index 4529b1174c..acede5a56b 100644 --- a/packages/catalog-model/src/entity/ref.ts +++ b/packages/catalog-model/src/entity/ref.ts @@ -64,45 +64,6 @@ export function getCompoundEntityRef(entity: Entity): CompoundEntityRef { }; } -/** - * Parses an entity reference, either on string or compound form, and always - * returns a complete entity name including kind, namespace and name. - * - * @remarks - * - * This function automatically assumes the default namespace "default" unless - * otherwise specified as part of the options, and will throw an error if no - * kind was specified in the input reference and no default kind was given. - * - * @deprecated Please use parseEntityRef instead - * @public - * @param ref - The reference to parse - * @param context - The context of defaults that the parsing happens within - * @returns A complete entity name - */ -export function parseEntityName( - ref: string | { kind?: string; namespace?: string; name: string }, - context: { - /** The default kind, if none is given in the reference */ - defaultKind?: string; - /** The default namespace, if none is given in the reference */ - defaultNamespace?: string; - } = {}, -): CompoundEntityRef { - const { kind, namespace, name } = parseEntityRef(ref, { - defaultNamespace: DEFAULT_NAMESPACE, - ...context, - }); - - if (!kind) { - throw new Error( - `Entity reference ${namespace}/${name} did not contain a kind`, - ); - } - - return { kind, namespace, name }; -} - /** * Parses an entity reference, either on string or compound form, and returns * a structure with a name, and optional kind and namespace. diff --git a/packages/catalog-model/src/index.ts b/packages/catalog-model/src/index.ts index 19eacd4925..b5ccfed149 100644 --- a/packages/catalog-model/src/index.ts +++ b/packages/catalog-model/src/index.ts @@ -24,5 +24,5 @@ export * from './entity'; export { EntityPolicies } from './EntityPolicies'; export * from './kinds'; export * from './location'; -export type { EntityName, EntityRef, CompoundEntityRef } from './types'; +export type { EntityName, CompoundEntityRef } from './types'; export * from './validation'; diff --git a/packages/catalog-model/src/location/annotation.ts b/packages/catalog-model/src/location/annotation.ts index 61dbc0b305..bd282e2871 100644 --- a/packages/catalog-model/src/location/annotation.ts +++ b/packages/catalog-model/src/location/annotation.ts @@ -15,19 +15,25 @@ */ /** - * Constant storing location annotation. + * Entity annotation containing the location from which the entity is sourced. * - * @public */ + * @public + */ export const ANNOTATION_LOCATION = 'backstage.io/managed-by-location'; + /** - * Constant storing origin location annotation + * Entity annotation containing the originally sourced location which ultimately + * led to this entity being ingested. * - * @public */ + * @public + */ export const ANNOTATION_ORIGIN_LOCATION = 'backstage.io/managed-by-origin-location'; /** - * Contant storing source location annotation + * Entity annotation pointing to the source (e.g. source code repository root or + * similar) for this entity. * - * @public */ + * @public + */ export const ANNOTATION_SOURCE_LOCATION = 'backstage.io/source-location'; diff --git a/packages/catalog-model/src/location/index.ts b/packages/catalog-model/src/location/index.ts index dc709144b2..c53a1d2c83 100644 --- a/packages/catalog-model/src/location/index.ts +++ b/packages/catalog-model/src/location/index.ts @@ -24,4 +24,3 @@ export { parseLocationRef, stringifyLocationRef, } from './helpers'; -export type { LocationSpec } from './types'; diff --git a/packages/catalog-model/src/location/types.ts b/packages/catalog-model/src/location/types.ts deleted file mode 100644 index 9e2c9339e6..0000000000 --- a/packages/catalog-model/src/location/types.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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. - */ - -/** - * Holds the entity location information. - * - * @remarks - * - * `presence` flag: when using repo importer plugin, location is being created before the component yaml file is merged to the main branch. - * This flag is then set to indicate that the file can be not present. - * default value: 'required'. - * - * @public - * @deprecated Import from `@backstage/plugin-catalog-backend` instead. - */ -export type LocationSpec = { - type: string; - target: string; - presence?: 'optional' | 'required'; -}; diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts index e7d819ace9..7b3f23c64f 100644 --- a/packages/catalog-model/src/types.ts +++ b/packages/catalog-model/src/types.ts @@ -33,25 +33,3 @@ export type CompoundEntityRef = { * @public */ export type EntityName = CompoundEntityRef; - -/** - * A reference by name to an entity, either as a compact string representation, - * or as a compound reference structure. - * - * @deprecated Please use string directly, or EntityName (depending on what you actually need) - * @remarks - * - * The string representation is on the form `[:][/]`. - * - * Left-out parts of the reference need to be handled by the application, - * either by rejecting the reference or by falling back to default values. - * - * @public - */ -export type EntityRef = - | string - | { - kind?: string; - namespace?: string; - name: string; - }; From ed027b8b280db54697bd37cd2e592de1f067e370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 4 Mar 2022 16:59:57 +0100 Subject: [PATCH 220/353] just fix some random ugly comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../DependencyGraph/DependencyGraph.tsx | 2 +- .../test-utils/src/testUtils/logCollector.ts | 26 ++++++++++------ .../catalog-backend/src/ingestion/types.ts | 2 +- .../catalog-backend/src/processing/types.ts | 4 +-- plugins/catalog-backend/src/service/types.ts | 3 +- .../src/scaffolder/actions/types.ts | 3 +- plugins/tech-insights-node/src/facts.ts | 30 ++++++++----------- .../techdocs-backend/src/service/router.ts | 4 +-- 8 files changed, 40 insertions(+), 34 deletions(-) diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx index 49f209d04e..6db8e2dd02 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.tsx @@ -99,7 +99,7 @@ export interface DependencyGraphProps /** * Margin on top and bottom of whole graph * - * @remarks + * @remarks * * Default: 0 */ diff --git a/packages/test-utils/src/testUtils/logCollector.ts b/packages/test-utils/src/testUtils/logCollector.ts index b9fa6220ed..7529a00e7a 100644 --- a/packages/test-utils/src/testUtils/logCollector.ts +++ b/packages/test-utils/src/testUtils/logCollector.ts @@ -18,37 +18,44 @@ /** * Severity levels of {@link CollectedLogs} - * @public */ + * @public + */ export type LogFuncs = 'log' | 'warn' | 'error'; /** * AsyncLogCollector type used in {@link (withLogCollector:1)} callback function. - * @public */ + * @public + */ export type AsyncLogCollector = () => Promise; /** * SyncLogCollector type used in {@link (withLogCollector:2)} callback function. - * @public */ + * @public + */ export type SyncLogCollector = () => void; /** * Union type used in {@link (withLogCollector:3)} callback function. - * @public */ + * @public + */ export type LogCollector = AsyncLogCollector | SyncLogCollector; /** * Map of severity level and corresponding log lines. - * @public */ + * @public + */ export type CollectedLogs = { [key in T]: string[] }; const allCategories = ['log', 'warn', 'error']; /** * Asynchronous log collector with that collects all categories - * @public */ + * @public + */ export function withLogCollector( callback: AsyncLogCollector, ): Promise>; /** * Synchronous log collector with that collects all categories - * @public */ + * @public + */ export function withLogCollector( callback: SyncLogCollector, ): CollectedLogs; @@ -64,7 +71,8 @@ export function withLogCollector( /** * Synchronous log collector with that only collects selected categories - * @public */ + * @public + */ export function withLogCollector( logsToCollect: T[], callback: SyncLogCollector, @@ -73,7 +81,7 @@ export function withLogCollector( /** * Log collector that collect logs either from a sync or async collector. * @public - * */ + */ export function withLogCollector( logsToCollect: LogFuncs[] | LogCollector, callback?: LogCollector, diff --git a/plugins/catalog-backend/src/ingestion/types.ts b/plugins/catalog-backend/src/ingestion/types.ts index 37d12d12b1..6d1e9a3927 100644 --- a/plugins/catalog-backend/src/ingestion/types.ts +++ b/plugins/catalog-backend/src/ingestion/types.ts @@ -62,7 +62,7 @@ export type AnalyzeLocationExistingEntity = { * enough info for the frontend to know what form data to show to the user * for overriding/completing the info. * @public - * */ + */ export type AnalyzeLocationGenerateEntity = { // Some form of partial representation of the entity entity: RecursivePartial; diff --git a/plugins/catalog-backend/src/processing/types.ts b/plugins/catalog-backend/src/processing/types.ts index 484cd73939..b178522c14 100644 --- a/plugins/catalog-backend/src/processing/types.ts +++ b/plugins/catalog-backend/src/processing/types.ts @@ -48,7 +48,7 @@ export type EntityProcessingResult = /** * Responsible for executing the individual processing steps in order to fully process an entity. * @public - * */ + */ export interface CatalogProcessingOrchestrator { process(request: EntityProcessingRequest): Promise; } @@ -56,7 +56,7 @@ export interface CatalogProcessingOrchestrator { /** * Entities that are not yet processed. * @public - * */ + */ export type DeferredEntity = { entity: Entity; locationKey?: string; diff --git a/plugins/catalog-backend/src/service/types.ts b/plugins/catalog-backend/src/service/types.ts index 02268ab63c..fc315b9d89 100644 --- a/plugins/catalog-backend/src/service/types.ts +++ b/plugins/catalog-backend/src/service/types.ts @@ -77,7 +77,8 @@ export interface RefreshService { /** * Interacts with the database to manage locations. - * @public */ + * @public + */ export interface LocationStore { createLocation(location: LocationInput): Promise; listLocations(): Promise; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts index 2a87f58fd8..55ef68423e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts @@ -26,7 +26,8 @@ import { /** * ActionContext is passed into scaffolder actions. - * @public */ + * @public + */ export type ActionContext = { /** * Base URL for the location of the task spec, typically the url of the source entity file. diff --git a/plugins/tech-insights-node/src/facts.ts b/plugins/tech-insights-node/src/facts.ts index 7a70f92f92..24b6764ba4 100644 --- a/plugins/tech-insights-node/src/facts.ts +++ b/plugins/tech-insights-node/src/facts.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { DateTime, DurationLike } from 'luxon'; import { Config } from '@backstage/config'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -62,11 +63,10 @@ export type TechInsightFact = { }; /** - * Response type used when returning from database and API. * Adds a field for ref for easier usage * - * @public + * @public */ export type FlatTechInsightFact = TechInsightFact & { /** @@ -76,12 +76,12 @@ export type FlatTechInsightFact = TechInsightFact & { }; /** - * @public - * * A record type to specify individual fact shapes * * Used as part of a schema to validate, identify and generically construct usage implementations * of individual fact values in the system. + * + * @public */ export type FactSchema = { /** @@ -142,11 +142,9 @@ export type FactRetrieverContext = { }; /** - * @public - * * FactRetriever interface * - * A component specifying + * @public */ export interface FactRetriever { /** @@ -191,10 +189,9 @@ export interface FactRetriever { } /** - * @public - * * A Luxon duration like object for time to live value * + * @public * @example * \{ timeToLive: 1209600000 \} * \{ timeToLive: \{ weeks: 4 \} \} @@ -203,10 +200,9 @@ export interface FactRetriever { export type TTL = { timeToLive: DurationLike }; /** - * @public - * * A maximum number for items to be kept in the database for each fact retriever/entity pair * + * @public * @example * \{ maxItems: 10 \} * @@ -214,25 +210,25 @@ export type TTL = { timeToLive: DurationLike }; export type MaxItems = { maxItems: number }; /** - * @public - * * A fact lifecycle definition. Determines which strategy to use to purge expired facts from the database. + * + * @public */ export type FactLifecycle = TTL | MaxItems; /** - * @public - * * A flat serializable structure for Facts. * Containing information about fact schema, version, id, and entity filters + * + * @public */ export type FactSchemaDefinition = Omit; /** - * @public - * * Registration of a fact retriever * Used to add and schedule individual fact retrievers to the fact retriever engine. + * + * @public */ export type FactRetrieverRegistration = { /** diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index 73f212d049..9d240cb7fa 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -347,8 +347,8 @@ export function createEventStream( } /** - * @deprecated use event-stream implementation of the sync endpoint - * */ + * @deprecated use event-stream implementation of the sync endpoint + */ export function createHttpResponse( res: Response, ): DocsSynchronizerSyncOpts { From c1168bb44012a75f1237fd28da03a465275eaa3d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 4 Mar 2022 17:06:40 +0100 Subject: [PATCH 221/353] Create polite-apples-relax.md Signed-off-by: Patrik Oldsberg --- .changeset/polite-apples-relax.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/polite-apples-relax.md diff --git a/.changeset/polite-apples-relax.md b/.changeset/polite-apples-relax.md new file mode 100644 index 0000000000..f6d10aaabf --- /dev/null +++ b/.changeset/polite-apples-relax.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixed display of the location in the log message that is printed when entity envelope validation fails. From 2a5500427704ededcb17c3d599e3b61bbb07a8e9 Mon Sep 17 00:00:00 2001 From: Alisa Wong Date: Fri, 4 Mar 2022 12:26:55 -0500 Subject: [PATCH 222/353] add names to sidebar submenu styles Signed-off-by: Alisa Wong --- .../src/layout/Sidebar/SidebarSubmenu.tsx | 107 +++++++++--------- .../src/layout/Sidebar/SidebarSubmenuItem.tsx | 103 +++++++++-------- 2 files changed, 108 insertions(+), 102 deletions(-) diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx index 60175e2439..5a511b2094 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx @@ -26,62 +26,65 @@ import { import { BackstageTheme } from '@backstage/theme'; const useStyles = (props: { left: number }) => - makeStyles(theme => ({ - root: { - zIndex: 1000, - position: 'relative', - overflow: 'visible', - width: theme.spacing(7) + 1, - }, - drawer: { - display: 'flex', - flexFlow: 'column nowrap', - alignItems: 'flex-start', - position: 'fixed', - [theme.breakpoints.up('sm')]: { - marginLeft: props.left, - transition: theme.transitions.create('margin-left', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.shortest, - }), - }, - top: 0, - bottom: 0, - padding: 0, - background: theme.palette.navigation.submenu?.background ?? '#404040', - overflowX: 'hidden', - msOverflowStyle: 'none', - scrollbarWidth: 'none', - cursor: 'default', - width: submenuConfig.drawerWidthClosed, - transitionDelay: `${submenuConfig.defaultOpenDelayMs}ms`, - '& > *': { - flexShrink: 0, - }, - '&::-webkit-scrollbar': { - display: 'none', - }, - }, - drawerOpen: { - width: submenuConfig.drawerWidthOpen, - [theme.breakpoints.down('xs')]: { - width: '100%', + makeStyles( + theme => ({ + root: { + zIndex: 1000, position: 'relative', - paddingLeft: theme.spacing(3), - left: 0, + overflow: 'visible', + width: theme.spacing(7) + 1, + }, + drawer: { + display: 'flex', + flexFlow: 'column nowrap', + alignItems: 'flex-start', + position: 'fixed', + [theme.breakpoints.up('sm')]: { + marginLeft: props.left, + transition: theme.transitions.create('margin-left', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.shortest, + }), + }, top: 0, + bottom: 0, + padding: 0, + background: theme.palette.navigation.submenu?.background ?? '#404040', + overflowX: 'hidden', + msOverflowStyle: 'none', + scrollbarWidth: 'none', + cursor: 'default', + width: submenuConfig.drawerWidthClosed, + transitionDelay: `${submenuConfig.defaultOpenDelayMs}ms`, + '& > *': { + flexShrink: 0, + }, + '&::-webkit-scrollbar': { + display: 'none', + }, }, - }, - title: { - fontSize: 24, - fontWeight: 500, - color: '#FFF', - padding: 20, - [theme.breakpoints.down('xs')]: { - display: 'none', + drawerOpen: { + width: submenuConfig.drawerWidthOpen, + [theme.breakpoints.down('xs')]: { + width: '100%', + position: 'relative', + paddingLeft: theme.spacing(3), + left: 0, + top: 0, + }, }, - }, - })); + title: { + fontSize: 24, + fontWeight: 500, + color: '#FFF', + padding: 20, + [theme.breakpoints.down('xs')]: { + display: 'none', + }, + }, + }), + { name: 'BackstageSidebarSubmenu' }, + ); /** * Holds a title for text Header of a sidebar submenu and children diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx index d204728bcf..5c9dce010d 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx @@ -26,59 +26,62 @@ import ArrowDropUpIcon from '@material-ui/icons/ArrowDropUp'; import { SidebarItemWithSubmenuContext } from './config'; import { isLocationMatch } from './utils'; -const useStyles = makeStyles(theme => ({ - item: { - height: 48, - width: '100%', - '&:hover': { +const useStyles = makeStyles( + theme => ({ + item: { + height: 48, + width: '100%', + '&:hover': { + background: '#6f6f6f', + color: theme.palette.navigation.selectedColor, + }, + display: 'flex', + alignItems: 'center', + color: theme.palette.navigation.color, + padding: 20, + cursor: 'pointer', + position: 'relative', + background: 'none', + border: 'none', + }, + itemContainer: { + width: '100%', + }, + selected: { background: '#6f6f6f', - color: theme.palette.navigation.selectedColor, + color: '#FFF', }, - display: 'flex', - alignItems: 'center', - color: theme.palette.navigation.color, - padding: 20, - cursor: 'pointer', - position: 'relative', - background: 'none', - border: 'none', - }, - itemContainer: { - width: '100%', - }, - selected: { - background: '#6f6f6f', - color: '#FFF', - }, - label: { - margin: 14, - marginLeft: 7, - fontSize: 14, - }, - dropdownArrow: { - position: 'absolute', - right: 21, - }, - dropdown: { - display: 'flex', - flexDirection: 'column', - alignItems: 'end', - }, - dropdownItem: { - width: '100%', - padding: '10px 0 10px 0', - }, - textContent: { - color: theme.palette.navigation.color, - display: 'flex', - justifyContent: 'center', - [theme.breakpoints.down('xs')]: { - display: 'block', - paddingLeft: theme.spacing(4), + label: { + margin: 14, + marginLeft: 7, + fontSize: 14, }, - fontSize: '14px', - }, -})); + dropdownArrow: { + position: 'absolute', + right: 21, + }, + dropdown: { + display: 'flex', + flexDirection: 'column', + alignItems: 'end', + }, + dropdownItem: { + width: '100%', + padding: '10px 0 10px 0', + }, + textContent: { + color: theme.palette.navigation.color, + display: 'flex', + justifyContent: 'center', + [theme.breakpoints.down('xs')]: { + display: 'block', + paddingLeft: theme.spacing(4), + }, + fontSize: '14px', + }, + }), + { name: 'BackstageSidebarSubmenuItem' }, +); /** * Clickable item displayed when submenu item is clicked. From bae72d6f4d124f9987c3f54377a87319f72f72b6 Mon Sep 17 00:00:00 2001 From: Jonathan Ash Date: Fri, 4 Mar 2022 17:47:20 +0000 Subject: [PATCH 223/353] Tech Radar ring names are now coloured by theme (via theme.palette.text.primary) Signed-off-by: Jonathan Ash --- .changeset/shaggy-apricots-hug.md | 5 +++++ plugins/tech-radar/src/components/RadarGrid/RadarGrid.tsx | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/shaggy-apricots-hug.md diff --git a/.changeset/shaggy-apricots-hug.md b/.changeset/shaggy-apricots-hug.md new file mode 100644 index 0000000000..62bea4c3bd --- /dev/null +++ b/.changeset/shaggy-apricots-hug.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-radar': patch +--- + +Tech Radar Ring names are now coloured from theme via theme.palette.text.primary (instead of a hard coded colour) diff --git a/plugins/tech-radar/src/components/RadarGrid/RadarGrid.tsx b/plugins/tech-radar/src/components/RadarGrid/RadarGrid.tsx index 94ef2b1bda..ef62b8abd9 100644 --- a/plugins/tech-radar/src/components/RadarGrid/RadarGrid.tsx +++ b/plugins/tech-radar/src/components/RadarGrid/RadarGrid.tsx @@ -23,7 +23,7 @@ export type Props = { rings: Ring[]; }; -const useStyles = makeStyles(() => ({ +const useStyles = makeStyles(theme => ({ ring: { fill: 'none', stroke: '#bbb', @@ -37,7 +37,7 @@ const useStyles = makeStyles(() => ({ text: { pointerEvents: 'none', userSelect: 'none', - fill: '#e5e5e5', + fill: theme.palette.text.primary, fontSize: '25px', fontWeight: 800, }, From cea6f10b9703df5e177f4fadb6e3aa0c3914500c Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 3 Mar 2022 14:05:19 +0100 Subject: [PATCH 224/353] Recreate a shell of @backstage/techdocs-common for smooth deprecation process Signed-off-by: Eric Peterson --- .changeset/techdocs-byta-namnet.md | 5 + .changeset/techdocs-empty-office.md | 7 + packages/techdocs-common/.eslintrc.js | 3 + packages/techdocs-common/CHANGELOG.md | 1065 ++++++++++++++++++++++++ packages/techdocs-common/README.md | 53 ++ packages/techdocs-common/api-report.md | 9 + packages/techdocs-common/package.json | 51 ++ packages/techdocs-common/src/index.ts | 17 + 8 files changed, 1210 insertions(+) create mode 100644 .changeset/techdocs-byta-namnet.md create mode 100644 .changeset/techdocs-empty-office.md create mode 100644 packages/techdocs-common/.eslintrc.js create mode 100644 packages/techdocs-common/CHANGELOG.md create mode 100644 packages/techdocs-common/README.md create mode 100644 packages/techdocs-common/api-report.md create mode 100644 packages/techdocs-common/package.json create mode 100644 packages/techdocs-common/src/index.ts diff --git a/.changeset/techdocs-byta-namnet.md b/.changeset/techdocs-byta-namnet.md new file mode 100644 index 0000000000..f77849513b --- /dev/null +++ b/.changeset/techdocs-byta-namnet.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-node': patch +--- + +Renamed `@backstage/techdocs-common` to `@backstage/plugin-techdocs-node`. diff --git a/.changeset/techdocs-empty-office.md b/.changeset/techdocs-empty-office.md new file mode 100644 index 0000000000..082521be35 --- /dev/null +++ b/.changeset/techdocs-empty-office.md @@ -0,0 +1,7 @@ +--- +'@backstage/techdocs-common': patch +--- + +**DEPRECATION** + +The `@backstage/techdocs-common` package is being renamed `@backstage/plugin-techdocs-node`. We may continue to publish changes to `@backstage/techdocs-common` for a time, but will stop doing so in the near future. If you depend on this package, you should update your dependencies to point at the renamed package. diff --git a/packages/techdocs-common/.eslintrc.js b/packages/techdocs-common/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/packages/techdocs-common/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md new file mode 100644 index 0000000000..f41c944a89 --- /dev/null +++ b/packages/techdocs-common/CHANGELOG.md @@ -0,0 +1,1065 @@ +# @backstage/techdocs-common + +## 0.11.11 + +### Patch Changes + +- 955be6bc7d: adds passing projectID to the Storage client +- ff0a16fb1a: Modify techdocs builder to automatically append techdocs-core plugin to mkdocs.yaml file if it is missing. Adds an optional configuration item if this plugin needs to be omitted. +- Updated dependencies + - @backstage/catalog-model@0.12.0 + - @backstage/backend-common@0.12.0 + - @backstage/integration@0.8.0 + - @backstage/search-common@0.3.0 + +## 0.11.10 + +### Patch Changes + +- 209fd128e6: Updated usage of `github:` location types in docs to use `url:` instead. +- 13ef228d03: Clean up the API interface for TechDocs common library. +- Updated dependencies + - @backstage/backend-common@0.11.0 + - @backstage/catalog-model@0.11.0 + - @backstage/integration@0.7.5 + +## 0.11.9 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/backend-common@0.10.9 + - @backstage/catalog-model@0.10.1 + - @backstage/config@0.1.15 + - @backstage/errors@0.2.2 + - @backstage/integration@0.7.4 + - @backstage/search-common@0.2.4 + +## 0.11.8 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- 216725b434: Updated to use new names for `parseLocationRef` and `stringifyLocationRef` +- 7aeb491394: Replace use of deprecated `ENTITY_DEFAULT_NAMESPACE` constant with `DEFAULT_NAMESPACE`. +- Updated dependencies + - @backstage/backend-common@0.10.8 + - @backstage/errors@0.2.1 + - @backstage/integration@0.7.3 + - @backstage/catalog-model@0.10.0 + - @backstage/config@0.1.14 + - @backstage/search-common@0.2.3 + +## 0.11.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7 + +## 0.11.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + +## 0.11.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6 + +## 0.11.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + +## 0.11.5 + +### Patch Changes + +- ff93fbeeec: Fix interpolated string for "Failed to generate docs from ..." +- Updated dependencies + - @backstage/search-common@0.2.2 + - @backstage/backend-common@0.10.5 + +## 0.11.4 + +### Patch Changes + +- 47277c0d8c: Updated the default version of the `@spotify/techdocs` container used when `techdocs.generator.runIn` is `docker` to `v0.3.6`, which includes an update to `mkdocs-monorepo-plugin` that allows glob-based wildcard includes. +- Updated dependencies + - @backstage/integration@0.7.2 + - @backstage/backend-common@0.10.4 + - @backstage/config@0.1.13 + - @backstage/catalog-model@0.9.10 + +## 0.11.4-next.0 + +### Patch Changes + +- 47277c0d8c: Updated the default version of the `@spotify/techdocs` container used when `techdocs.generator.runIn` is `docker` to `v0.3.6`, which includes an update to `mkdocs-monorepo-plugin` that allows glob-based wildcard includes. +- Updated dependencies + - @backstage/backend-common@0.10.4-next.0 + - @backstage/config@0.1.13-next.0 + - @backstage/catalog-model@0.9.10-next.0 + - @backstage/integration@0.7.2-next.0 + +## 0.11.3 + +### Patch Changes + +- 5333451def: Cleaned up API exports +- Updated dependencies + - @backstage/config@0.1.12 + - @backstage/integration@0.7.1 + - @backstage/backend-common@0.10.3 + - @backstage/errors@0.2.0 + - @backstage/catalog-model@0.9.9 + +## 0.11.2 + +### Patch Changes + +- c2c8768771: Bump `@azure/identity` from `^1.5.0` to `^2.0.1`. +- Updated dependencies + - @backstage/backend-common@0.10.1 + - @backstage/integration@0.7.0 + +## 0.11.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.0 + +## 0.11.0 + +### Minor Changes + +- 1bada775a9: Added the ability for the TechDocs Backend to (optionally) leverage a cache + store to improve performance when reading files from a cloud storage provider. + +### Patch Changes + +- dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly +- Updated dependencies + - @backstage/backend-common@0.9.13 + +## 0.10.8 + +### Patch Changes + +- bab752e2b3: Change default port of backend from 7000 to 7007. + + This is due to the AirPlay Receiver process occupying port 7000 and preventing local Backstage instances on MacOS to start. + + You can change the port back to 7000 or any other value by providing an `app-config.yaml` with the following values: + + ``` + backend: + listen: 0.0.0.0:7123 + baseUrl: http://localhost:7123 + ``` + + More information can be found here: https://backstage.io/docs/conf/writing + +- Updated dependencies + - @backstage/errors@0.1.5 + - @backstage/backend-common@0.9.11 + +## 0.10.7 + +### Patch Changes + +- 0b60a051c9: Added OpenStack Swift case migration support. +- 9e64a7ac1e: Allow amazon web services s3 buckets to pass an server side encryption configuration so they can publish to encrypted buckets +- Updated dependencies + - @backstage/catalog-model@0.9.7 + - @backstage/backend-common@0.9.10 + +## 0.10.6 + +### Patch Changes + +- a2d4389587: 1. Techdocs publisher constructors now use parameter objects when being + instantiated + + 2. Internal refactor of `LocalPublish` publisher to use `fromConfig` for + creation to be aligned with other publishers; this does not impact + `LocalPublish` usage. + + ```diff + - const publisher = new LocalPublish(config, logger, discovery); + + const publisher = LocalPublish.fromConfig(config, logger, discovery); + ``` + +- 6129c89a47: Default TechDocs container used at docs generation-time is now [v0.3.5](https://github.com/backstage/techdocs-container/releases/tag/v0.3.5). +- f3c7eec64b: Updated to properly join URL segments under any OS for both AWS S3 and GCP +- Updated dependencies + - @backstage/backend-common@0.9.9 + +## 0.10.5 + +### Patch Changes + +- d207f6ee9e: Support optional bucketRootPath configuration parameter in S3 and GCS publishers +- Updated dependencies + - @backstage/config@0.1.11 + - @backstage/errors@0.1.4 + - @backstage/integration@0.6.9 + - @backstage/backend-common@0.9.8 + - @backstage/catalog-model@0.9.6 + - @backstage/search-common@0.2.1 + +## 0.10.4 + +### Patch Changes + +- 87f5b9db13: Use docs/README.md or README.md as fallback if docs/index.md is missing +- 36e67d2f24: Internal updates to apply more strict checks to throw errors. +- Updated dependencies + - @backstage/backend-common@0.9.7 + - @backstage/errors@0.1.3 + - @backstage/catalog-model@0.9.5 + +## 0.10.3 + +### Patch Changes + +- 156421c59a: Sets the default techdocs docker image to the [latest released version - v0.3.3](https://github.com/backstage/techdocs-container/releases/tag/v0.3.3). +- Updated dependencies + - @backstage/catalog-model@0.9.4 + - @backstage/backend-common@0.9.6 + - @backstage/integration@0.6.7 + +## 0.10.2 + +### Patch Changes + +- 1c75e8bf98: Add more context to techdocs log lines when files are not found along with + ensuring that the routers return 404 with a descriptive message. +- e92f0f728b: Locks the version of the default docker image used to generate TechDocs. As of + this changelog entry, it is v0.3.2! +- Updated dependencies + - @backstage/backend-common@0.9.5 + - @backstage/integration@0.6.6 + +## 0.10.1 + +### Patch Changes + +- 96fef17a18: Upgrade git-parse-url to v11.6.0 +- Updated dependencies + - @backstage/backend-common@0.9.3 + - @backstage/integration@0.6.4 + +## 0.10.0 + +### Minor Changes + +- 8b0f6f860: Set the correct `edit_uri` or `repo_url` for documentation pages that are hosted on GitHub and GitLab. + + The constructor of the `TechDocsGenerator` changed. + Prefer the use of `TechdocsGenerator.fromConfig(…)` instead: + + ```diff + - const techdocsGenerator = new TechdocsGenerator({ + + const techdocsGenerator = TechdocsGenerator.fromConfig(config, { + logger, + containerRunner, + - config, + }); + ``` + +### Patch Changes + +- 30ed662a3: Adding in-context search to TechDocs Reader component. Using existing search-backend to query for indexed search results scoped into a specific entity's techdocs. Needs TechDocsCollator enabled on the backend to work. + + Adding extra information to indexed tech docs documents for search. + +- 3624616e7: "Local" (out-of-the-box) publisher explicitly follows lower-case entity triplet + logic. +- 67ba7e088: Only write the updated `mkdocs.yml` file if the content was updated. + + This keeps local files unchanged if the `dir` annotation is used in combination with the `file` location. + +- 8eab6be6a: Force using `posix` path for cloud storage +- Updated dependencies + - @backstage/integration@0.6.3 + - @backstage/search-common@0.2.0 + - @backstage/catalog-model@0.9.1 + - @backstage/backend-common@0.9.1 + +## 0.9.0 + +### Minor Changes + +- 58452cdb7: OpenStack Swift Client changed with Trendyol's OpenStack Swift SDK. + + ## Migration from old OpenStack Swift Configuration + + Let's assume we have the old OpenStack Swift configuration here. + + ```yaml + techdocs: + publisher: + type: 'openStackSwift' + openStackSwift: + containerName: 'name-of-techdocs-storage-bucket' + credentials: + username: ${OPENSTACK_SWIFT_STORAGE_USERNAME} + password: ${OPENSTACK_SWIFT_STORAGE_PASSWORD} + authUrl: ${OPENSTACK_SWIFT_STORAGE_AUTH_URL} + keystoneAuthVersion: ${OPENSTACK_SWIFT_STORAGE_AUTH_VERSION} + domainId: ${OPENSTACK_SWIFT_STORAGE_DOMAIN_ID} + domainName: ${OPENSTACK_SWIFT_STORAGE_DOMAIN_NAME} + region: ${OPENSTACK_SWIFT_STORAGE_REGION} + ``` + + ##### Step 1: Change the credential keys + + Since the new SDK uses _Application Credentials_ to authenticate OpenStack, we + need to change the keys `credentials.username` to `credentials.id`, + `credentials.password` to `credentials.secret` and use Application Credential ID + and secret here. For more detail about credentials look + [here](https://docs.openstack.org/api-ref/identity/v3/?expanded=password-authentication-with-unscoped-authorization-detail,authenticating-with-an-application-credential-detail#authenticating-with-an-application-credential). + + ##### Step 2: Remove the unused keys + + Since the new SDK doesn't use the old way authentication, we don't need the keys + `openStackSwift.keystoneAuthVersion`, `openStackSwift.domainId`, + `openStackSwift.domainName` and `openStackSwift.region`. So you can remove them. + + ##### Step 3: Add Swift URL + + The new SDK needs the OpenStack Swift connection URL for connecting the Swift. + So you need to add a new key called `openStackSwift.swiftUrl` and give the + OpenStack Swift url here. Example url should look like that: + `https://example.com:6780/swift/v1` + + ##### That's it! + + Your new configuration should look like that! + + ```yaml + techdocs: + publisher: + type: 'openStackSwift' + openStackSwift: + containerName: 'name-of-techdocs-storage-bucket' + credentials: + id: ${OPENSTACK_SWIFT_STORAGE_APPLICATION_CREDENTIALS_ID} + secret: ${OPENSTACK_SWIFT_STORAGE_APPLICATION_CREDENTIALS_SECRET} + authUrl: ${OPENSTACK_SWIFT_STORAGE_AUTH_URL} + swiftUrl: ${OPENSTACK_SWIFT_STORAGE_SWIFT_URL} + ``` + +- c772d9a84: TechDocs sites can now be accessed using paths containing entity triplets of + any case (e.g. `/docs/namespace/KIND/name` or `/docs/namespace/kind/name`). + + If you do not use an external storage provider for serving TechDocs, this is a + transparent change and no action is required from you. + + If you _do_ use an external storage provider for serving TechDocs (one of\* GCS, + AWS S3, or Azure Blob Storage), you must run a migration command against your + storage provider before updating. + + [A migration guide is available here](https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-alpha-to-beta). + + - (\*) We're seeking help from the community to bring OpenStack Swift support + [to feature parity](https://github.com/backstage/backstage/issues/6763) with the above. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.9.0 + - @backstage/integration@0.6.2 + - @backstage/config@0.1.8 + +## 0.8.1 + +### Patch Changes + +- bc405be6e: Stale TechDocs content (files that had previously been published but which have + since been removed) is now removed from storage at publish-time. This is now + supported by the following publishers: + + - Google GCS + - AWS S3 + - Azure Blob Storage + + You may need to apply a greater level of permissions (e.g. the ability to + delete objects in your storage provider) to any credentials/accounts used by + the TechDocs CLI or TechDocs backend in order for this change to take effect. + + For more details, see [#6132][issue-ref]. + + [issue-ref]: https://github.com/backstage/backstage/issues/6132 + +- Updated dependencies + - @backstage/integration@0.6.0 + - @backstage/backend-common@0.8.9 + +## 0.8.0 + +### Minor Changes + +- 48ea3d25b: TechDocs has dropped all support for the long-ago deprecated git-based common + prepares as well as all corresponding values in `backstage.io/techdocs-ref` + annotations. + + Entities whose `backstage.io/techdocs-ref` annotation values still begin with + `github:`, `gitlab:`, `bitbucket:`, or `azure/api:` will no longer be generated + by TechDocs. Be sure to update these values so that they align with their + expected format and your usage of TechDocs. + + For details, see [this explainer on TechDocs ref annotation values][how]. + + [how]: https://backstage.io/docs/features/techdocs/how-to-guides#how-to-understand-techdocs-ref-annotation-values + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.8.8 + - @backstage/config@0.1.6 + - @backstage/integration@0.5.9 + +## 0.7.1 + +### Patch Changes + +- 59a5fa319: Migrated files are now printed when `techdocs-cli migrate` is run with the + `--verbose` flag set. +- 54356336e: TechDocs generator stage now supports `mkdocs.yaml` file, in addition to `.yml` + depending on whichever is present at the time of generation. (Assumes the + latest `spotify/techdocs` container, running mkdocs `v1.2.2` or greater). + +## 0.7.0 + +### Minor Changes + +- d32d01e5b: Improve the annotation `backstage.io/techdocs-ref: dir:` that links to a path that is relative to the source of the annotated entity. + This annotation works with the basic and the recommended flow, however, it will be most useful with the basic approach. + + This change remove the deprecation of the `dir` reference and provides first-class support for it. + In addition, this change removes the support of the deprecated `github`, `gitlab`, and `azure/api` locations from the `dir` reference preparer. + + #### Example Usage + + The annotation is convenient if the documentation is stored in the same location, i.e. the same git repository, as the `catalog-info.yaml`. + While it is still supported to add full URLs such as `backstage.io/techdocs-ref: url:https://...` for custom setups, documentation is mostly stored in the same repository as the entity definition. + By automatically resolving the target relative to the registration location of the entity, the configuration overhead for this default setup is minimized. + Since it leverages the `@backstage/integrations` package for the URL resolution, this is compatible with every supported source. + + Consider the following examples: + + 1. "I have a repository with a single `catalog-info.yaml` and a TechDocs page in the root folder!" + + ``` + https://github.com/backstage/example/tree/main/ + |- catalog-info.yaml + | > apiVersion: backstage.io/v1alpha1 + | > kind: Component + | > metadata: + | > name: example + | > annotations: + | > backstage.io/techdocs-ref: dir:. # -> same folder + | > spec: {} + |- docs/ + |- mkdocs.yml + ``` + + 2. "I have a repository with a single `catalog-info.yaml` and my TechDocs page in located in a folder!" + + ``` + https://bitbucket.org/my-owner/my-project/src/master/ + |- catalog-info.yaml + | > apiVersion: backstage.io/v1alpha1 + | > kind: Component + | > metadata: + | > name: example + | > annotations: + | > backstage.io/techdocs-ref: dir:./some-folder # -> subfolder + | > spec: {} + |- some-folder/ + |- docs/ + |- mkdocs.yml + ``` + + 3. "I have a mono repository that hosts multiple components!" + + ``` + https://dev.azure.com/organization/project/_git/repository + |- my-1st-module/ + |- catalog-info.yaml + | > apiVersion: backstage.io/v1alpha1 + | > kind: Component + | > metadata: + | > name: my-1st-module + | > annotations: + | > backstage.io/techdocs-ref: dir:. # -> same folder + | > spec: {} + |- docs/ + |- mkdocs.yml + |- my-2nd-module/ + |- catalog-info.yaml + | > apiVersion: backstage.io/v1alpha1 + | > kind: Component + | > metadata: + | > name: my-2nd-module + | > annotations: + | > backstage.io/techdocs-ref: dir:. # -> same folder + | > spec: {} + |- docs/ + |- mkdocs.yml + |- catalog-info.yaml + | > apiVersion: backstage.io/v1alpha1 + | > kind: Location + | > metadata: + | > name: example + | > spec: + | > targets: + | > - ./*/catalog-info.yaml + ``` + +### Patch Changes + +- 6e5aed1c9: Fix validation of mkdocs.yml docs_dir +- 250984333: Add link to https://backstage.io/docs/features/techdocs/configuration in the log warning message about updating techdocs.generate key. +- Updated dependencies + - @backstage/backend-common@0.8.7 + +## 0.6.8 + +### Patch Changes + +- d5eaab91d: Adds custom docker image support to the techdocs generator. This change adds a new `techdocs.generator` configuration key and deprecates the existing `techdocs.generators.techdocs` key. + + ```yaml + techdocs: + # recommended, going forward: + generator: + runIn: 'docker' # or 'local' + # New optional settings + dockerImage: my-org/techdocs # use a custom docker image + pullImage: false # disable automatic pulling of image (e.g. if custom docker login is required) + # legacy (deprecated): + generators: + techdocs: 'docker' # or 'local' + ``` + +- c18e8eb91: Provide optional `logger: Logger` and `logStream: Writable` arguments to the `GeneratorBase#run(...)` command. + They receive all log messages that are emitted during the generator run. +- ae84b20cf: Revert the upgrade to `fs-extra@10.0.0` as that seemed to have broken all installs inexplicably. +- Updated dependencies + - @backstage/backend-common@0.8.6 + +## 0.6.7 + +### Patch Changes + +- 683308ecf: Fix openStack swift publisher encoding issue. Remove utf8 forced encoding on binary files +- 6841e0113: fix minor version of git-url-parse as 11.5.x introduced a bug for Bitbucket Server +- Updated dependencies + - @backstage/integration@0.5.8 + - @backstage/catalog-model@0.9.0 + - @backstage/backend-common@0.8.5 + +## 0.6.6 + +### Patch Changes + +- ab5cc376f: Use new utilities from `@backstage/backend-common` for safely resolving child paths +- b47fc34bc: Update "service catalog" references to "software catalog" +- Updated dependencies + - @backstage/backend-common@0.8.4 + - @backstage/integration@0.5.7 + +## 0.6.5 + +### Patch Changes + +- c17c0fcf9: Adding additional checks on tech docs to prevent folder traversal via mkdocs.yml docs_dir value. +- Updated dependencies + - @backstage/catalog-model@0.8.4 + +## 0.6.4 + +### Patch Changes + +- aad98c544: Fixes multiple XSS and sanitization bypass vulnerabilities in TechDocs. +- 090594755: Support parsing `mkdocs.yml` files that are using custom yaml tags like + `!!python/name:materialx.emoji.twemoji`. +- Updated dependencies [ebe802bc4] +- Updated dependencies [49d7ec169] + - @backstage/catalog-model@0.8.1 + - @backstage/integration@0.5.5 + +## 0.6.3 + +### Patch Changes + +- 8cefadca0: Adding validation to mkdocs.yml parsing to prevent directory tree traversing +- Updated dependencies [0fd4ea443] +- Updated dependencies [add62a455] +- Updated dependencies [704875e26] + - @backstage/integration@0.5.4 + - @backstage/catalog-model@0.8.0 + +## 0.6.2 + +### Patch Changes + +- 65e6c4541: Remove circular dependencies +- Updated dependencies [f7f7783a3] +- Updated dependencies [c7dad9218] +- Updated dependencies [65e6c4541] +- Updated dependencies [68fdbf014] +- Updated dependencies [5001de908] + - @backstage/catalog-model@0.7.10 + - @backstage/backend-common@0.8.1 + - @backstage/integration@0.5.3 + +## 0.6.1 + +### Patch Changes + +- e04f1ccfb: Fixed a bug that prevented loading static assets from GCS, S3, Azure, and OpenStackSwift whose keys contain spaces or other special characters. +- Updated dependencies [22fd8ce2a] +- Updated dependencies [10c008a3a] +- Updated dependencies [f9fb4a205] +- Updated dependencies [16be1d093] + - @backstage/backend-common@0.8.0 + - @backstage/catalog-model@0.7.9 + +## 0.6.0 + +### Minor Changes + +- e0bfd3d44: Migrate the package to use the `ContainerRunner` interface instead of `runDockerContainer(…)`. + It also no longer provides the `ContainerRunner` as an input to the `GeneratorBase#run(…)` function, but expects it as a constructor parameter instead. + + If you use the `TechdocsGenerator` you need to update the usage: + + ```diff + + const containerRunner = new DockerContainerRunner({ dockerClient }); + + - const generator = new TechdocsGenerator(logger, config); + + const techdocsGenerator = new TechdocsGenerator({ + + logger, + + containerRunner, + + config, + + }); + + await this.generator.run({ + inputDir: preparedDir, + outputDir, + - dockerClient: this.dockerClient, + parsedLocationAnnotation, + etag: newEtag, + }); + ``` + +### Patch Changes + +- e9e56b01a: Adding optional config to enable S3-like API for tech-docs using s3ForcePathStyle option. + This allows providers like LocalStack, Minio and Wasabi (+possibly others) to be used to host tech docs. +- Updated dependencies [e0bfd3d44] +- Updated dependencies [38ca05168] +- Updated dependencies [d8b81fd28] + - @backstage/backend-common@0.7.0 + - @backstage/integration@0.5.2 + - @backstage/catalog-model@0.7.8 + - @backstage/config@0.1.5 + +## 0.5.1 + +### Patch Changes + +- f4af06ebe: Gracefully handle HTTP request failures in download method of AzureBlobStorage publisher. + +## 0.5.0 + +### Minor Changes + +- bc9d62f4f: Move the sanity checks of the publisher configurations to a dedicated `PublisherBase#getReadiness()` method instead of throwing an error when doing `Publisher.fromConfig(...)`. + You should include the check when your backend to get early feedback about a potential misconfiguration: + + ```diff + // packages/backend/src/plugins/techdocs.ts + + export default async function createPlugin({ + logger, + config, + discovery, + reader, + }: PluginEnvironment): Promise { + // ... + + const publisher = await Publisher.fromConfig(config, { + logger, + discovery, + }) + + + // checks if the publisher is working and logs the result + + await publisher.getReadiness(); + + // Docker client (conditionally) used by the generators, based on techdocs.generators config. + const dockerClient = new Docker(); + + // ... + } + ``` + + If you want to crash your application on invalid configurations, you can throw an `Error` to preserve the old behavior. + Please be aware that this is not the recommended for the use in a Backstage backend but might be helpful in CLI tools such as the `techdocs-cli`. + + ```ts + const publisher = await Publisher.fromConfig(config, { + logger, + discovery, + }); + + const ready = await publisher.getReadiness(); + if (!ready.isAvailable) { + throw new Error('Invalid TechDocs publisher configuration'); + } + ``` + +### Patch Changes + +- Updated dependencies [bb5055aee] +- Updated dependencies [5d0740563] + - @backstage/catalog-model@0.7.7 + +## 0.4.5 + +### Patch Changes + +- 8686eb38c: Use errors from `@backstage/errors` +- 424742dc1: Applies only if you use TechDocs local builder instead of building on CI/CD i.e. if `techdocs.builder` in your `app-config.yaml` is set to `'local'` + + Improvements + + 1. Do not check for updates in the repository if a check has been made in the last 60 seconds. This is to prevent the annoying check for update on every page switch or load. + 2. No need to maintain an in-memory etag storage, and use the one stored in `techdocs_metadata.json` file alongside generated docs. + + New feature + + 1. You can now use a mix of basic and recommended setup i.e. `techdocs.builder` is `'local'` but using an external cloud storage instead of local storage. Previously, in this setup, the docs would never get updated. + +- Updated dependencies [8686eb38c] +- Updated dependencies [0434853a5] +- Updated dependencies [8686eb38c] + - @backstage/backend-common@0.6.0 + - @backstage/config@0.1.4 + +## 0.4.4 + +### Patch Changes + +- d7245b733: Remove runDockerContainer, and start using the utility function provided by @backstage/backend-common +- 0b42fff22: Make use of parseLocationReference/stringifyLocationReference +- 2ef5bc7ea: Implement proper AWS Credentials precedence with assume-role and explicit credentials +- aa095e469: OpenStack Swift publisher added for tech-docs. +- bc46435f5: - Improve deprecation warning messaging in logs. + - Replace temp folder path from git provider domain(`source`) to full git host name (`resource`). (See: https://github.com/IonicaBizau/git-url-parse#giturlparseurl) +- a501128db: Refactor log messaging to improve clarity +- ca4a904f6: Add an optional configuration option for setting the url endpoint for AWS S3 publisher: `techdocs.publisher.awsS3.endpoint` +- Updated dependencies [277644e09] +- Updated dependencies [52f613030] +- Updated dependencies [d7245b733] +- Updated dependencies [0b42fff22] +- Updated dependencies [905cbfc96] +- Updated dependencies [761698831] +- Updated dependencies [d4e77ec5f] + - @backstage/integration@0.5.1 + - @backstage/backend-common@0.5.6 + - @backstage/catalog-model@0.7.4 + +## 0.4.3 + +### Patch Changes + +- f43192207: remove usage of res.send() for res.json() and res.end() to ensure content types are more consistently application/json on backend responses and error cases +- 61299519f: Remove read-store-upload loop when uploading S3 objects for TechDocs +- Updated dependencies [12d8f27a6] +- Updated dependencies [497859088] +- Updated dependencies [8adb48df4] + - @backstage/catalog-model@0.7.3 + - @backstage/backend-common@0.5.5 + +## 0.4.2 + +### Patch Changes + +- 2499f6cde: Add support for assuming role in AWS integrations +- 1e4ddd71d: Fix AWS, GCS and Azure publisher to work on Windows. +- Updated dependencies [bad21a085] +- Updated dependencies [a1f5e6545] + - @backstage/catalog-model@0.7.2 + - @backstage/config@0.1.3 + +## 0.4.1 + +### Patch Changes + +- fb28da212: Switched to using `'x-access-token'` for authenticating Git over HTTPS towards GitHub. +- 26e143e60: After TechDocs generate step, insert build timestamp to techdocs_metadata.json +- c6655413d: Improved error reporting in AzureBlobStorage to surface errors when fetching metadata and uploading files fails. +- 44414239f: Pass user and group ID when invoking docker container. When TechDocs invokes Docker, docker could be run as a `root` user which results in generation of files by applications run by non-root user (e.g. TechDocs) will not have access to modify. This PR passes in current user and group ID to docker so that the file permissions of the generated files and folders are correct. +- b0a41c707: Add etag of the prepared file tree to techdocs_metadata.json in the storage +- Updated dependencies [16fb1d03a] +- Updated dependencies [491f3a0ec] +- Updated dependencies [491f3a0ec] +- Updated dependencies [434b4e81a] +- Updated dependencies [fb28da212] + - @backstage/backend-common@0.5.4 + - @backstage/integration@0.5.0 + +## 0.4.0 + +### Minor Changes + +- 08142b256: URL Preparer will now use proper etag based caching introduced in https://github.com/backstage/backstage/pull/4120. Previously, builds used to be cached for 30 minutes. + +### Patch Changes + +- 77ad0003a: Revert AWS SDK version to v2 +- 08142b256: TechDocs will throw warning in backend logs when legacy git preparer or dir preparer is used to preparer docs. Migrate to URL Preparer by updating `backstage.io/techdocs-ref` annotation to be prefixed with `url:`. + Detailed docs are here https://backstage.io/docs/features/techdocs/how-to-guides#how-to-use-url-reader-in-techdocs-prepare-step + See benefits and reason for doing so https://github.com/backstage/backstage/issues/4409 +- Updated dependencies [ffffea8e6] +- Updated dependencies [82b2c11b6] +- Updated dependencies [965e200c6] +- Updated dependencies [ffffea8e6] +- Updated dependencies [5a5163519] + - @backstage/backend-common@0.5.3 + - @backstage/integration@0.4.0 + +## 0.3.7 + +### Patch Changes + +- c777df180: 1. Added option to use Azure Blob Storage as a choice to store the static generated files for TechDocs. +- e44925723: `techdocs.requestUrl` and `techdocs.storageUrl` are now optional configs and the discovery API will be used to get the URL where techdocs plugin is hosted. +- f0320190d: dir preparer will use URL Reader in its implementation. +- Updated dependencies [c4abcdb60] +- Updated dependencies [2430ee7c2] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [064c513e1] +- Updated dependencies [7881f2117] +- Updated dependencies [3149bfe63] +- Updated dependencies [2e62aea6f] +- Updated dependencies [11cb5ef94] + - @backstage/integration@0.3.2 + - @backstage/backend-common@0.5.2 + - @backstage/catalog-model@0.7.1 + +## 0.3.6 + +### Patch Changes + +- 9dd057662: Upgrade [git-url-parse](https://www.npmjs.com/package/git-url-parse) to [v11.4.4](https://github.com/IonicaBizau/git-url-parse/pull/125) which fixes parsing an Azure DevOps branch ref. +- db2328c88: Add rate limiter for concurrent execution of file uploads in AWS and Google publishers +- Updated dependencies [26a3a6cf0] +- Updated dependencies [664dd08c9] +- Updated dependencies [6800da78d] +- Updated dependencies [9dd057662] + - @backstage/backend-common@0.5.1 + - @backstage/integration@0.3.1 + +## 0.3.5 + +### Patch Changes + +- 53c9c51f2: TechDocs backend now streams files through from Google Cloud Storage to the browser, improving memory usage. +- a5e27d5c1: Create type for TechDocsMetadata (#3716) + + This change introduces a new type (TechDocsMetadata) in packages/techdocs-common. This type is then introduced in the endpoint response in techdocs-backend and in the api interface in techdocs (frontend). + +- Updated dependencies [def2307f3] +- Updated dependencies [0b135e7e0] +- Updated dependencies [294a70cab] +- Updated dependencies [fa8ba330a] +- Updated dependencies [0ea032763] +- Updated dependencies [5345a1f98] +- Updated dependencies [ed6baab66] +- Updated dependencies [09a370426] +- Updated dependencies [a93f42213] + - @backstage/catalog-model@0.7.0 + - @backstage/backend-common@0.5.0 + - @backstage/integration@0.3.0 + +## 0.3.4 + +### Patch Changes + +- a594a7257: @backstage/techdocs-common can now be imported in an environment without @backstage/plugin-techdocs-backend being installed. + +## 0.3.3 + +### Patch Changes + +- 68ad5af51: Improve techdocs-common Generator API for it to be used by techdocs-cli. TechDocs generator.run function now takes + an input AND an output directory. Most probably you use techdocs-common via plugin-techdocs-backend, and so there + is no breaking change for you. + But if you use techdocs-common separately, you need to create an output directory and pass into the generator. +- 371f67ecd: fix to-string breakage of binary files +- f1e74777a: Fix bug where binary files (`png`, etc.) could not load when using AWS or GCS publisher. +- dbe4450c3: Google Cloud authentication in TechDocs has been improved. + + 1. `techdocs.publisher.googleGcs.credentials` is now optional. If it is missing, `GOOGLE_APPLICATION_CREDENTIALS` + environment variable (and some other methods) will be used to authenticate. + Read more here https://cloud.google.com/docs/authentication/production + + 2. `techdocs.publisher.googleGcs.projectId` is no longer used. You can remove it from your `app-config.yaml`. + +- 5826d0973: AWS SDK version bump for TechDocs. +- b3b9445df: AWS S3 authentication in TechDocs has been improved. + + 1. `techdocs.publisher.awsS3.bucketName` is now the only required config. `techdocs.publisher.awsS3.credentials` and `techdocs.publisher.awsS3.region` are optional. + + 2. If `techdocs.publisher.awsS3.credentials` and `techdocs.publisher.awsS3.region` are missing, the AWS environment variables `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION` will be used. There are more better ways of setting up AWS authentication. Read the guide at https://backstage.io/docs/features/techdocs/using-cloud-storage + +- Updated dependencies [466354aaa] +- Updated dependencies [f3b064e1c] +- Updated dependencies [abbee6fff] +- Updated dependencies [147fadcb9] + - @backstage/integration@0.2.0 + - @backstage/catalog-model@0.6.1 + - @backstage/backend-common@0.4.3 + +## 0.3.2 + +### Patch Changes + +- 7ec525481: 1. Added option to use AWS S3 as a choice to store the static generated files for TechDocs. +- f8ba88ded: Fix for `integration.github.apiBaseUrl` configuration not properly overriding apiBaseUrl used by techdocs +- 00042e73c: Moving the Git actions to isomorphic-git instead of the node binding version of nodegit +- Updated dependencies [5ecd50f8a] +- Updated dependencies [00042e73c] +- Updated dependencies [0829ff126] +- Updated dependencies [036a84373] + - @backstage/backend-common@0.4.2 + - @backstage/integration@0.1.5 + +## 0.3.1 + +### Patch Changes + +- 8804e8981: Using @backstage/integration package for GitHub/GitLab/Azure tokens and request options. + + Most probably you do not have to make any changes in the app because of this change. + However, if you are using the `DirectoryPreparer` or `CommonGitPreparer` exported by + `@backstage/techdocs-common` package, you now need to add pass in a `config` (from `@backstage/config`) + instance as argument. + + ``` + + const directoryPreparer = new DirectoryPreparer(logger); + const commonGitPreparer = new CommonGitPreparer(logger); + + const directoryPreparer = new DirectoryPreparer(config, logger); + const commonGitPreparer = new CommonGitPreparer(config, logger); + ``` + +## 0.3.0 + +### Minor Changes + +- a8573e53b: techdocs-backend: Simplified file, removing individual preparers and generators. + techdocs-backend: UrlReader is now available to use in preparers. + + In your Backstage app, `packages/backend/plugins/techdocs.ts` file has now been simplified, + to remove registering individual preparers and generators. + + Please update the file when upgrading the version of `@backstage/plugin-techdocs-backend` package. + + ```typescript + const preparers = await Preparers.fromConfig(config, { + logger, + reader, + }); + + const generators = await Generators.fromConfig(config, { + logger, + }); + + const publisher = await Publisher.fromConfig(config, { + logger, + discovery, + }); + ``` + + You should be able to remove unnecessary imports, and just do + + ```typescript + import { + createRouter, + Preparers, + Generators, + Publisher, + } from '@backstage/plugin-techdocs-backend'; + ``` + +## 0.2.0 + +### Minor Changes + +- dae4f3983: _Breaking changes_ + + 1. Added option to use Google Cloud Storage as a choice to store the static generated files for TechDocs. + It can be configured using `techdocs.publisher.type` option in `app-config.yaml`. + Step-by-step guide to configure GCS is available here https://backstage.io/docs/features/techdocs/using-cloud-storage + Set `techdocs.publisher.type` to `'local'` if you want to continue using local filesystem to store TechDocs files. + + 2. `techdocs.builder` is now required and can be set to `'local'` or `'external'`. (Set it to `'local'` for now, since CI/CD build + workflow for TechDocs will be available soon (in few weeks)). + If builder is set to 'local' and you open a TechDocs page, `techdocs-backend` will try to generate the docs, publish to storage and + show the generated docs afterwords. + If builder is set to `'external'`, `techdocs-backend` will only fetch the docs and will NOT try to generate and publish. In this case of `'external'`, + we assume that docs are being built in the CI/CD pipeline of the repository. + TechDocs will not assume a default value for `techdocs.builder`. It is better to explicitly define it in the `app-config.yaml`. + + 3. When configuring TechDocs in your backend, there is a difference in how a new publisher is created. + + ``` + --- const publisher = new LocalPublish(logger, discovery); + +++ const publisher = Publisher.fromConfig(config, logger, discovery); + ``` + + Based on the config `techdocs.publisher.type`, the publisher could be either Local publisher or Google Cloud Storage publisher. + + 4. `techdocs.storageUrl` is now a required config. Should be `http://localhost:7007/api/techdocs/static/docs` in most setups. + + 5. Parts of `@backstage/plugin-techdocs-backend` have been moved to a new package `@backstage/techdocs-common` to generate docs. Also to publish docs + to-and-fro between TechDocs and a storage (either local or external). However, a Backstage app does NOT need to import the `techdocs-common` package - + app should only import `@backstage/plugin-techdocs` and `@backstage/plugin-techdocs-backend`. + + _Patch changes_ + + 1. See all of TechDocs config options and its documentation https://backstage.io/docs/features/techdocs/configuration + + 2. Logic about serving static files and metadata retrieval have been abstracted away from the router in `techdocs-backend` to the instance of publisher. + + 3. Removed Material UI Spinner from TechDocs header. Spinners cause unnecessary UX distraction. + Case 1 (when docs are built and are to be served): Spinners appear for a split second before the name of site shows up. This unnecessarily distracts eyes because spinners increase the size of the Header. A dot (.) would do fine. Definitely more can be done. + Case 2 (when docs are being generated): There is already a linear progress bar (which is recommended in Storybook). + +### Patch Changes + +- Updated dependencies [c911061b7] +- Updated dependencies [1d1c2860f] +- Updated dependencies [0e6298f7e] +- Updated dependencies [4eafdec4a] +- Updated dependencies [ac3560b42] + - @backstage/catalog-model@0.6.0 + - @backstage/backend-common@0.4.1 diff --git a/packages/techdocs-common/README.md b/packages/techdocs-common/README.md new file mode 100644 index 0000000000..452b09b60b --- /dev/null +++ b/packages/techdocs-common/README.md @@ -0,0 +1,53 @@ +# @backstage/techdocs-common + +**WARNING**: This package is moving to `@backstage/plugin-techdocs-node`. +Please update any dependencies you may have, as this package will no longer be +published or updated in the near future. + +Common node.js functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli + +This package is used by `techdocs-backend` to serve docs from different types of publishers (Google GCS, Local, etc.). +It is also used to build docs and publish them to storage, by both `techdocs-backend` and `techdocs-cli`. + +## Usage + +Create a preparer instance from the [preparers available](/plugins/techdocs-node/src/stages/prepare) at which takes an Entity instance. +Run the [docs generator](/plugins/techdocs-node/src/stages/generate) on the prepared directory. +Publish the generated directory files to a [storage](/plugins/techdocs-node/src/stages/publish) of your choice. + +Example: + +```js +async () => { + const preparedDir = await preparer.prepare(entity); + + const parsedLocationAnnotation = getLocationForEntity(entity); + const { resultDir } = await generator.run({ + directory: preparedDir, + dockerClient: dockerClient, + parsedLocationAnnotation, + }); + + await publisher.publish({ + entity: entity, + directory: resultDir, + }); +}; +``` + +## Features + +Currently the build process is split up in these three stages. + +- Preparers +- Generators +- Publishers + +Preparers read your entity data and creates a working directory with your documentation source code. For example if you have set your `backstage.io/techdocs-ref` to `url:https://github.com/backstage/backstage.git` it will clone that repository to a temp folder and pass that on to the generator. + +Generators takes the prepared source and runs the `techdocs-container` on it. It then passes on the output folder of that build to the publisher. + +Publishers gets a folder path from the generator and publish it to your storage solution. Read documentation to know more about configuring storage solutions. +http://backstage.io/docs/features/techdocs/configuration + +Any of these can be extended. We want to extend our support to most of the storage providers (Publishers) and source code host providers (Preparers). diff --git a/packages/techdocs-common/api-report.md b/packages/techdocs-common/api-report.md new file mode 100644 index 0000000000..62851bbd68 --- /dev/null +++ b/packages/techdocs-common/api-report.md @@ -0,0 +1,9 @@ +## API Report File for "@backstage/techdocs-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +export * from '@backstage/plugin-techdocs-node'; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json new file mode 100644 index 0000000000..1d37974f29 --- /dev/null +++ b/packages/techdocs-common/package.json @@ -0,0 +1,51 @@ +{ + "name": "@backstage/techdocs-common", + "description": "Common node.js functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", + "version": "0.11.11", + "main": "src/index.ts", + "types": "src/index.ts", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "node-library" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/techdocs-common" + }, + "keywords": [ + "techdocs", + "backstage" + ], + "license": "Apache-2.0", + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" + }, + "bugs": { + "url": "https://github.com/backstage/backstage/issues" + }, + "dependencies": { + "@backstage/plugin-techdocs-node": "^0.11.11" + }, + "devDependencies": {}, + "jest": { + "roots": [ + ".." + ] + } +} diff --git a/packages/techdocs-common/src/index.ts b/packages/techdocs-common/src/index.ts new file mode 100644 index 0000000000..a881eef5ea --- /dev/null +++ b/packages/techdocs-common/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 '@backstage/plugin-techdocs-node'; From 6921edc6a04798bdeafb4afe9f928c3083fb7f80 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 3 Mar 2022 16:14:32 +0100 Subject: [PATCH 225/353] Move packages/search-common -> plugins/search-common Signed-off-by: Eric Peterson --- {packages => plugins}/search-common/.eslintrc.js | 0 {packages => plugins}/search-common/CHANGELOG.md | 0 {packages => plugins}/search-common/README.md | 0 {packages => plugins}/search-common/api-report.md | 0 {packages => plugins}/search-common/package.json | 0 {packages => plugins}/search-common/src/index.test.ts | 0 {packages => plugins}/search-common/src/index.ts | 0 {packages => plugins}/search-common/src/types.ts | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename {packages => plugins}/search-common/.eslintrc.js (100%) rename {packages => plugins}/search-common/CHANGELOG.md (100%) rename {packages => plugins}/search-common/README.md (100%) rename {packages => plugins}/search-common/api-report.md (100%) rename {packages => plugins}/search-common/package.json (100%) rename {packages => plugins}/search-common/src/index.test.ts (100%) rename {packages => plugins}/search-common/src/index.ts (100%) rename {packages => plugins}/search-common/src/types.ts (100%) diff --git a/packages/search-common/.eslintrc.js b/plugins/search-common/.eslintrc.js similarity index 100% rename from packages/search-common/.eslintrc.js rename to plugins/search-common/.eslintrc.js diff --git a/packages/search-common/CHANGELOG.md b/plugins/search-common/CHANGELOG.md similarity index 100% rename from packages/search-common/CHANGELOG.md rename to plugins/search-common/CHANGELOG.md diff --git a/packages/search-common/README.md b/plugins/search-common/README.md similarity index 100% rename from packages/search-common/README.md rename to plugins/search-common/README.md diff --git a/packages/search-common/api-report.md b/plugins/search-common/api-report.md similarity index 100% rename from packages/search-common/api-report.md rename to plugins/search-common/api-report.md diff --git a/packages/search-common/package.json b/plugins/search-common/package.json similarity index 100% rename from packages/search-common/package.json rename to plugins/search-common/package.json diff --git a/packages/search-common/src/index.test.ts b/plugins/search-common/src/index.test.ts similarity index 100% rename from packages/search-common/src/index.test.ts rename to plugins/search-common/src/index.test.ts diff --git a/packages/search-common/src/index.ts b/plugins/search-common/src/index.ts similarity index 100% rename from packages/search-common/src/index.ts rename to plugins/search-common/src/index.ts diff --git a/packages/search-common/src/types.ts b/plugins/search-common/src/types.ts similarity index 100% rename from packages/search-common/src/types.ts rename to plugins/search-common/src/types.ts From 3e54f6c436dba323a468b08811d41a15172d63e5 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 3 Mar 2022 17:02:37 +0100 Subject: [PATCH 226/353] Use @backstage/plugin-search-common instead of @backstage/search-common Signed-off-by: Eric Peterson --- .changeset/search-wellington-paranormal.md | 13 +++++++++++++ docs/features/search/README.md | 2 +- docs/features/search/how-to-guides.md | 2 +- packages/app/package.json | 2 +- plugins/catalog-backend/api-report.md | 2 +- plugins/catalog-backend/package.json | 2 +- .../src/search/DefaultCatalogCollatorFactory.ts | 2 +- plugins/catalog/api-report.md | 2 +- plugins/catalog/package.json | 2 +- .../CatalogSearchResultListItem.tsx | 2 +- .../api-report.md | 8 ++++---- .../package.json | 2 +- .../src/engines/ElasticSearchSearchEngine.ts | 2 +- .../engines/ElasticSearchSearchEngineIndexer.ts | 2 +- plugins/search-backend-module-pg/api-report.md | 6 +++--- plugins/search-backend-module-pg/package.json | 2 +- .../src/PgSearchEngine/PgSearchEngine.ts | 2 +- .../src/PgSearchEngine/PgSearchEngineIndexer.ts | 2 +- .../src/database/DatabaseDocumentStore.test.ts | 2 +- .../src/database/DatabaseDocumentStore.ts | 2 +- .../src/database/types.ts | 2 +- plugins/search-backend-node/api-report.md | 16 ++++++++-------- plugins/search-backend-node/package.json | 2 +- .../search-backend-node/src/IndexBuilder.test.ts | 2 +- plugins/search-backend-node/src/IndexBuilder.ts | 2 +- .../src/engines/LunrSearchEngine.test.ts | 5 ++++- .../src/engines/LunrSearchEngine.ts | 2 +- .../src/engines/LunrSearchEngineIndexer.ts | 2 +- plugins/search-backend-node/src/index.ts | 4 ++-- .../indexing/BatchSearchEngineIndexer.test.ts | 2 +- .../src/indexing/BatchSearchEngineIndexer.ts | 2 +- .../src/indexing/DecoratorBase.test.ts | 2 +- .../src/indexing/DecoratorBase.ts | 2 +- .../src/test-utils/TestPipeline.ts | 2 +- plugins/search-backend-node/src/types.ts | 2 +- plugins/search-backend/api-report.md | 2 +- plugins/search-backend/package.json | 2 +- .../src/service/AuthorizedSearchEngine.test.ts | 2 +- .../src/service/AuthorizedSearchEngine.ts | 2 +- plugins/search-backend/src/service/router.ts | 5 ++++- plugins/search-common/CHANGELOG.md | 2 +- plugins/search-common/README.md | 2 +- plugins/search-common/api-report.md | 2 +- plugins/search-common/package.json | 4 ++-- plugins/search/api-report.md | 8 ++++---- plugins/search/package.json | 2 +- plugins/search/src/apis.ts | 2 +- .../DefaultResultListItem.tsx | 2 +- .../components/SearchContext/SearchContext.tsx | 2 +- .../SearchContextForStorybook.stories.tsx | 2 +- .../src/components/SearchResult/SearchResult.tsx | 2 +- plugins/techdocs-backend/api-report.md | 2 +- plugins/techdocs-backend/package.json | 2 +- .../src/search/DefaultTechDocsCollatorFactory.ts | 2 +- plugins/techdocs-node/api-report.md | 2 +- plugins/techdocs-node/package.json | 2 +- plugins/techdocs-node/src/techdocsTypes.ts | 2 +- scripts/api-extractor.ts | 1 + yarn.lock | 2 +- 59 files changed, 94 insertions(+), 74 deletions(-) create mode 100644 .changeset/search-wellington-paranormal.md diff --git a/.changeset/search-wellington-paranormal.md b/.changeset/search-wellington-paranormal.md new file mode 100644 index 0000000000..7cd1ead2e8 --- /dev/null +++ b/.changeset/search-wellington-paranormal.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-search-backend-module-elasticsearch': patch +'@backstage/plugin-search-backend-module-pg': patch +'@backstage/plugin-search-backend-node': patch +'@backstage/plugin-search-backend': patch +'@backstage/plugin-search': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-techdocs-node': patch +--- + +Use `@backstage/plugin-search-common` package instead of `@backstage/search-common`. diff --git a/docs/features/search/README.md b/docs/features/search/README.md index 0856451882..f5533927fa 100644 --- a/docs/features/search/README.md +++ b/docs/features/search/README.md @@ -128,7 +128,7 @@ plugins integrated to search. | Frontend Plugin | @backstage/plugin-search | | Backend Plugin | @backstage/plugin-search-backend | | Indexer Plugin | @backstage/plugin-search-backend-node | -| Common Code | @backstage/search-common | +| Common Code | @backstage/plugin-search-common | ## Get Involved diff --git a/docs/features/search/how-to-guides.md b/docs/features/search/how-to-guides.md index ac36d64673..f1d06f38f5 100644 --- a/docs/features/search/how-to-guides.md +++ b/docs/features/search/how-to-guides.md @@ -365,7 +365,7 @@ implement the factory method that instantiates the stream: ```ts import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; -import { SearchEngine } from '@backstage/search-common'; +import { SearchEngine } from '@backstage/plugin-search-common'; export class YourSearchEngineIndexer extends BatchSearchEngineIndexer { constructor({ type }: { type: string }) { // Customize the number of documents passed to the index method per batch. diff --git a/packages/app/package.json b/packages/app/package.json index d272b88e52..45cde6df52 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -46,13 +46,13 @@ "@backstage/plugin-rollbar": "^0.4.1", "@backstage/plugin-scaffolder": "^0.14.0", "@backstage/plugin-search": "^0.7.2", + "@backstage/plugin-search-common": "^0.3.0", "@backstage/plugin-sentry": "^0.3.39", "@backstage/plugin-shortcuts": "^0.2.2", "@backstage/plugin-tech-radar": "^0.5.8", "@backstage/plugin-techdocs": "^0.15.0", "@backstage/plugin-todo": "^0.2.3", "@backstage/plugin-user-settings": "^0.4.0", - "@backstage/search-common": "^0.3.0", "@backstage/plugin-tech-insights": "^0.1.11", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 0d9f306009..09f3e4b9d9 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -12,7 +12,7 @@ import { CompoundEntityRef } from '@backstage/catalog-model'; import { ConditionalPolicyDecision } from '@backstage/plugin-permission-node'; import { Conditions } from '@backstage/plugin-permission-node'; import { Config } from '@backstage/config'; -import { DocumentCollatorFactory } from '@backstage/search-common'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { Entity } from '@backstage/catalog-model'; import { EntityPolicy } from '@backstage/catalog-model'; import express from 'express'; diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index a1404189f8..9b8e6347a5 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -44,7 +44,7 @@ "@backstage/plugin-permission-common": "^0.5.2", "@backstage/plugin-permission-node": "^0.5.3", "@backstage/plugin-scaffolder-common": "^0.2.3", - "@backstage/search-common": "^0.3.0", + "@backstage/plugin-search-common": "^0.3.0", "@backstage/types": "^0.1.3", "@octokit/graphql": "^4.5.8", "@types/express": "^4.17.6", diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts index 440a8c8ebb..4d28559c82 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts @@ -29,7 +29,7 @@ import { UserEntity, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { DocumentCollatorFactory } from '@backstage/search-common'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { catalogEntityReadPermission, CatalogEntityDocument, diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 97a36ad020..4dd5e7c32e 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -11,7 +11,7 @@ import { CompoundEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { InfoCardVariants } from '@backstage/core-components'; import { Observable } from '@backstage/types'; import { Overrides } from '@material-ui/core/styles/overrides'; diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 0a8a4af58c..07477316fa 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -42,7 +42,7 @@ "@backstage/integration-react": "^0.1.24", "@backstage/plugin-catalog-common": "^0.2.0", "@backstage/plugin-catalog-react": "^0.8.0", - "@backstage/search-common": "^0.3.0", + "@backstage/plugin-search-common": "^0.3.0", "@backstage/theme": "^0.2.15", "@backstage/types": "^0.1.2", "@material-ui/core": "^4.12.2", diff --git a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx index 54cfe43364..422841b134 100644 --- a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx +++ b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx @@ -24,7 +24,7 @@ import { makeStyles, } from '@material-ui/core'; import { Link } from '@backstage/core-components'; -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; const useStyles = makeStyles({ flexContainer: { diff --git a/plugins/search-backend-module-elasticsearch/api-report.md b/plugins/search-backend-module-elasticsearch/api-report.md index d4edcc6b25..50e5a61a14 100644 --- a/plugins/search-backend-module-elasticsearch/api-report.md +++ b/plugins/search-backend-module-elasticsearch/api-report.md @@ -9,11 +9,11 @@ import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node' import { Client } from '@elastic/elasticsearch'; import { Config } from '@backstage/config'; import type { ConnectionOptions } from 'tls'; -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { Logger as Logger_2 } from 'winston'; -import { SearchEngine } from '@backstage/search-common'; -import { SearchQuery } from '@backstage/search-common'; -import { SearchResultSet } from '@backstage/search-common'; +import { SearchEngine } from '@backstage/plugin-search-common'; +import { SearchQuery } from '@backstage/plugin-search-common'; +import { SearchResultSet } from '@backstage/plugin-search-common'; // Warning: (ae-missing-release-tag) "ElasticSearchClientOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-search-backend-module-elasticsearch" does not have an export "ElasticSearchEngine" diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index f49c86f41f..b5d20038e8 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -24,8 +24,8 @@ }, "dependencies": { "@backstage/config": "^0.1.15", - "@backstage/search-common": "^0.3.0", "@backstage/plugin-search-backend-node": "^0.5.0", + "@backstage/plugin-search-common": "^0.3.0", "@elastic/elasticsearch": "7.13.0", "@acuris/aws-es-connection": "^2.2.0", "aws-sdk": "^2.948.0", diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index c16567fee0..f2ed5ed2b0 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -24,7 +24,7 @@ import { SearchEngine, SearchQuery, SearchResultSet, -} from '@backstage/search-common'; +} from '@backstage/plugin-search-common'; import { Client } from '@elastic/elasticsearch'; import esb from 'elastic-builder'; import { isEmpty, isNaN as nan, isNumber } from 'lodash'; diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts index 2e4996cd2f..cb3674ddd1 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.ts @@ -15,7 +15,7 @@ */ import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { Client } from '@elastic/elasticsearch'; import { Readable } from 'stream'; import { Logger } from 'winston'; diff --git a/plugins/search-backend-module-pg/api-report.md b/plugins/search-backend-module-pg/api-report.md index ba8fde5f17..39847a4935 100644 --- a/plugins/search-backend-module-pg/api-report.md +++ b/plugins/search-backend-module-pg/api-report.md @@ -4,12 +4,12 @@ ```ts import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { Knex } from 'knex'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { SearchEngine } from '@backstage/plugin-search-backend-node'; -import { SearchQuery } from '@backstage/search-common'; -import { SearchResultSet } from '@backstage/search-common'; +import { SearchQuery } from '@backstage/plugin-search-common'; +import { SearchResultSet } from '@backstage/plugin-search-common'; // Warning: (ae-missing-release-tag) "ConcretePgSearchQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index fb95dff540..505bb53d09 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -24,8 +24,8 @@ }, "dependencies": { "@backstage/backend-common": "^0.12.0", - "@backstage/search-common": "^0.3.0", "@backstage/plugin-search-backend-node": "^0.5.0", + "@backstage/plugin-search-common": "^0.3.0", "lodash": "^4.17.21", "knex": "^1.0.2" }, diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts index ac42007401..8f75112837 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts @@ -15,7 +15,7 @@ */ import { PluginDatabaseManager } from '@backstage/backend-common'; import { SearchEngine } from '@backstage/plugin-search-backend-node'; -import { SearchQuery, SearchResultSet } from '@backstage/search-common'; +import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common'; import { PgSearchEngineIndexer } from './PgSearchEngineIndexer'; import { DatabaseDocumentStore, diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts index 53d040cb87..fa27509a69 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.ts @@ -15,7 +15,7 @@ */ import { BatchSearchEngineIndexer } from '@backstage/plugin-search-backend-node'; -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { Knex } from 'knex'; import { DatabaseStore } from '../database'; diff --git a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.test.ts b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.test.ts index 71c012ec00..b973c2e14f 100644 --- a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.test.ts +++ b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { DatabaseDocumentStore } from './DatabaseDocumentStore'; describe('DatabaseDocumentStore', () => { diff --git a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts index 0d3ee63ca4..33b2d0c0d6 100644 --- a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts +++ b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { resolvePackagePath } from '@backstage/backend-common'; -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { Knex } from 'knex'; import { DatabaseStore, diff --git a/plugins/search-backend-module-pg/src/database/types.ts b/plugins/search-backend-module-pg/src/database/types.ts index 0a0dc35682..7e87658745 100644 --- a/plugins/search-backend-module-pg/src/database/types.ts +++ b/plugins/search-backend-module-pg/src/database/types.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { Knex } from 'knex'; export interface PgSearchQuery { diff --git a/plugins/search-backend-node/api-report.md b/plugins/search-backend-node/api-report.md index 37945c8902..44e2d0c92d 100644 --- a/plugins/search-backend-node/api-report.md +++ b/plugins/search-backend-node/api-report.md @@ -5,17 +5,17 @@ ```ts /// -import { DocumentCollatorFactory } from '@backstage/search-common'; -import { DocumentDecoratorFactory } from '@backstage/search-common'; -import { DocumentTypeInfo } from '@backstage/search-common'; -import { IndexableDocument } from '@backstage/search-common'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; +import { DocumentDecoratorFactory } from '@backstage/plugin-search-common'; +import { DocumentTypeInfo } from '@backstage/plugin-search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { Logger as Logger_2 } from 'winston'; import { default as lunr_2 } from 'lunr'; -import { QueryTranslator } from '@backstage/search-common'; +import { QueryTranslator } from '@backstage/plugin-search-common'; import { Readable } from 'stream'; -import { SearchEngine } from '@backstage/search-common'; -import { SearchQuery } from '@backstage/search-common'; -import { SearchResultSet } from '@backstage/search-common'; +import { SearchEngine } from '@backstage/plugin-search-common'; +import { SearchQuery } from '@backstage/plugin-search-common'; +import { SearchResultSet } from '@backstage/plugin-search-common'; import { Transform } from 'stream'; import { Writable } from 'stream'; diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index dc8d1046b1..793bfdf74a 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@backstage/errors": "^0.2.2", - "@backstage/search-common": "^0.3.0", + "@backstage/plugin-search-common": "^0.3.0", "@types/lunr": "^2.3.3", "lodash": "^4.17.21", "lunr": "^2.3.9", diff --git a/plugins/search-backend-node/src/IndexBuilder.test.ts b/plugins/search-backend-node/src/IndexBuilder.test.ts index 95a2e1d439..6a32343205 100644 --- a/plugins/search-backend-node/src/IndexBuilder.test.ts +++ b/plugins/search-backend-node/src/IndexBuilder.test.ts @@ -18,7 +18,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import { DocumentCollatorFactory, DocumentDecoratorFactory, -} from '@backstage/search-common'; +} from '@backstage/plugin-search-common'; import { Readable, Transform } from 'stream'; import { IndexBuilder } from './IndexBuilder'; import { LunrSearchEngine, SearchEngine } from './index'; diff --git a/plugins/search-backend-node/src/IndexBuilder.ts b/plugins/search-backend-node/src/IndexBuilder.ts index e0a059e303..6be4489127 100644 --- a/plugins/search-backend-node/src/IndexBuilder.ts +++ b/plugins/search-backend-node/src/IndexBuilder.ts @@ -19,7 +19,7 @@ import { DocumentDecoratorFactory, DocumentTypeInfo, SearchEngine, -} from '@backstage/search-common'; +} from '@backstage/plugin-search-common'; import { Transform, pipeline } from 'stream'; import { Logger } from 'winston'; import { Scheduler } from './index'; diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts index 88a3ba9a24..46aaf47c60 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts @@ -16,7 +16,10 @@ import { getVoidLogger } from '@backstage/backend-common'; import lunr from 'lunr'; -import { IndexableDocument, SearchEngine } from '@backstage/search-common'; +import { + IndexableDocument, + SearchEngine, +} from '@backstage/plugin-search-common'; import { ConcreteLunrQuery, LunrSearchEngine, diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index 394cfffac8..b642647466 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -20,7 +20,7 @@ import { SearchResultSet, QueryTranslator, SearchEngine, -} from '@backstage/search-common'; +} from '@backstage/plugin-search-common'; import lunr from 'lunr'; import { Logger } from 'winston'; import { LunrSearchEngineIndexer } from './LunrSearchEngineIndexer'; diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.ts b/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.ts index 01c95f07d4..49afafea64 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import lunr from 'lunr'; import { BatchSearchEngineIndexer } from '../indexing'; diff --git a/plugins/search-backend-node/src/index.ts b/plugins/search-backend-node/src/index.ts index 2edab1b01d..e84c5d6a4d 100644 --- a/plugins/search-backend-node/src/index.ts +++ b/plugins/search-backend-node/src/index.ts @@ -37,6 +37,6 @@ export * from './indexing'; export * from './test-utils'; /** - * @deprecated Import from @backstage/search-common instead + * @deprecated Import from @backstage/plugin-search-common instead */ -export type { SearchEngine } from '@backstage/search-common'; +export type { SearchEngine } from '@backstage/plugin-search-common'; diff --git a/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.test.ts b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.test.ts index b692ce3aff..b4c9ff9e46 100644 --- a/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.test.ts +++ b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { BatchSearchEngineIndexer } from './BatchSearchEngineIndexer'; import { TestPipeline } from '../test-utils'; diff --git a/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts index b368dd8184..6070a577ed 100644 --- a/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts +++ b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts @@ -15,7 +15,7 @@ */ import { assertError } from '@backstage/errors'; -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { Writable } from 'stream'; /** diff --git a/plugins/search-backend-node/src/indexing/DecoratorBase.test.ts b/plugins/search-backend-node/src/indexing/DecoratorBase.test.ts index 3b045dcddf..12c61bd7d3 100644 --- a/plugins/search-backend-node/src/indexing/DecoratorBase.test.ts +++ b/plugins/search-backend-node/src/indexing/DecoratorBase.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { DecoratorBase } from './DecoratorBase'; import { TestPipeline } from '../test-utils'; diff --git a/plugins/search-backend-node/src/indexing/DecoratorBase.ts b/plugins/search-backend-node/src/indexing/DecoratorBase.ts index 382ce443e3..0541ab82d7 100644 --- a/plugins/search-backend-node/src/indexing/DecoratorBase.ts +++ b/plugins/search-backend-node/src/indexing/DecoratorBase.ts @@ -15,7 +15,7 @@ */ import { assertError } from '@backstage/errors'; -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { Transform } from 'stream'; /** diff --git a/plugins/search-backend-node/src/test-utils/TestPipeline.ts b/plugins/search-backend-node/src/test-utils/TestPipeline.ts index c0c2bf5f89..162572d929 100644 --- a/plugins/search-backend-node/src/test-utils/TestPipeline.ts +++ b/plugins/search-backend-node/src/test-utils/TestPipeline.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { pipeline, Readable, Transform, Writable } from 'stream'; /** diff --git a/plugins/search-backend-node/src/types.ts b/plugins/search-backend-node/src/types.ts index 68ccfe4c7e..d21bfaf453 100644 --- a/plugins/search-backend-node/src/types.ts +++ b/plugins/search-backend-node/src/types.ts @@ -18,7 +18,7 @@ import { DocumentCollatorFactory, DocumentDecoratorFactory, SearchEngine, -} from '@backstage/search-common'; +} from '@backstage/plugin-search-common'; import { Logger } from 'winston'; /** diff --git a/plugins/search-backend/api-report.md b/plugins/search-backend/api-report.md index 796f54c09d..d9d408395f 100644 --- a/plugins/search-backend/api-report.md +++ b/plugins/search-backend/api-report.md @@ -4,7 +4,7 @@ ```ts import { Config } from '@backstage/config'; -import { DocumentTypeInfo } from '@backstage/search-common'; +import { DocumentTypeInfo } from '@backstage/plugin-search-common'; import express from 'express'; import { Logger as Logger_2 } from 'winston'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index 2733be841e..c42406686c 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -26,11 +26,11 @@ "@backstage/backend-common": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/search-common": "^0.3.0", "@backstage/plugin-auth-node": "^0.1.4", "@backstage/plugin-permission-common": "^0.5.2", "@backstage/plugin-permission-node": "^0.5.3", "@backstage/plugin-search-backend-node": "^0.5.0", + "@backstage/plugin-search-common": "^0.3.0", "@backstage/types": "^0.1.3", "@types/express": "^4.17.6", "dataloader": "^2.0.0", diff --git a/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts b/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts index 137ffc24f1..ba63f8484e 100644 --- a/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts +++ b/plugins/search-backend/src/service/AuthorizedSearchEngine.test.ts @@ -24,7 +24,7 @@ import { DocumentTypeInfo, IndexableDocument, SearchEngine, -} from '@backstage/search-common'; +} from '@backstage/plugin-search-common'; import { encodePageCursor, decodePageCursor, diff --git a/plugins/search-backend/src/service/AuthorizedSearchEngine.ts b/plugins/search-backend/src/service/AuthorizedSearchEngine.ts index 82ed061196..2e65e18a84 100644 --- a/plugins/search-backend/src/service/AuthorizedSearchEngine.ts +++ b/plugins/search-backend/src/service/AuthorizedSearchEngine.ts @@ -31,7 +31,7 @@ import { SearchQuery, SearchResult, SearchResultSet, -} from '@backstage/search-common'; +} from '@backstage/plugin-search-common'; import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; import { Writable } from 'stream'; diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index 3f25c4f24a..92e4e5526f 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -24,7 +24,10 @@ import { Config } from '@backstage/config'; import { JsonObject, JsonValue } from '@backstage/types'; import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; -import { DocumentTypeInfo, SearchResultSet } from '@backstage/search-common'; +import { + DocumentTypeInfo, + SearchResultSet, +} from '@backstage/plugin-search-common'; import { SearchEngine } from '@backstage/plugin-search-backend-node'; import { AuthorizedSearchEngine } from './AuthorizedSearchEngine'; diff --git a/plugins/search-common/CHANGELOG.md b/plugins/search-common/CHANGELOG.md index 112d3d583a..d4f12f6146 100644 --- a/plugins/search-common/CHANGELOG.md +++ b/plugins/search-common/CHANGELOG.md @@ -1,4 +1,4 @@ -# @backstage/search-common +# @backstage/plugin-search-common ## 0.3.0 diff --git a/plugins/search-common/README.md b/plugins/search-common/README.md index 66828cc7af..7299c758d4 100644 --- a/plugins/search-common/README.md +++ b/plugins/search-common/README.md @@ -1,3 +1,3 @@ -# @backstage/search-common +# @backstage/plugin-search-common Common functionalities for Search, to be shared between various search-enabled plugins. diff --git a/plugins/search-common/api-report.md b/plugins/search-common/api-report.md index 475d85e11c..8ab25d5e42 100644 --- a/plugins/search-common/api-report.md +++ b/plugins/search-common/api-report.md @@ -1,4 +1,4 @@ -## API Report File for "@backstage/search-common" +## API Report File for "@backstage/plugin-search-common" > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). diff --git a/plugins/search-common/package.json b/plugins/search-common/package.json index 0e4482e377..98c02e5cc6 100644 --- a/plugins/search-common/package.json +++ b/plugins/search-common/package.json @@ -1,5 +1,5 @@ { - "name": "@backstage/search-common", + "name": "@backstage/plugin-search-common", "description": "Common functionalities for Search, to be shared between various search-enabled plugins", "version": "0.3.0", "main": "src/index.ts", @@ -17,7 +17,7 @@ "repository": { "type": "git", "url": "https://github.com/backstage/backstage", - "directory": "packages/search-common" + "directory": "plugins/search-common" }, "keywords": [ "backstage", diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index 10ce787a6c..0a277dab85 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -9,16 +9,16 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { AsyncState } from 'react-use/lib/useAsync'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { InputBaseProps } from '@material-ui/core'; import { JsonObject } from '@backstage/types'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; -import { SearchQuery } from '@backstage/search-common'; -import { SearchResult as SearchResult_2 } from '@backstage/search-common'; -import { SearchResultSet } from '@backstage/search-common'; +import { SearchQuery } from '@backstage/plugin-search-common'; +import { SearchResult as SearchResult_2 } from '@backstage/plugin-search-common'; +import { SearchResultSet } from '@backstage/plugin-search-common'; // Warning: (ae-missing-release-tag) "DefaultResultListItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/search/package.json b/plugins/search/package.json index 6499920806..eabfae1b16 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -39,7 +39,7 @@ "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", "@backstage/plugin-catalog-react": "^0.8.0", - "@backstage/search-common": "^0.3.0", + "@backstage/plugin-search-common": "^0.3.0", "@backstage/theme": "^0.2.15", "@backstage/types": "^0.1.3", "@material-ui/core": "^4.12.2", diff --git a/plugins/search/src/apis.ts b/plugins/search/src/apis.ts index 008002deb1..908942d87d 100644 --- a/plugins/search/src/apis.ts +++ b/plugins/search/src/apis.ts @@ -20,7 +20,7 @@ import { IdentityApi, } from '@backstage/core-plugin-api'; import { ResponseError } from '@backstage/errors'; -import { SearchQuery, SearchResultSet } from '@backstage/search-common'; +import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common'; import qs from 'qs'; export const searchApiRef = createApiRef({ diff --git a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx index a6c9a36bb6..d9fbfe315e 100644 --- a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx +++ b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx @@ -15,7 +15,7 @@ */ import React, { ReactNode } from 'react'; -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { ListItem, ListItemIcon, diff --git a/plugins/search/src/components/SearchContext/SearchContext.tsx b/plugins/search/src/components/SearchContext/SearchContext.tsx index a9f42fe5ca..100d8a3ac3 100644 --- a/plugins/search/src/components/SearchContext/SearchContext.tsx +++ b/plugins/search/src/components/SearchContext/SearchContext.tsx @@ -16,7 +16,7 @@ import { JsonObject } from '@backstage/types'; import { useApi, AnalyticsContext } from '@backstage/core-plugin-api'; -import { SearchResultSet } from '@backstage/search-common'; +import { SearchResultSet } from '@backstage/plugin-search-common'; import React, { createContext, PropsWithChildren, diff --git a/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx b/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx index 36d670a1ed..7d6c35b00c 100644 --- a/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx +++ b/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import { ApiProvider } from '@backstage/core-app-api'; -import { SearchResultSet } from '@backstage/search-common'; +import { SearchResultSet } from '@backstage/plugin-search-common'; import { TestApiRegistry } from '@backstage/test-utils'; import React, { ComponentProps, PropsWithChildren } from 'react'; import { searchApiRef } from '../../apis'; diff --git a/plugins/search/src/components/SearchResult/SearchResult.tsx b/plugins/search/src/components/SearchResult/SearchResult.tsx index 34b6840f21..d6ca9fd968 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.tsx @@ -19,7 +19,7 @@ import { Progress, ResponseErrorPanel, } from '@backstage/core-components'; -import { SearchResult } from '@backstage/search-common'; +import { SearchResult } from '@backstage/plugin-search-common'; import React from 'react'; import { useSearch } from '../SearchContext'; diff --git a/plugins/techdocs-backend/api-report.md b/plugins/techdocs-backend/api-report.md index acce0b4981..f306ce4f35 100644 --- a/plugins/techdocs-backend/api-report.md +++ b/plugins/techdocs-backend/api-report.md @@ -7,7 +7,7 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; -import { DocumentCollatorFactory } from '@backstage/search-common'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { Entity } from '@backstage/catalog-model'; import express from 'express'; import { GeneratorBuilder } from '@backstage/plugin-techdocs-node'; diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index aea75b1c3d..b11e038879 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -41,8 +41,8 @@ "@backstage/errors": "^0.2.2", "@backstage/integration": "^0.8.0", "@backstage/plugin-catalog-common": "^0.2.0", + "@backstage/plugin-search-common": "^0.3.0", "@backstage/plugin-techdocs-node": "^0.11.11", - "@backstage/search-common": "^0.3.0", "@types/express": "^4.17.6", "dockerode": "^3.3.1", "express": "^4.17.1", diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts index 7ba76350e4..41d637e319 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts @@ -31,7 +31,7 @@ import { } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common'; -import { DocumentCollatorFactory } from '@backstage/search-common'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { TechDocsDocument } from '@backstage/plugin-techdocs-node'; import unescape from 'lodash/unescape'; import fetch from 'node-fetch'; diff --git a/plugins/techdocs-node/api-report.md b/plugins/techdocs-node/api-report.md index 9ec1aafb09..768865896b 100644 --- a/plugins/techdocs-node/api-report.md +++ b/plugins/techdocs-node/api-report.md @@ -10,7 +10,7 @@ import { Config } from '@backstage/config'; import { ContainerRunner } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import express from 'express'; -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; import { Logger as Logger_2 } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { ScmIntegrationRegistry } from '@backstage/integration'; diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json index 7cbf49f954..ee2edd3e58 100644 --- a/plugins/techdocs-node/package.json +++ b/plugins/techdocs-node/package.json @@ -46,8 +46,8 @@ "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", - "@backstage/search-common": "^0.3.0", "@backstage/integration": "^0.8.0", + "@backstage/plugin-search-common": "^0.3.0", "@google-cloud/storage": "^5.6.0", "@trendyol-js/openstack-swift-sdk": "^0.0.5", "@types/express": "^4.17.6", diff --git a/plugins/techdocs-node/src/techdocsTypes.ts b/plugins/techdocs-node/src/techdocsTypes.ts index 0b722f4da5..77436b35d5 100644 --- a/plugins/techdocs-node/src/techdocsTypes.ts +++ b/plugins/techdocs-node/src/techdocsTypes.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { IndexableDocument } from '@backstage/search-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; /** * TechDocs indexable document interface diff --git a/scripts/api-extractor.ts b/scripts/api-extractor.ts index d37fa51c60..153861b875 100644 --- a/scripts/api-extractor.ts +++ b/scripts/api-extractor.ts @@ -237,6 +237,7 @@ const NO_WARNING_PACKAGES = [ 'plugins/scaffolder-backend-module-yeoman', 'plugins/scaffolder-common', 'plugins/search-backend-node', + 'plugins/search-common', 'plugins/techdocs-backend', 'plugins/techdocs-node', 'plugins/tech-insights', diff --git a/yarn.lock b/yarn.lock index 48784ac13c..fc8d5480a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12049,6 +12049,7 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: "@backstage/plugin-rollbar" "^0.4.1" "@backstage/plugin-scaffolder" "^0.14.0" "@backstage/plugin-search" "^0.7.2" + "@backstage/plugin-search-common" "^0.3.0" "@backstage/plugin-sentry" "^0.3.39" "@backstage/plugin-shortcuts" "^0.2.2" "@backstage/plugin-tech-insights" "^0.1.11" @@ -12056,7 +12057,6 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: "@backstage/plugin-techdocs" "^0.15.0" "@backstage/plugin-todo" "^0.2.3" "@backstage/plugin-user-settings" "^0.4.0" - "@backstage/search-common" "^0.3.0" "@backstage/theme" "^0.2.15" "@material-ui/core" "^4.12.2" "@material-ui/icons" "^4.9.1" From d52155466adeab052bdbcf86d8921b445367ef10 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 3 Mar 2022 17:12:01 +0100 Subject: [PATCH 227/353] Recreate a shell of @backstage/search-common for smooth deprecation process Signed-off-by: Eric Peterson --- .changeset/search-byta-namnet.md | 5 + .changeset/search-common-people.md | 7 ++ packages/search-common/.eslintrc.js | 3 + packages/search-common/CHANGELOG.md | 148 +++++++++++++++++++++++++++ packages/search-common/README.md | 7 ++ packages/search-common/api-report.md | 9 ++ packages/search-common/package.json | 50 +++++++++ packages/search-common/src/index.ts | 17 +++ 8 files changed, 246 insertions(+) create mode 100644 .changeset/search-byta-namnet.md create mode 100644 .changeset/search-common-people.md create mode 100644 packages/search-common/.eslintrc.js create mode 100644 packages/search-common/CHANGELOG.md create mode 100644 packages/search-common/README.md create mode 100644 packages/search-common/api-report.md create mode 100644 packages/search-common/package.json create mode 100644 packages/search-common/src/index.ts diff --git a/.changeset/search-byta-namnet.md b/.changeset/search-byta-namnet.md new file mode 100644 index 0000000000..c3abc81996 --- /dev/null +++ b/.changeset/search-byta-namnet.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-common': patch +--- + +Renamed `@backstage/search-common` to `@backstage/plugin-search-common`. diff --git a/.changeset/search-common-people.md b/.changeset/search-common-people.md new file mode 100644 index 0000000000..2ed34c7573 --- /dev/null +++ b/.changeset/search-common-people.md @@ -0,0 +1,7 @@ +--- +'@backstage/search-common': patch +--- + +**DEPRECATION** + +The `@backstage/search-common` package is being renamed `@backstage/plugin-search-common`. We may continue to publish changes to `@backstage/search-common` for a time, but will stop doing so in the near future. If you depend on this package, you should update your dependencies to point at the renamed package. diff --git a/packages/search-common/.eslintrc.js b/packages/search-common/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/packages/search-common/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/packages/search-common/CHANGELOG.md b/packages/search-common/CHANGELOG.md new file mode 100644 index 0000000000..112d3d583a --- /dev/null +++ b/packages/search-common/CHANGELOG.md @@ -0,0 +1,148 @@ +# @backstage/search-common + +## 0.3.0 + +### Minor Changes + +- 022507c860: **BREAKING** + + The Backstage Search Platform's indexing process has been rewritten as a stream + pipeline in order to improve efficiency and performance on large document sets. + + The concepts of `Collator` and `Decorator` have been replaced with readable and + transform object streams (respectively), as well as factory classes to + instantiate them. Accordingly, the `SearchEngine.index()` method has also been + replaced with a `getIndexer()` factory method that resolves to a writable + object stream. + + Check [this upgrade guide](https://backstage.io/docs/features/search/how-to-guides#how-to-migrate-from-search-alpha-to-beta) + for further details. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.5.2 + +## 0.2.4 + +### Patch Changes + +- Fix for the previous release with missing type declarations. +- Updated dependencies + - @backstage/types@0.1.3 + - @backstage/plugin-permission-common@0.5.1 + +## 0.2.3 + +### Patch Changes + +- c77c5c7eb6: Added `backstage.role` to `package.json` +- Updated dependencies + - @backstage/plugin-permission-common@0.5.0 + - @backstage/types@0.1.2 + +## 0.2.2 + +### Patch Changes + +- 9a511968b1: - Add optional visibilityPermission property to DocumentCollator type + - Add new DocumentTypeInfo type for housing information about the document types stored in a search engine. +- b2e918fa0b: Add optional resourceRef field to the IndexableDocument type for use when authorizing access to documents. +- 96cbebc629: Add optional query request options containing authorization token to SearchEngine#query. + +## 0.2.1 + +### Patch Changes + +- 10615525f3: Switch to use the json and observable types from `@backstage/types` + +## 0.2.0 + +### Minor Changes + +- a13f21cdc: Implement optional `pageCursor` based paging in search. + + To use paging in your app, add a `` to your + `SearchPage.tsx`. + +## 0.1.3 + +### Patch Changes + +- d9c13d535: Implements configuration and indexing functionality for ElasticSearch search engine. Adds indexing, searching and default translator for ElasticSearch and modifies default backend example-app to use ES if it is configured. + + ## Example configurations: + + ### AWS + + Using AWS hosted ElasticSearch the only configuration options needed is the URL to the ElasticSearch service. The implementation assumes + that environment variables for AWS access key id and secret access key are defined in accordance to the [default AWS credential chain.](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html). + + ```yaml + search: + elasticsearch: + provider: aws + node: https://my-backstage-search-asdfqwerty.eu-west-1.es.amazonaws.com + ``` + + ### Elastic.co + + Elastic Cloud hosted ElasticSearch uses a Cloud ID to determine the instance of hosted ElasticSearch to connect to. Additionally, username and password needs to be provided either directly or using environment variables like defined in [Backstage documentation.](https://backstage.io/docs/conf/writing#includes-and-dynamic-data) + + ```yaml + search: + elasticsearch: + provider: elastic + cloudId: backstage-elastic:asdfqwertyasdfqwertyasdfqwertyasdfqwerty== + auth: + username: elastic + password: changeme + ``` + + ### Others + + Other ElasticSearch instances can be connected to by using standard ElasticSearch authentication methods and exposed URL, provided that the cluster supports that. The configuration options needed are the URL to the node and authentication information. Authentication can be handled by either providing username/password or and API key or a bearer token. In case both username/password combination and one of the tokens are provided, token takes precedence. For more information how to create an API key, see [Elastic documentation on API keys](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html) and how to create a bearer token, see [Elastic documentation on tokens.](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-service-token.html) + + #### Configuration examples + + ##### With username and password + + ```yaml + search: + elasticsearch: + node: http://localhost:9200 + auth: + username: elastic + password: changeme + ``` + + ##### With bearer token + + ```yaml + search: + elasticsearch: + node: http://localhost:9200 + auth: + bearer: token + ``` + + ##### With API key + + ```yaml + search: + elasticsearch: + node: http://localhost:9200 + auth: + apiKey: base64EncodedKey + ``` + +- Updated dependencies + - @backstage/config@0.1.6 + +## 0.1.2 + +### Patch Changes + +- db1c8f93b: The ` set of components exported by the Search Plugin are now updated to use the Search Backend API. These will be made available as the default non-"next" versions in a follow-up release. + + The interfaces for decorators and collators in the Search Backend have also seen minor, breaking revisions ahead of a general release. If you happen to be building on top of these interfaces, check and update your implementations accordingly. The APIs will be considered more stable in a follow-up release. diff --git a/packages/search-common/README.md b/packages/search-common/README.md new file mode 100644 index 0000000000..3a75dac4c2 --- /dev/null +++ b/packages/search-common/README.md @@ -0,0 +1,7 @@ +# @backstage/search-common + +**WARNING**: This package is moving to `@backstage/plugin-search-common`. +Please update any dependencies you may have, as this package will no longer be +published or updated in the near future. + +Common functionalities for Search, to be shared between various search-enabled plugins. diff --git a/packages/search-common/api-report.md b/packages/search-common/api-report.md new file mode 100644 index 0000000000..03965ace66 --- /dev/null +++ b/packages/search-common/api-report.md @@ -0,0 +1,9 @@ +## API Report File for "@backstage/search-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +export * from '@backstage/plugin-search-common'; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/search-common/package.json b/packages/search-common/package.json new file mode 100644 index 0000000000..e85ed2dd83 --- /dev/null +++ b/packages/search-common/package.json @@ -0,0 +1,50 @@ +{ + "name": "@backstage/search-common", + "description": "Common functionalities for Search, to be shared between various search-enabled plugins", + "version": "0.3.0", + "main": "src/index.ts", + "types": "src/index.ts", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "common-library" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/search-common" + }, + "keywords": [ + "backstage", + "search" + ], + "license": "Apache-2.0", + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" + }, + "bugs": { + "url": "https://github.com/backstage/backstage/issues" + }, + "dependencies": { + "@backstage/plugin-search-common": "^0.3.0" + }, + "devDependencies": {}, + "jest": { + "roots": [ + ".." + ] + } +} diff --git a/packages/search-common/src/index.ts b/packages/search-common/src/index.ts new file mode 100644 index 0000000000..8e21c5f936 --- /dev/null +++ b/packages/search-common/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 '@backstage/plugin-search-common'; From 30d7d3975c68894da666de838183d17af6338087 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 5 Mar 2022 09:00:04 +0100 Subject: [PATCH 228/353] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Patrik Oldsberg Co-authored-by: Fredrik Adelöw --- SECURITY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 1ff2460d19..49e26349b0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -61,7 +61,7 @@ function writeTemporaryFile(tmpDir: string, name: string, content: string) { If the `name` of the file is controlled by the user, they can for example enter `../../../../etc/hosts` as the name of the file. This can lead to a file being written outside the intended directory, which in turn can be used to inject malicious code or other form of attacks. -The recommended solution to this is to use `resolveSafeChildPath` from `@backstage/backend-common` to resolve the file path instead. It makes sure that the resolved path does not fall outside the provided directory. If you simply what to validate whether a file path is safe, you can use `isChildPath` instead. +The recommended solution to this is to use `resolveSafeChildPath` from `@backstage/backend-common` to resolve the file path instead. It makes sure that the resolved path does not fall outside the provided directory. If you simply want to validate whether a file path is safe, you can use `isChildPath` instead. The insecure example above should instead be written like this: @@ -103,7 +103,7 @@ res.send({ ok: true }); // BAD res.json({ ok: true }); // GOOD ``` -If you absolute must return a string with `.send(...)`, use an explicit and secure `Content-Type`: +If you absolutely must return a string with `.send(...)`, use an explicit and secure `Content-Type`: ```ts res.send(`message=${message}`); // BAD From b41cd11be32dae2936640646cc447d7f36a58016 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 5 Mar 2022 10:03:40 +0100 Subject: [PATCH 229/353] Revert "Revert "scripts/prepare-release: update to detect patch versions from patch branches"" Signed-off-by: Patrik Oldsberg --- docs/publishing.md | 9 -- scripts/prepare-release.js | 174 +++++++++++++++++++++++++++++-------- 2 files changed, 137 insertions(+), 46 deletions(-) diff --git a/docs/publishing.md b/docs/publishing.md index fbbef93493..a0d67b7d24 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -67,12 +67,3 @@ process is used to release an emergency fix as version `6.5.1` in the patch rele - [ ] The fix, which you can likely cherry-pick from your patch branch: `git cherry-pick origin/patch/v1.18.0^` - [ ] An updated `CHANGELOG.md` of all patched packages from the tip of the patch branch, `git checkout origin/patch/v1.18.0 -- {packages,plugins}/*/CHANGELOG.md`. - [ ] A changeset with the message "Applied the fix from version `6.5.1` of this package, which is part of the `v1.18.1` release of Backstage." - - [ ] An entry in `.changeset/patched.json` that sets the current release version to `6.5.1`: - - ```json - { - "currentReleaseVersion": { - "@backstage/plugin-foo": "6.5.1" - } - } - ``` diff --git a/scripts/prepare-release.js b/scripts/prepare-release.js index be82e19344..5cc888fd66 100755 --- a/scripts/prepare-release.js +++ b/scripts/prepare-release.js @@ -28,6 +28,10 @@ const execFile = promisify(execFileCb); // All of these are considered to be main-line release branches const MAIN_BRANCHES = ['master', 'origin/master', 'changeset-release/master']; +// This prefix is used for patch branches, followed by the release version WITH a 'v' prefix +// For example, `patch/v1.2.0` +const PATCH_BRANCH_PREFIX = 'patch/'; + const DEPENDENCY_TYPES = [ 'dependencies', 'devDependencies', @@ -35,25 +39,84 @@ const DEPENDENCY_TYPES = [ 'peerDependencies', ]; +/** + * Returns the most recent release version on the main branch that is not a pre-release. + */ +async function getPreviousReleaseVersion(repo) { + // TODO(Rugvip): Figure out which field to sort by to avoid manual sort after + const { stdout: tagsStr } = await execFile( + 'git', + ['tag', '--list', 'v*', '--merged=HEAD'], + { shell: true, cwd: repo.root.dir }, + ); + const tags = tagsStr.trim().split(/\r\n|\n/); + const [latestTag] = semver.rsort(tags).filter(t => !semver.prerelease(t)); + return latestTag; +} + +/** + * Finds the tip of the patch branch of a given release version. + * Returns undefined if no patch branch exists. + */ +async function findTipOfPatchBranch(repo, release) { + try { + await execFile('git', ['fetch', 'origin', PATCH_BRANCH_PREFIX + release], { + shell: true, + cwd: repo.root.dir, + }); + } catch (error) { + if (error.stderr?.match(/fatal: couldn't find remote ref/i)) { + return undefined; + } + throw error; + } + const { stdout: refStr } = await execFile('git', ['rev-parse', 'FETCH_HEAD']); + return refStr.trim(); +} + +/** + * Returns a map of packages to their versions for any package version + * in that does not match the current version in the working directory. + */ +async function detectPatchVersionsForRef(repo, ref) { + const patchVersions = new Map(); + + for (const pkg of repo.packages) { + const pkgJsonPath = path.join( + path.relative(repo.root.dir, pkg.dir), + 'package.json', + ); + const { stdout: pkgJsonStr } = await execFile('git', [ + 'show', + `${ref}:${pkgJsonPath}`, + ]); + if (pkgJsonStr) { + const releasePkgJson = JSON.parse(pkgJsonStr); + const pkgJson = pkg.packageJson; + if (releasePkgJson.name !== pkgJson.name) { + throw new Error( + `Mismatched package name at ${pkg.dir}, ${releasePkgJson.name} !== ${pkgJson.name}`, + ); + } + if (releasePkgJson.version !== pkgJson.version) { + patchVersions.set(pkgJson.name, releasePkgJson.version); + } + } + } + + return patchVersions; +} + /** * Bumps up the versions of packages to account for * the base versions that are set in .changeset/patched.json. * This may be needed when we have made emergency releases. */ -async function updatePatchVersions() { - const patchedJsonPath = path.resolve('.changeset', 'patched.json'); - const { currentReleaseVersion } = await fs.readJson(patchedJsonPath); - if (Object.keys(currentReleaseVersion).length === 0) { - console.log('No currentReleaseVersion overrides found, skipping.'); - return; - } - - const { packages } = await getPackages(path.resolve('.')); - +async function applyPatchVersions(repo, patchVersions) { const pendingVersionBumps = new Map(); - for (const [name, version] of Object.entries(currentReleaseVersion)) { - const pkg = packages.find(p => p.packageJson.name === name); + for (const [name, version] of patchVersions) { + const pkg = repo.packages.find(p => p.packageJson.name === name); if (!pkg) { throw new Error(`Package ${name} not found`); } @@ -81,7 +144,7 @@ async function updatePatchVersions() { }); } - for (const { dir, packageJson } of packages) { + for (const { dir, packageJson } of [repo.root, ...repo.packages]) { let hasChanges = false; if (pendingVersionBumps.has(packageJson.name)) { @@ -117,20 +180,44 @@ async function updatePatchVersions() { }); } } +} - await fs.writeJSON( - patchedJsonPath, - { currentReleaseVersion: {} }, - { spaces: 2, encoding: 'utf8' }, - ); +/** + * Detects any patched packages version since the most recent release on + * the main branch, and then bumps all packages in the repo accordingly. + */ +async function updatePackageVersions(repo) { + const previousRelease = await getPreviousReleaseVersion(repo); + console.log(`Found release version: ${previousRelease}`); + + const patchRef = await findTipOfPatchBranch(repo, previousRelease); + if (patchRef) { + console.log(`Tip of the patch branch: ${patchRef}`); + + const patchVersions = await detectPatchVersionsForRef(repo, patchRef); + if (patchVersions.size > 0) { + console.log( + `Found ${patchVersions.size} packages that were patched since the last release`, + ); + for (const [name, version] of patchVersions) { + console.log(` ${name}: ${version}`); + } + + await applyPatchVersions(repo, patchVersions); + } else { + console.log('No packages were patched since the last release'); + } + } else { + console.log('No patch branch found'); + } } /** * Returns the mode and tag that is currently set * in the .changeset/pre.json file */ -async function getPreInfo(rootPath) { - const pre = path.join(rootPath, '.changeset', 'pre.json'); +async function getPreInfo(repo) { + const pre = path.join(repo.root.dir, '.changeset', 'pre.json'); if (!(await fs.pathExists(pre))) { return { mode: undefined, tag: undefined }; } @@ -139,26 +226,30 @@ async function getPreInfo(rootPath) { return { mode, tag }; } +/** + * Returns the name of the current git branch + */ +async function getCurrentBranch(repo) { + const { stdout } = await execFile( + 'git', + ['rev-parse', '--abbrev-ref', 'HEAD'], + { cwd: repo.root.dir, shell: true }, + ); + return stdout.trim(); +} + /** * Bumps the release version in the root package.json. * * This takes into account whether we're in pre-release mode or on a patch branch. */ -async function updateBackstageReleaseVersion() { - const rootPath = path.resolve(__dirname, '..'); - const branchName = await execFile( - 'git', - ['rev-parse', '--abbrev-ref', 'HEAD'], - { shell: true }, - ).then(({ stdout }) => stdout.trim()); - const { mode: preMode, tag: preTag } = await getPreInfo(rootPath); +async function updateBackstageReleaseVersion(repo, type) { + const { mode: preMode, tag: preTag } = await getPreInfo(repo); - const packagePath = path.join(rootPath, 'package.json'); - const package = await fs.readJson(packagePath); - const { version: currentVersion } = package; + const { version: currentVersion } = repo.root.packageJson; let nextVersion; - if (MAIN_BRANCHES.includes(branchName)) { + if (type === 'minor') { if (preMode === 'pre') { if (semver.prerelease(currentVersion)) { nextVersion = semver.inc(currentVersion, 'pre', preTag); @@ -170,7 +261,7 @@ async function updateBackstageReleaseVersion() { } else { nextVersion = semver.inc(currentVersion, 'minor'); } - } else { + } else if (type === 'patch') { if (preMode) { throw new Error(`Unexpected pre mode ${preMode} on branch ${branchName}`); } @@ -178,9 +269,9 @@ async function updateBackstageReleaseVersion() { } await fs.writeJson( - packagePath, + path.join(repo.root.dir, 'package.json'), { - ...package, + ...repo.root.packageJson, version: nextVersion, }, { spaces: 2, encoding: 'utf8' }, @@ -188,8 +279,17 @@ async function updateBackstageReleaseVersion() { } async function main() { - await updatePatchVersions(); - await updateBackstageReleaseVersion(); + const repo = await getPackages(__dirname); + const branchName = await getCurrentBranch(repo); + const isMainBranch = MAIN_BRANCHES.includes(branchName); + + console.log(`Current branch: ${branchName}`); + if (isMainBranch) { + console.log('Main release, updating package versions'); + await updatePackageVersions(repo); + } + + await updateBackstageReleaseVersion(repo, isMainBranch ? 'minor' : 'patch'); } main().catch(error => { From c4729f65e64f45169d585938a368905b71c1b35a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 5 Mar 2022 10:18:40 +0100 Subject: [PATCH 230/353] scripts/prepare-release: read release version from root package instead Signed-off-by: Patrik Oldsberg --- scripts/prepare-release.js | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/scripts/prepare-release.js b/scripts/prepare-release.js index 5cc888fd66..bcd634bfec 100755 --- a/scripts/prepare-release.js +++ b/scripts/prepare-release.js @@ -28,9 +28,9 @@ const execFile = promisify(execFileCb); // All of these are considered to be main-line release branches const MAIN_BRANCHES = ['master', 'origin/master', 'changeset-release/master']; -// This prefix is used for patch branches, followed by the release version WITH a 'v' prefix +// This prefix is used for patch branches, followed by the release version // For example, `patch/v1.2.0` -const PATCH_BRANCH_PREFIX = 'patch/'; +const PATCH_BRANCH_PREFIX = 'patch/v'; const DEPENDENCY_TYPES = [ 'dependencies', @@ -39,21 +39,6 @@ const DEPENDENCY_TYPES = [ 'peerDependencies', ]; -/** - * Returns the most recent release version on the main branch that is not a pre-release. - */ -async function getPreviousReleaseVersion(repo) { - // TODO(Rugvip): Figure out which field to sort by to avoid manual sort after - const { stdout: tagsStr } = await execFile( - 'git', - ['tag', '--list', 'v*', '--merged=HEAD'], - { shell: true, cwd: repo.root.dir }, - ); - const tags = tagsStr.trim().split(/\r\n|\n/); - const [latestTag] = semver.rsort(tags).filter(t => !semver.prerelease(t)); - return latestTag; -} - /** * Finds the tip of the patch branch of a given release version. * Returns undefined if no patch branch exists. @@ -187,10 +172,11 @@ async function applyPatchVersions(repo, patchVersions) { * the main branch, and then bumps all packages in the repo accordingly. */ async function updatePackageVersions(repo) { - const previousRelease = await getPreviousReleaseVersion(repo); - console.log(`Found release version: ${previousRelease}`); + const rootPkgPath = path.resolve(repo.root.dir, 'package.json'); + const { version: currentRelease } = await fs.readJson(rootPkgPath); + console.log(`Current release version: ${currentRelease}`); - const patchRef = await findTipOfPatchBranch(repo, previousRelease); + const patchRef = await findTipOfPatchBranch(repo, currentRelease); if (patchRef) { console.log(`Tip of the patch branch: ${patchRef}`); From 9cdd51a6b622dd757decd9a8a8c6626378fa399c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 5 Mar 2022 10:19:14 +0100 Subject: [PATCH 231/353] scripts/prepare-release: handle packages that were added since last release Signed-off-by: Patrik Oldsberg --- scripts/prepare-release.js | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/scripts/prepare-release.js b/scripts/prepare-release.js index bcd634bfec..bc0a58b966 100755 --- a/scripts/prepare-release.js +++ b/scripts/prepare-release.js @@ -71,21 +71,31 @@ async function detectPatchVersionsForRef(repo, ref) { path.relative(repo.root.dir, pkg.dir), 'package.json', ); - const { stdout: pkgJsonStr } = await execFile('git', [ - 'show', - `${ref}:${pkgJsonPath}`, - ]); - if (pkgJsonStr) { - const releasePkgJson = JSON.parse(pkgJsonStr); - const pkgJson = pkg.packageJson; - if (releasePkgJson.name !== pkgJson.name) { - throw new Error( - `Mismatched package name at ${pkg.dir}, ${releasePkgJson.name} !== ${pkgJson.name}`, - ); + try { + const { stdout: pkgJsonStr } = await execFile('git', [ + 'show', + `${ref}:${pkgJsonPath}`, + ]); + if (pkgJsonStr) { + const releasePkgJson = JSON.parse(pkgJsonStr); + const pkgJson = pkg.packageJson; + if (releasePkgJson.name !== pkgJson.name) { + throw new Error( + `Mismatched package name at ${pkg.dir}, ${releasePkgJson.name} !== ${pkgJson.name}`, + ); + } + if (releasePkgJson.version !== pkgJson.version) { + patchVersions.set(pkgJson.name, releasePkgJson.version); + } } - if (releasePkgJson.version !== pkgJson.version) { - patchVersions.set(pkgJson.name, releasePkgJson.version); + } catch (error) { + if ( + error.stderr?.match(/^fatal: Path .* exists on disk, but not in .*$/m) + ) { + console.log(`Skipping new package ${pkg.packageJson.name}`); + continue; } + throw error; } } From 6926e97a03e5fe0abb31b4e57d6394175790db67 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 5 Mar 2022 10:21:29 +0100 Subject: [PATCH 232/353] changesets: remove patched.json Signed-off-by: Patrik Oldsberg --- .changeset/patched.json | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 .changeset/patched.json diff --git a/.changeset/patched.json b/.changeset/patched.json deleted file mode 100644 index dd63e86169..0000000000 --- a/.changeset/patched.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "currentReleaseVersion": { - "@backstage/backend-common": "0.12.1", - "@backstage/catalog-model": "0.12.1", - "@backstage/cli": "0.15.1", - "@backstage/plugin-catalog-backend": "0.23.1", - "@backstage/plugin-catalog-common": "0.2.1", - "@backstage/plugin-catalog-react": "0.8.1" - } -} From c3bc369a54628423e721e9053897863172f5fdac Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 5 Mar 2022 11:39:05 +0100 Subject: [PATCH 233/353] cli: add dependents to PackageGraph nodes Signed-off-by: Patrik Oldsberg --- .../cli/src/lib/monorepo/PackageGraph.test.ts | 21 +++++++++++++ packages/cli/src/lib/monorepo/PackageGraph.ts | 30 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/packages/cli/src/lib/monorepo/PackageGraph.test.ts b/packages/cli/src/lib/monorepo/PackageGraph.test.ts index 06427a7313..feb1709bf7 100644 --- a/packages/cli/src/lib/monorepo/PackageGraph.test.ts +++ b/packages/cli/src/lib/monorepo/PackageGraph.test.ts @@ -87,6 +87,11 @@ describe('PackageGraph', () => { localDependencies: new Map([['b', b]]), localDevDependencies: new Map([['c', c]]), localOptionalDependencies: new Map(), + allLocalDependents: new Map(), + publishedLocalDependents: new Map(), + localDependents: new Map(), + localDevDependents: new Map(), + localOptionalDependents: new Map(), }); expect(b).toMatchObject({ name: 'b', @@ -96,6 +101,11 @@ describe('PackageGraph', () => { localDependencies: new Map(), localDevDependencies: new Map([['c', c]]), localOptionalDependencies: new Map(), + allLocalDependents: new Map([['a', a]]), + publishedLocalDependents: new Map([['a', a]]), + localDependents: new Map([['a', a]]), + localDevDependents: new Map(), + localOptionalDependents: new Map(), }); expect(c).toMatchObject({ name: 'c', @@ -105,6 +115,17 @@ describe('PackageGraph', () => { localDependencies: new Map(), localDevDependencies: new Map(), localOptionalDependencies: new Map(), + allLocalDependents: new Map([ + ['a', a], + ['b', b], + ]), + publishedLocalDependents: new Map(), + localDependents: new Map(), + localDevDependents: new Map([ + ['a', a], + ['b', b], + ]), + localOptionalDependents: new Map(), }); }); diff --git a/packages/cli/src/lib/monorepo/PackageGraph.ts b/packages/cli/src/lib/monorepo/PackageGraph.ts index 4bbb9eac67..c65f5c003c 100644 --- a/packages/cli/src/lib/monorepo/PackageGraph.ts +++ b/packages/cli/src/lib/monorepo/PackageGraph.ts @@ -47,6 +47,7 @@ export type PackageGraphNode = { dir: string; /** The package data of the package itself */ packageJson: ExtendedPackageJSON; + /** All direct local dependencies of the package */ allLocalDependencies: Map; /** All direct local dependencies that will be present in the published package */ @@ -57,6 +58,17 @@ export type PackageGraphNode = { localDevDependencies: Map; /** Local optionalDependencies */ localOptionalDependencies: Map; + + /** All direct incoming local dependencies of the package */ + allLocalDependents: Map; + /** All direct incoming local dependencies that will be present in the published package */ + publishedLocalDependents: Map; + /** Incoming local dependencies */ + localDependents: Map; + /** Incoming local devDependencies */ + localDevDependents: Map; + /** Incoming local optionalDependencies */ + localOptionalDependents: Map; }; export class PackageGraph extends Map { @@ -82,11 +94,18 @@ export class PackageGraph extends Map { name, dir: pkg.dir, packageJson: pkg.packageJson as ExtendedPackageJSON, + allLocalDependencies: new Map(), publishedLocalDependencies: new Map(), localDependencies: new Map(), localDevDependencies: new Map(), localOptionalDependencies: new Map(), + + allLocalDependents: new Map(), + publishedLocalDependents: new Map(), + localDependents: new Map(), + localDevDependents: new Map(), + localOptionalDependents: new Map(), }); } @@ -98,6 +117,10 @@ export class PackageGraph extends Map { node.allLocalDependencies.set(depName, depPkg); node.publishedLocalDependencies.set(depName, depPkg); node.localDependencies.set(depName, depPkg); + + depPkg.allLocalDependents.set(node.name, node); + depPkg.publishedLocalDependents.set(node.name, node); + depPkg.localDependents.set(node.name, node); } } for (const depName of Object.keys( @@ -107,6 +130,9 @@ export class PackageGraph extends Map { if (depPkg) { node.allLocalDependencies.set(depName, depPkg); node.localDevDependencies.set(depName, depPkg); + + depPkg.allLocalDependents.set(node.name, node); + depPkg.localDevDependents.set(node.name, node); } } for (const depName of Object.keys( @@ -117,6 +143,10 @@ export class PackageGraph extends Map { node.allLocalDependencies.set(depName, depPkg); node.publishedLocalDependencies.set(depName, depPkg); node.localOptionalDependencies.set(depName, depPkg); + + depPkg.allLocalDependents.set(node.name, node); + depPkg.publishedLocalDependents.set(node.name, node); + depPkg.localOptionalDependents.set(node.name, node); } } } From 4aa28c71f432e5bb3a025d39bf998b8ac608f477 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 5 Mar 2022 11:46:10 +0100 Subject: [PATCH 234/353] scripts/prepare-release: fix git error check Signed-off-by: Patrik Oldsberg --- scripts/prepare-release.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prepare-release.js b/scripts/prepare-release.js index bc0a58b966..c4a790b2cc 100755 --- a/scripts/prepare-release.js +++ b/scripts/prepare-release.js @@ -90,7 +90,7 @@ async function detectPatchVersionsForRef(repo, ref) { } } catch (error) { if ( - error.stderr?.match(/^fatal: Path .* exists on disk, but not in .*$/m) + error.stderr?.match(/^fatal: Path .* exists on disk, but not in .*$/im) ) { console.log(`Skipping new package ${pkg.packageJson.name}`); continue; From 2c528506aa700e5e8ec3ce57962de6fdddad50b5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 5 Mar 2022 11:44:20 +0100 Subject: [PATCH 235/353] cli: add --since flag for repo build Signed-off-by: Patrik Oldsberg --- .changeset/big-months-deliver.md | 5 +++++ packages/cli/src/commands/index.ts | 4 ++++ packages/cli/src/commands/repo/build.ts | 13 ++++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 .changeset/big-months-deliver.md diff --git a/.changeset/big-months-deliver.md b/.changeset/big-months-deliver.md new file mode 100644 index 0000000000..1cf873beb1 --- /dev/null +++ b/.changeset/big-months-deliver.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added `--since ` flag for `repo build` command.` diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index d3baa37b3f..5cb02c82b4 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -41,6 +41,10 @@ export function registerRepoCommand(program: CommanderStatic) { '--all', 'Build all packages, including bundled app and backend packages.', ) + .option( + '--since ', + 'Only build packages and their dev dependents that changed since the specified ref', + ) .action(lazy(() => import('./repo/build').then(m => m.command))); command diff --git a/packages/cli/src/commands/repo/build.ts b/packages/cli/src/commands/repo/build.ts index ceb468ebfa..fe50bee84f 100644 --- a/packages/cli/src/commands/repo/build.ts +++ b/packages/cli/src/commands/repo/build.ts @@ -78,7 +78,18 @@ function createScriptOptionsParser(anyCmd: Command, commandPath: string[]) { } export async function command(cmd: Command): Promise { - const packages = await PackageGraph.listTargetPackages(); + let packages = await PackageGraph.listTargetPackages(); + + if (cmd.since) { + const graph = PackageGraph.fromPackages(packages); + const changedPackages = await graph.listChangedPackages({ ref: cmd.since }); + const withDevDependents = graph.collectPackageNames( + changedPackages.map(pkg => pkg.name), + pkg => pkg.localDevDependents.keys(), + ); + packages = Array.from(withDevDependents).map(name => graph.get(name)!); + } + const apps = new Array(); const backends = new Array(); From bf95bb806c52cb8d6771f3967c9a760868762b29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sat, 5 Mar 2022 14:42:10 +0100 Subject: [PATCH 236/353] remove CatalogApi.getEntityByName MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/olive-emus-speak.md | 10 ++++++++++ .changeset/thick-gifts-cheat.md | 5 +++++ packages/catalog-client/api-report.md | 6 +----- packages/catalog-client/src/CatalogClient.ts | 2 +- packages/catalog-client/src/types/api.ts | 13 ------------- .../src/lib/catalog/CatalogIdentityClient.test.ts | 1 - plugins/badges-backend/src/service/router.test.ts | 1 - .../CatalogGraphCard/CatalogGraphCard.test.tsx | 1 - .../CatalogGraphPage/CatalogGraphPage.test.tsx | 1 - .../EntityRelationsGraph.test.tsx | 1 - .../EntityRelationsGraph/useEntityStore.test.ts | 1 - .../src/api/CatalogImportClient.test.ts | 1 - .../StepPrepareCreatePullRequest.test.tsx | 1 - .../DefaultExplorePage/DefaultExplorePage.test.tsx | 1 - .../DomainExplorerContent.test.tsx | 1 - .../GroupsExplorerContent.test.tsx | 1 - .../src/components/FossaPage/FossaPage.test.tsx | 1 - 17 files changed, 17 insertions(+), 31 deletions(-) create mode 100644 .changeset/olive-emus-speak.md create mode 100644 .changeset/thick-gifts-cheat.md diff --git a/.changeset/olive-emus-speak.md b/.changeset/olive-emus-speak.md new file mode 100644 index 0000000000..3cb6a775cf --- /dev/null +++ b/.changeset/olive-emus-speak.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-badges-backend': patch +'@backstage/plugin-catalog-graph': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-explore': patch +'@backstage/plugin-fossa': patch +--- + +Remove usages of now-removed `CatalogApi.getEntityByName` diff --git a/.changeset/thick-gifts-cheat.md b/.changeset/thick-gifts-cheat.md new file mode 100644 index 0000000000..00154a8492 --- /dev/null +++ b/.changeset/thick-gifts-cheat.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': minor +--- + +**BREAKING**: Removed previously deprecated `CatalogApi.getEntityByName`, please use `getEntityByRef` instead. diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index d85b9c4d0d..ac8f712e11 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -38,11 +38,6 @@ export interface CatalogApi { request: GetEntityAncestorsRequest, options?: CatalogRequestOptions, ): Promise; - // @deprecated - getEntityByName( - name: CompoundEntityRef, - options?: CatalogRequestOptions, - ): Promise; getEntityByRef( entityRef: string | CompoundEntityRef, options?: CatalogRequestOptions, @@ -95,6 +90,7 @@ export class CatalogClient implements CatalogApi { request: GetEntityAncestorsRequest, options?: CatalogRequestOptions, ): Promise; + // @deprecated (undocumented) getEntityByName( compoundName: CompoundEntityRef, options?: CatalogRequestOptions, diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index 53397d061e..85aada616c 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -190,7 +190,7 @@ export class CatalogClient implements CatalogApi { // longer, to minimize the risk for breakages. Suggested date for removal: // August 2022 /** - * {@inheritdoc CatalogApi.getEntityByName} + * @deprecated Use getEntityByRef instead */ async getEntityByName( compoundName: CompoundEntityRef, diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index 26af3889e2..ba0c5e9359 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -315,19 +315,6 @@ export interface CatalogApi { options?: CatalogRequestOptions, ): Promise; - /** - * Gets a single entity from the catalog by its ref (kind, namespace, name) - * triplet. - * - * @deprecated Use getEntityRef instead - * @param name - A complete entity ref - * @param options - Additional options - */ - getEntityByName( - name: CompoundEntityRef, - options?: CatalogRequestOptions, - ): Promise; - /** * Removes a single entity from the catalog by entity UID. * diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index 08c46f2c21..b99c8a7615 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -27,7 +27,6 @@ describe('CatalogIdentityClient', () => { const catalogApi: jest.Mocked = { getLocationById: jest.fn(), getEntityByRef: jest.fn(), - getEntityByName: jest.fn(), getEntities: jest.fn(), addLocation: jest.fn(), removeLocationById: jest.fn(), diff --git a/plugins/badges-backend/src/service/router.test.ts b/plugins/badges-backend/src/service/router.test.ts index c492449cb3..1750a55be7 100644 --- a/plugins/badges-backend/src/service/router.test.ts +++ b/plugins/badges-backend/src/service/router.test.ts @@ -61,7 +61,6 @@ describe('createRouter', () => { addLocation: jest.fn(), getEntities: jest.fn(), getEntityByRef: jest.fn(), - getEntityByName: jest.fn(), getLocationByRef: jest.fn(), getLocationById: jest.fn(), removeLocationById: jest.fn(), diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx index 858fca1a83..9fbe110811 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx @@ -58,7 +58,6 @@ describe('', () => { catalog = { getEntities: jest.fn(), getEntityByRef: jest.fn(async _ => ({ ...entity, relations: [] })), - getEntityByName: jest.fn(), removeEntityByUid: jest.fn(), getLocationById: jest.fn(), getLocationByRef: jest.fn(), diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx index 0bf6469123..9453d5a000 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx @@ -91,7 +91,6 @@ describe('', () => { getEntityByRef: jest.fn(async (n: any) => n === 'b:d/e' ? entityE : entityC, ), - getEntityByName: jest.fn(), removeEntityByUid: jest.fn(), getLocationById: jest.fn(), getLocationByRef: jest.fn(), diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx index 58ff81a91f..e8c9239739 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx @@ -156,7 +156,6 @@ describe('', () => { catalog = { getEntities: jest.fn(), getEntityByRef: jest.fn(async n => entities[n as string]), - getEntityByName: jest.fn(), removeEntityByUid: jest.fn(), getLocationById: jest.fn(), getLocationByRef: jest.fn(), diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts index 5349b64a72..9956137287 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts @@ -30,7 +30,6 @@ describe('useEntityStore', () => { catalogApi = { getEntities: jest.fn(), getEntityByRef: jest.fn(), - getEntityByName: jest.fn(), removeEntityByUid: jest.fn(), getLocationById: jest.fn(), getLocationByRef: jest.fn(), diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index 969ff25c00..5a65738d61 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -94,7 +94,6 @@ describe('CatalogImportClient', () => { addLocation: jest.fn(), removeLocationById: jest.fn(), getEntityByRef: jest.fn(), - getEntityByName: jest.fn(), getLocationByRef: jest.fn(), getLocationById: jest.fn(), removeEntityByUid: jest.fn(), diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx index dd8087c51e..ea2bee9425 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx @@ -39,7 +39,6 @@ describe('', () => { getEntities: jest.fn(), addLocation: jest.fn(), getEntityByRef: jest.fn(), - getEntityByName: jest.fn(), getLocationByRef: jest.fn(), getLocationById: jest.fn(), removeLocationById: jest.fn(), diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx index c490dd00da..a79d02a2a0 100644 --- a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx @@ -29,7 +29,6 @@ describe('', () => { removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), getEntityByRef: jest.fn(), - getEntityByName: jest.fn(), refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), getEntityFacets: jest.fn(), diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx index d103541333..c30e845112 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx @@ -30,7 +30,6 @@ describe('', () => { removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), getEntityByRef: jest.fn(), - getEntityByName: jest.fn(), refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), getEntityFacets: jest.fn(), diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx index 99500336cf..4488a4d0d9 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx @@ -30,7 +30,6 @@ describe('', () => { removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), getEntityByRef: jest.fn(), - getEntityByName: jest.fn(), refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), getEntityFacets: jest.fn(), diff --git a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx index f178b7de7f..e6cf65771a 100644 --- a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx +++ b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx @@ -30,7 +30,6 @@ describe('', () => { addLocation: jest.fn(), getEntities: jest.fn(), getEntityByRef: jest.fn(), - getEntityByName: jest.fn(), getLocationByRef: jest.fn(), getLocationById: jest.fn(), removeEntityByUid: jest.fn(), From 53c16b506d4728ed49ff3833ce4d7db905dd65dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sat, 5 Mar 2022 16:01:18 +0100 Subject: [PATCH 237/353] switch around the ldap org docs to prefer provider over processor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- docs/integrations/ldap/org.md | 268 ++++++++++++++++++---------------- 1 file changed, 143 insertions(+), 125 deletions(-) diff --git a/docs/integrations/ldap/org.md b/docs/integrations/ldap/org.md index 75e29687d7..645524493b 100644 --- a/docs/integrations/ldap/org.md +++ b/docs/integrations/ldap/org.md @@ -14,9 +14,11 @@ entities that mirror your org setup. ## Installation -1. The processor is not installed by default, therefore you have to add a - dependency to `@backstage/plugin-catalog-backend-module-ldap` to your backend - package. +This guide will use the Entity Provider method. If you for some reason prefer +the Processor method (not recommended), it is described separately below. + +The provider is not installed by default, therefore you have to add a dependency +to `@backstage/plugin-catalog-backend-module-ldap` to your backend package. ```bash # From your Backstage root directory @@ -24,63 +26,99 @@ cd packages/backend yarn add @backstage/plugin-catalog-backend-module-ldap ``` -2. The `LdapOrgReaderProcessor` is not registered by default, so you have to - register it in the catalog plugin: +> Note: When configuring to use a Provider instead of a Processor you do not +> need to add a _location_ pointing to your LDAP server -```typescript +Update the catalog plugin initialization in your backend to add the provider and +schedule it: + +```ts // packages/backend/src/plugins/catalog.ts -builder.addProcessor( - LdapOrgReaderProcessor.fromConfig(env.config, { +import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; +import { Duration } from 'luxon'; +import { LdapOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-ldap'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + // The target parameter below needs to match the ldap.providers.target + // value specified in your app-config + const ldapEntityProvider = LdapOrgEntityProvider.fromConfig(env.config, { + id: 'our-ldap-master', + target: 'ldaps://ds.example.net', logger: env.logger, - }), -); + }); + + const builder = await CatalogBuilder.create(env); + builder.addEntityProvider(ldapEntityProvider); + + // You can change the refresh interval for the other catalog entries + // independently, or just leave the line below out to use the default + // refresh interval. Note that this interval does NOT at all affect + // the LDAP refresh when using the provider method, which is good! + builder.setRefreshIntervalSeconds(100); + + const { processingEngine, router } = await builder.build(); + await processingEngine.start(); + + // Only perform this scheduling after starting the processing engine + await env.scheduler.scheduleTask({ + id: 'refresh_ldap', + // frequency sets how often you want to ingest users and groups from + // LDAP, in this case every 60 minutes + frequency: Duration.fromObject({ minutes: 60 }), + timeout: Duration.fromObject({ minutes: 15 }), + fn: async () => { + try { + await ldapEntityProvider.read(); + } catch (error) { + env.logger.error(error); + } + }, + }); + + return router; +} ``` +After this, you also have to add some configuration in your app-config that +describes what you want to import for that target. + ## Configuration The following configuration is a small example of how a setup could look for importing groups and users from a corporate LDAP server. ```yaml -catalog: - locations: - - type: ldap-org - target: ldaps://ds.example.net - processors: - ldapOrg: - providers: - - target: ldaps://ds.example.net - bind: - dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net - secret: ${LDAP_SECRET} - users: - dn: ou=people,ou=example,dc=example,dc=net - options: - filter: (uid=*) - map: - description: l - set: - metadata.customField: 'hello' - groups: - dn: ou=access,ou=groups,ou=example,dc=example,dc=net - options: - filter: (&(objectClass=some-group-class)(!(groupType=email))) - map: - description: l - set: - metadata.customField: 'hello' +ldap: + providers: + - target: ldaps://ds.example.net + bind: + dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net + secret: ${LDAP_SECRET} + users: + dn: ou=people,ou=example,dc=example,dc=net + options: + filter: (uid=*) + map: + description: l + set: + metadata.customField: 'hello' + groups: + dn: ou=access,ou=groups,ou=example,dc=example,dc=net + options: + filter: (&(objectClass=some-group-class)(!(groupType=email))) + map: + description: l + set: + metadata.customField: 'hello' ``` -Locations point out the specific org(s) you want to import. The `type` of these -locations must be `ldap-org`, and the `target` must point to the exact URL -(starting with `ldap://` or `ldaps://`) of the targeted LDAP server. You can -have several such location entries if you want, but typically you will have just -one. - -The processor itself is configured in the other block, under -`catalog.processors.ldapOrg`. There may be many providers, each targeting a -specific `target` which is supposed to be on the same form as the location -`target`. +There may be many providers, each targeting a specific `target` which is +supposed to match the `target` of a dedicated provider instance - i.e., you will +add one entity provider class instance per target to ingest from. These config blocks have a lot of options in them, so we will describe each "root" key within the block separately. @@ -163,7 +201,7 @@ below, with their default values, but they are all optional. If you leave out an optional mapping, it will still be copied using that default value. For example, even if you do not put in the field `displayName` in your -config, the processor will still copy the attribute `cn` into the entity field +config, the provider will still copy the attribute `cn` into the entity field `spec.profile.displayName`. ```yaml @@ -245,7 +283,7 @@ shown below, with their default values, but they are all optional. If you leave out an optional mapping, it will still be copied using that default value. For example, even if you do not put in the field `displayName` in your -config, the processor will still copy the attribute `cn` into the entity field +config, the provider will still copy the attribute `cn` into the entity field `spec.profile.displayName`. If the target field is optional, such as the display name, the importer will accept missing attributes and just leave the target field unset. If the target field is mandatory, such as the name of the entity, @@ -283,94 +321,74 @@ map: members: member ``` -## Customize the Processor +## Customize the Provider -In case you want to customize the ingested entities, the -`LdapOrgReaderProcessor` allows to pass transformers for users and groups. +In case you want to customize the ingested entities, the provider allows to pass +transformers for users and groups. Here we will show an example of overriding +the group transformer. 1. Create a transformer: -```ts -export async function myGroupTransformer( - vendor: LdapVendor, - config: GroupConfig, - group: SearchEntry, -): Promise { - // Transformations may change namespace, change entity naming pattern, fill - // profile with more or other details... + ```ts + export async function myGroupTransformer( + vendor: LdapVendor, + config: GroupConfig, + group: SearchEntry, + ): Promise { + // Transformations may change namespace, change entity naming pattern, fill + // profile with more or other details... - // Create the group entity on your own, or wrap the default transformer - return await defaultGroupTransformer(vendor, config, group); -} -``` + // Create the group entity on your own, or wrap the default transformer + return await defaultGroupTransformer(vendor, config, group); + } + ``` -2. Configure the processor with the transformer: +2. Configure the provider with the transformer: -```ts + ```ts + const ldapEntityProvider = LdapOrgEntityProvider.fromConfig(env.config, { + id: 'our-ldap-master', + target: 'ldaps://ds.example.net', + logger: env.logger, + groupTransformer: myGroupTransformer, + }); + ``` + +## Using a Processor instead of a Provider + +An alternative to using the Provider for ingesting LDAP entries is to use a +Processor. This is the old way that's based on registering locations with the +proper type and target, triggering the processor to run. + +The drawback of this method is that it will leave orphaned Group/User entities +whenever they are deleted on your LDAP server, and you cannot control the +frequency with which they are refreshed, separately from other processors. + +### Processor Installation + +The `LdapOrgReaderProcessor` is not registered by default, so you have to +register it in the catalog plugin: + +```typescript +// packages/backend/src/plugins/catalog.ts builder.addProcessor( - LdapOrgReaderProcessor.fromConfig(config, { - logger, - groupTransformer: myGroupTransformer, + LdapOrgReaderProcessor.fromConfig(env.config, { + logger: env.logger, }), ); ``` -## Using a Provider instead of a Processor +### Driving LDAP Org Processor Ingestion with Locations -An alternative to using the Processor for ingesting LDAP entries is to use a -Provider. Doing this can give you a little bit more freedom to handle the LDAP -ingestion more independently from the rest of the catalog ingestion. +Locations point out the specific org(s) you want to import. The `type` of these +locations must be `ldap-org`, and the `target` must point to the exact URL +(starting with `ldap://` or `ldaps://`) of the targeted LDAP server. You can +have several such location entries if you want, but typically you will have just +one. -This can be useful if you have a lot of Users and Groups and hitting your LDAP -server is resource intensive but you still want your other catalog entries to be -updated frequently. - -> Note: When configuring to use a Provider instead of a Processor you do not -> need to add a _location_ pointing to your LDAP server - -```ts -// packages/backend/src/plugins/catalog.ts -import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; -import { PluginEnvironment } from '../types'; - -import { Router } from 'express'; -import { LdapOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-ldap'; - -import { Duration } from 'luxon'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - const ldapEntityProvider = LdapOrgEntityProvider.fromConfig(env.config, { - id: 'custom-ldap', - // target needs to match the catalog.processors.ldapOrg.providers.target specified in app-config - target: 'ldaps://ds.example.net', - logger: env.logger, - }); - - const builder = await CatalogBuilder.create(env); - builder.addEntityProvider(ldapEntityProvider); - - // You can change the refresh interval for the other catalog entries independently, or just leave the line below out to use the default refresh interval - builder.setRefreshIntervalSeconds(100); - - const { processingEngine, router } = await builder.build(); - await processingEngine.start(); - - await env.scheduler.scheduleTask({ - id: 'refresh_ldap', - // frequency sets how often you want to ingest users and groups from LDAP, in this case every 60 minutes - frequency: Duration.fromObject({ minutes: 60 }), - timeout: Duration.fromObject({ minutes: 15 }), - fn: async () => { - try { - await ldapEntityProvider.read(); - } catch (error) { - env.logger.error(error); - } - }, - }); - - return router; -} +```yaml +catalog: + locations: + - type: ldap-org + target: ldaps://ds.example.net ``` From 5b797098b57824fe10475e5f795e3100b395877d Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 5 Mar 2022 09:33:45 -0600 Subject: [PATCH 238/353] Added example of icon for theme Signed-off-by: Andre Wanlin --- docs/getting-started/app-custom-theme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md index 34eca2429a..b661afcdb1 100644 --- a/docs/getting-started/app-custom-theme.md +++ b/docs/getting-started/app-custom-theme.md @@ -57,6 +57,7 @@ done like this: import { createApp } from '@backstage/app-defaults'; import { ThemeProvider } from '@material-ui/core/styles'; import CssBaseline from '@material-ui/core/CssBaseline'; +import LightIcon from '@material-ui/icons/WbSunny'; const app = createApp({ apis: ..., @@ -65,6 +66,7 @@ const app = createApp({ id: 'my-theme', title: 'My Custom Theme', variant: 'light', + icon: , Provider: ({ children }) => ( {children} From f115a7f8fd9bc30c9905b851050c59dfe1190bc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sat, 5 Mar 2022 15:16:22 +0100 Subject: [PATCH 239/353] moved AwsS3DiscoveryProcessor to where it should live MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/fair-ants-look.md | 5 +++++ .changeset/twenty-planes-dress.md | 5 +++++ docs/integrations/aws-s3/discovery.md | 2 +- .../catalog-backend-module-aws/api-report.md | 16 ++++++++++++++++ plugins/catalog-backend-module-aws/package.json | 5 ++++- .../processors}/AwsS3DiscoveryProcessor.test.ts | 17 ++++++++++++++--- .../src/processors}/AwsS3DiscoveryProcessor.ts | 12 +++++++++--- .../__fixtures__/awsS3-mock-object.txt | 0 .../src/processors/index.ts | 1 + plugins/catalog-backend/api-report.md | 14 -------------- plugins/catalog-backend/package.json | 2 -- .../catalog-backend/src/modules/aws/index.ts | 17 ----------------- plugins/catalog-backend/src/modules/index.ts | 1 - 13 files changed, 55 insertions(+), 42 deletions(-) create mode 100644 .changeset/fair-ants-look.md create mode 100644 .changeset/twenty-planes-dress.md rename plugins/{catalog-backend/src/modules/aws => catalog-backend-module-aws/src/processors}/AwsS3DiscoveryProcessor.test.ts (85%) rename plugins/{catalog-backend/src/modules/aws => catalog-backend-module-aws/src/processors}/AwsS3DiscoveryProcessor.ts (89%) rename plugins/{catalog-backend/src/modules/aws => catalog-backend-module-aws/src/processors}/__fixtures__/awsS3-mock-object.txt (100%) delete mode 100644 plugins/catalog-backend/src/modules/aws/index.ts diff --git a/.changeset/fair-ants-look.md b/.changeset/fair-ants-look.md new file mode 100644 index 0000000000..4e70a042f8 --- /dev/null +++ b/.changeset/fair-ants-look.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +**BREAKING**: Removed `AwsS3DiscoveryProcessor`, which now instead should be imported from `@backstage/plugin-catalog-backend-module-aws`. diff --git a/.changeset/twenty-planes-dress.md b/.changeset/twenty-planes-dress.md new file mode 100644 index 0000000000..4d1a5dc1d1 --- /dev/null +++ b/.changeset/twenty-planes-dress.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-aws': patch +--- + +Added `AwsS3DiscoveryProcessor`, which was moved here from `@backstage/plugin-catalog-backend` where it previously resided. diff --git a/docs/integrations/aws-s3/discovery.md b/docs/integrations/aws-s3/discovery.md index 545a578e63..4e72c80b95 100644 --- a/docs/integrations/aws-s3/discovery.md +++ b/docs/integrations/aws-s3/discovery.md @@ -33,7 +33,7 @@ the below to `packages/backend/src/plugins/catalog.ts`: ```ts /* packages/backend/src/plugins/catalog.ts */ -import { AwsS3DiscoveryProcessor } from '@backstage/plugin-catalog-backend'; +import { AwsS3DiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-aws'; const builder = await CatalogBuilder.create(env); /** ... other processors ... */ diff --git a/plugins/catalog-backend-module-aws/api-report.md b/plugins/catalog-backend-module-aws/api-report.md index 1cab6555fe..ef58210e11 100644 --- a/plugins/catalog-backend-module-aws/api-report.md +++ b/plugins/catalog-backend-module-aws/api-report.md @@ -5,9 +5,11 @@ ```ts import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; +import { CatalogProcessorParser } from '@backstage/plugin-catalog-backend'; import { Config } from '@backstage/config'; import { LocationSpec } from '@backstage/plugin-catalog-backend'; import { Logger as Logger_2 } from 'winston'; +import { UrlReader } from '@backstage/backend-common'; // @public export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { @@ -27,4 +29,18 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { emit: CatalogProcessorEmit, ): Promise; } + +// @public +export class AwsS3DiscoveryProcessor implements CatalogProcessor { + constructor(reader: UrlReader); + // (undocumented) + getProcessorName(): string; + // (undocumented) + readLocation( + location: LocationSpec, + optional: boolean, + emit: CatalogProcessorEmit, + parser: CatalogProcessorParser, + ): Promise; +} ``` diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index 2ea57652cc..3f8149a6f7 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -33,6 +33,7 @@ "start": "backstage-cli package start" }, "dependencies": { + "@backstage/backend-common": "^0.12.0", "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", @@ -40,12 +41,14 @@ "@backstage/types": "^0.1.3", "aws-sdk": "^2.840.0", "lodash": "^4.17.21", + "p-limit": "^3.0.2", "winston": "^3.2.1" }, "devDependencies": { "@backstage/cli": "^0.15.0", "@types/lodash": "^4.14.151", - "aws-sdk-mock": "^5.2.1" + "aws-sdk-mock": "^5.2.1", + "yaml": "^1.9.2" }, "files": [ "dist", diff --git a/plugins/catalog-backend/src/modules/aws/AwsS3DiscoveryProcessor.test.ts b/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.test.ts similarity index 85% rename from plugins/catalog-backend/src/modules/aws/AwsS3DiscoveryProcessor.test.ts rename to plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.test.ts index 249de71591..6aab7b2bec 100644 --- a/plugins/catalog-backend/src/modules/aws/AwsS3DiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.test.ts @@ -20,11 +20,12 @@ import { AwsS3DiscoveryProcessor } from './AwsS3DiscoveryProcessor'; import { CatalogProcessorEntityResult, CatalogProcessorResult, -} from '../../api'; -import { defaultEntityDataParser } from '../util/parse'; + processingResult, +} from '@backstage/plugin-catalog-backend'; import AWSMock from 'aws-sdk-mock'; import aws from 'aws-sdk'; import path from 'path'; +import YAML from 'yaml'; AWSMock.setSDKInstance(aws); const object: aws.S3.Types.Object = { @@ -62,7 +63,17 @@ describe('readLocation', () => { it('should load from url', async () => { const generated = (await new Promise(emit => - processor.readLocation(spec, false, emit, defaultEntityDataParser), + processor.readLocation( + spec, + false, + emit, + async function* r({ data, location }) { + yield processingResult.entity( + location, + YAML.parse(data.toString('utf8')) as any, + ); + }, + ), )) as CatalogProcessorEntityResult; expect(generated.type).toBe('entity'); expect(generated.location).toEqual({ diff --git a/plugins/catalog-backend/src/modules/aws/AwsS3DiscoveryProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.ts similarity index 89% rename from plugins/catalog-backend/src/modules/aws/AwsS3DiscoveryProcessor.ts rename to plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.ts index 737f8f7c2f..0390defb2a 100644 --- a/plugins/catalog-backend/src/modules/aws/AwsS3DiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.ts @@ -16,16 +16,22 @@ import { UrlReader } from '@backstage/backend-common'; import { isError } from '@backstage/errors'; -import limiterFactory from 'p-limit'; import { CatalogProcessor, CatalogProcessorEmit, CatalogProcessorParser, LocationSpec, processingResult, -} from '../../api'; +} from '@backstage/plugin-catalog-backend'; +import limiterFactory from 'p-limit'; -/** @public */ +/** + * A processor for automatic discovery of entities from S3 buckets. Handles the + * `s3-discovery` location type, and target bucket URLs e.g. on the form + * `https://testbucket.s3.us-east-2.amazonaws.com`. + * + * @public + */ export class AwsS3DiscoveryProcessor implements CatalogProcessor { constructor(private readonly reader: UrlReader) {} diff --git a/plugins/catalog-backend/src/modules/aws/__fixtures__/awsS3-mock-object.txt b/plugins/catalog-backend-module-aws/src/processors/__fixtures__/awsS3-mock-object.txt similarity index 100% rename from plugins/catalog-backend/src/modules/aws/__fixtures__/awsS3-mock-object.txt rename to plugins/catalog-backend-module-aws/src/processors/__fixtures__/awsS3-mock-object.txt diff --git a/plugins/catalog-backend-module-aws/src/processors/index.ts b/plugins/catalog-backend-module-aws/src/processors/index.ts index fa858aa744..8718e77a6f 100644 --- a/plugins/catalog-backend-module-aws/src/processors/index.ts +++ b/plugins/catalog-backend-module-aws/src/processors/index.ts @@ -15,3 +15,4 @@ */ export { AwsOrganizationCloudAccountProcessor } from './AwsOrganizationCloudAccountProcessor'; +export { AwsS3DiscoveryProcessor } from './AwsS3DiscoveryProcessor'; diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 64ab27ba25..922e1d2741 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -97,20 +97,6 @@ export class AnnotateScmSlugEntityProcessor implements CatalogProcessor { preProcessEntity(entity: Entity, location: LocationSpec): Promise; } -// @public (undocumented) -export class AwsS3DiscoveryProcessor implements CatalogProcessor { - constructor(reader: UrlReader); - // (undocumented) - getProcessorName(): string; - // (undocumented) - readLocation( - location: LocationSpec, - optional: boolean, - emit: CatalogProcessorEmit, - parser: CatalogProcessorParser, - ): Promise; -} - // @public export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { constructor(options: { diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 9b8e6347a5..abc7f85427 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -48,7 +48,6 @@ "@backstage/types": "^0.1.3", "@octokit/graphql": "^4.5.8", "@types/express": "^4.17.6", - "aws-sdk": "^2.840.0", "codeowners-utils": "^1.0.2", "core-js": "^3.6.5", "express": "^4.17.1", @@ -81,7 +80,6 @@ "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", "@vscode/sqlite3": "^5.0.7", - "aws-sdk-mock": "^5.2.1", "msw": "^0.35.0", "supertest": "^6.1.3", "wait-for-expect": "^3.0.2", diff --git a/plugins/catalog-backend/src/modules/aws/index.ts b/plugins/catalog-backend/src/modules/aws/index.ts deleted file mode 100644 index 9477d4104d..0000000000 --- a/plugins/catalog-backend/src/modules/aws/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * 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 { AwsS3DiscoveryProcessor } from './AwsS3DiscoveryProcessor'; diff --git a/plugins/catalog-backend/src/modules/index.ts b/plugins/catalog-backend/src/modules/index.ts index 6acf057ca3..1cb7e3007b 100644 --- a/plugins/catalog-backend/src/modules/index.ts +++ b/plugins/catalog-backend/src/modules/index.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -export * from './aws'; export * from './azure'; export * from './bitbucket'; export * from './codeowners'; From 4f79204c47ac22aac0d116278364ef412b9e6467 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 5 Mar 2022 10:55:44 +0100 Subject: [PATCH 240/353] plugins: migrate airbrake-backend and code-climate to package roles Signed-off-by: Patrik Oldsberg --- .changeset/proud-readers-nail.md | 6 ++++++ plugins/airbrake-backend/package.json | 17 ++++++++++------- plugins/code-climate/package.json | 17 ++++++++++------- 3 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 .changeset/proud-readers-nail.md diff --git a/.changeset/proud-readers-nail.md b/.changeset/proud-readers-nail.md new file mode 100644 index 0000000000..9b5a6b980c --- /dev/null +++ b/.changeset/proud-readers-nail.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-airbrake-backend': patch +'@backstage/plugin-code-climate': patch +--- + +Added `backstage.role` to `package.json` diff --git a/plugins/airbrake-backend/package.json b/plugins/airbrake-backend/package.json index 831578dd9e..c086a822ae 100644 --- a/plugins/airbrake-backend/package.json +++ b/plugins/airbrake-backend/package.json @@ -9,14 +9,17 @@ "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "backend-plugin" + }, "scripts": { - "start": "backstage-cli backend:dev", - "build": "backstage-cli backend:build", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { "@backstage/backend-common": "^0.12.0", diff --git a/plugins/code-climate/package.json b/plugins/code-climate/package.json index 567ec6b4b1..b5a82d727a 100644 --- a/plugins/code-climate/package.json +++ b/plugins/code-climate/package.json @@ -9,15 +9,18 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "backstage": { + "role": "frontend-plugin" + }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { "@backstage/catalog-model": "^0.12.0", From 2d9cfec088a4d9b9399b36c399a78e8fe8804247 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 5 Mar 2022 13:09:10 +0100 Subject: [PATCH 241/353] root: update scripts to use repo commands Signed-off-by: Patrik Oldsberg --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e9101c2280..b9a9a0f33c 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "dev": "concurrently \"yarn start\" \"yarn start-backend\"", "start": "yarn workspace example-app start", "start-backend": "yarn workspace example-backend start", - "build": "lerna run build", + "build": "backstage-cli repo build --all", "build:api-reports": "yarn build:api-reports:only --tsc", "build:api-reports:only": "ts-node -T -P scripts/tsconfig.json scripts/api-extractor.ts", "build:api-docs": "yarn build:api-reports --docs", @@ -18,9 +18,9 @@ "diff": "lerna run diff --", "test": "backstage-cli test", "test:all": "lerna run test -- --coverage", - "lint": "lerna run lint --since origin/master --", + "lint": "backstage-cli repo lint --since origin/master", "lint:docs": "node ./scripts/check-docs-quality", - "lint:all": "lerna run lint --", + "lint:all": "backstage-cli repo lint", "lint:type-deps": "node scripts/check-type-dependencies.js", "docker-build": "yarn tsc && yarn workspace example-backend build --build-dependencies && yarn workspace example-backend build-image", "backstage-create": "backstage-cli create --scope backstage --no-private", From de1e46c9b6f40b2757aa02165fe89f8bfe33e306 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 5 Mar 2022 13:09:37 +0100 Subject: [PATCH 242/353] workflows: switch to using repo commands Signed-off-by: Patrik Oldsberg --- .github/workflows/ci.yml | 6 +++--- .github/workflows/deploy_nightly.yml | 2 +- .github/workflows/deploy_packages.yml | 10 +++++++--- .github/workflows/verify_e2e-linux.yml | 3 +-- .github/workflows/verify_e2e-windows.yml | 3 +-- .github/workflows/verify_windows.yml | 2 +- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49c97bb197..1f0c6e7602 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,7 +119,7 @@ jobs: run: yarn backstage-cli config:check --lax - name: lint - run: yarn lerna -- run lint --since origin/master + run: yarn backstage-cli repo lint --since origin/master - name: type checking and declarations run: yarn tsc:full @@ -139,11 +139,11 @@ jobs: - name: build changed packages if: ${{ steps.yarn-lock.outcome == 'success' }} - run: yarn lerna -- run build --since origin/master --include-dependencies + run: yarn backstage-cli repo build --all --since origin/master - name: build all packages if: ${{ steps.yarn-lock.outcome == 'failure' }} - run: yarn lerna -- run build + run: yarn backstage-cli repo build --all - name: verify type dependencies run: yarn lint:type-deps diff --git a/.github/workflows/deploy_nightly.yml b/.github/workflows/deploy_nightly.yml index 4997668563..ab4c9bcfdb 100644 --- a/.github/workflows/deploy_nightly.yml +++ b/.github/workflows/deploy_nightly.yml @@ -56,7 +56,7 @@ jobs: run: yarn tsc - name: build - run: yarn build + run: yarn backstage-cli repo build # Prepares a nightly release version of any package with pending changesets # Pre-mode is exited if case we're in it, otherwise it has no effect diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index 9b7a233767..fd9ed216be 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -102,13 +102,13 @@ jobs: run: yarn backstage-cli config:check --lax - name: lint - run: yarn lerna -- run lint + run: yarn backstage-cli repo lint - name: type checking and declarations run: yarn tsc:full - name: build - run: yarn build + run: yarn backstage-cli repo build --all - name: verify type dependencies run: yarn lint:type-deps @@ -188,7 +188,11 @@ jobs: run: yarn tsc:full - name: build packages - run: yarn lerna -- run --ignore example-app --ignore example-backend build + run: yarn backstage-cli repo build + + - name: build embedded techdocs app + working-directory: packages/techdocs-cli-embedded-app + run: yarn build # Publishes current version of packages that are not already present in the registry - name: publish diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index 3e39aee1ae..e333aaba0c 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -71,8 +71,7 @@ jobs: # End of yarn setup - run: yarn tsc - - name: yarn build - run: yarn build --ignore example-app --ignore example-backend --ignore @techdocs/cli + - run: yarn backstage-cli repo build - name: run E2E test run: | sudo sysctl fs.inotify.max_user_watches=524288 diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index 0977831d19..bc6bc8a698 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -50,8 +50,7 @@ jobs: run: yarn install --frozen-lockfile - run: yarn tsc - - name: yarn build - run: yarn build --ignore example-app --ignore example-backend --ignore @techdocs/cli + - run: yarn backstage-cli repo build - name: run E2E test run: yarn e2e-test run env: diff --git a/.github/workflows/verify_windows.yml b/.github/workflows/verify_windows.yml index 9ae4397bba..5326e293d3 100644 --- a/.github/workflows/verify_windows.yml +++ b/.github/workflows/verify_windows.yml @@ -47,7 +47,7 @@ jobs: # End of yarn setup - name: lint - run: yarn lerna -- run lint + run: yarn backstage-cli repo lint - name: type checking and declarations run: yarn tsc:full From dc6002a7b93481c9cc6cee3ec82cef6d6a56c8be Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 5 Mar 2022 17:23:11 +0100 Subject: [PATCH 243/353] cli: fall back --since diff to use ref if merge base lookup fails Signed-off-by: Patrik Oldsberg --- .changeset/warm-bananas-behave.md | 5 +++++ packages/cli/src/lib/git.ts | 11 +++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 .changeset/warm-bananas-behave.md diff --git a/.changeset/warm-bananas-behave.md b/.changeset/warm-bananas-behave.md new file mode 100644 index 0000000000..b1b6b4c8ec --- /dev/null +++ b/.changeset/warm-bananas-behave.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The `--since` flag of repo commands now silently falls back to using the provided `ref` directly if no merge base is available. diff --git a/packages/cli/src/lib/git.ts b/packages/cli/src/lib/git.ts index f8783804d1..074260b38f 100644 --- a/packages/cli/src/lib/git.ts +++ b/packages/cli/src/lib/git.ts @@ -50,9 +50,16 @@ export async function listChangedFiles(ref: string) { if (!ref) { throw new Error('ref is required'); } - const [base] = await runGit('merge-base', 'HEAD', ref); - const tracked = await runGit('diff', '--name-only', base); + let diffRef = ref; + try { + const [base] = await runGit('merge-base', 'HEAD', ref); + diffRef = base; + } catch { + // silently fall back to using the ref directly if merge base is not available + } + + const tracked = await runGit('diff', '--name-only', diffRef); const untracked = await runGit('ls-files', '--others', '--exclude-standard'); return Array.from(new Set([...tracked, ...untracked])); From 3c2bc7390122ab4fee0cdb1d37c283cd93b94ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sun, 6 Mar 2022 16:56:18 +0100 Subject: [PATCH 244/353] add setupRequestMockHandlers to backend-test-utils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/neat-tools-design.md | 5 ++++ .changeset/pink-poets-grin.md | 11 +++++++ packages/backend-common/package.json | 2 +- .../src/reading/AzureUrlReader.test.ts | 2 +- .../src/reading/BitbucketUrlReader.test.ts | 2 +- .../src/reading/FetchUrlReader.test.ts | 2 +- .../src/reading/GithubUrlReader.test.ts | 2 +- .../src/reading/GitlabUrlReader.test.ts | 2 +- packages/backend-test-utils/api-report.md | 7 +++++ packages/backend-test-utils/package.json | 1 + packages/backend-test-utils/src/index.ts | 1 + packages/backend-test-utils/src/msw/index.ts | 17 +++++++++++ .../src/msw/setupRequestMockHandlers.ts | 30 +++++++++++++++++++ plugins/auth-backend/package.json | 2 +- .../src/providers/microsoft/provider.test.ts | 2 +- .../src/providers/oidc/provider.test.ts | 2 +- .../package.json | 2 +- .../src/microsoftGraph/client.test.ts | 2 +- plugins/catalog-backend/package.json | 1 - .../src/modules/azure/lib/azure.test.ts | 2 +- .../modules/core/UrlReaderProcessor.test.ts | 2 +- .../src/modules/github/lib/github.test.ts | 2 +- .../src/modules/gitlab/lib/client.test.ts | 2 +- plugins/rollbar-backend/package.json | 2 +- .../src/api/RollbarApi.test.ts | 2 +- plugins/scaffolder-backend/package.json | 2 +- .../actions/builtin/publish/bitbucket.test.ts | 2 +- plugins/techdocs-backend/package.json | 2 +- .../search/DefaultTechDocsCollator.test.ts | 2 +- .../DefaultTechDocsCollatorFactory.test.ts | 2 +- 30 files changed, 94 insertions(+), 23 deletions(-) create mode 100644 .changeset/neat-tools-design.md create mode 100644 .changeset/pink-poets-grin.md create mode 100644 packages/backend-test-utils/src/msw/index.ts create mode 100644 packages/backend-test-utils/src/msw/setupRequestMockHandlers.ts diff --git a/.changeset/neat-tools-design.md b/.changeset/neat-tools-design.md new file mode 100644 index 0000000000..92a52f0751 --- /dev/null +++ b/.changeset/neat-tools-design.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Add `setupRequestMockHandlers` which sets up a good `msw` server foundation, copied from `@backstage/test-utils` which is a frontend-only package and should not be used from backends. diff --git a/.changeset/pink-poets-grin.md b/.changeset/pink-poets-grin.md new file mode 100644 index 0000000000..d88431e9d5 --- /dev/null +++ b/.changeset/pink-poets-grin.md @@ -0,0 +1,11 @@ +--- +'@backstage/backend-common': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-rollbar-backend': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-techdocs-backend': patch +--- + +Use `setupRequestMockHandlers` from `@backstage/backend-test-utils` diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 93ad63150b..748576acf1 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -89,8 +89,8 @@ } }, "devDependencies": { + "@backstage/backend-test-utils": "^0.1.20", "@backstage/cli": "^0.15.0", - "@backstage/test-utils": "^0.3.0", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", "@types/concat-stream": "^2.0.0", diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index 23de8389ad..2eb7a60095 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -19,7 +19,7 @@ import { AzureIntegration, readAzureIntegrationConfig, } from '@backstage/integration'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index 371e11b364..9580780196 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -19,7 +19,7 @@ import { BitbucketIntegration, readBitbucketIntegrationConfig, } from '@backstage/integration'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; diff --git a/packages/backend-common/src/reading/FetchUrlReader.test.ts b/packages/backend-common/src/reading/FetchUrlReader.test.ts index 9d6edee765..d2fbbab668 100644 --- a/packages/backend-common/src/reading/FetchUrlReader.test.ts +++ b/packages/backend-common/src/reading/FetchUrlReader.test.ts @@ -16,7 +16,7 @@ import { ConfigReader } from '@backstage/config'; import { NotFoundError, NotModifiedError } from '@backstage/errors'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { getVoidLogger } from '../logging'; diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index ff73718d50..8e59b8d902 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -20,7 +20,7 @@ import { GitHubIntegration, readGitHubIntegrationConfig, } from '@backstage/integration'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index c8a7e17ae3..f422820a29 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -15,7 +15,7 @@ */ import { ConfigReader } from '@backstage/config'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index f31a2675b9..cba8953432 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -8,6 +8,13 @@ import { Knex } from 'knex'; // @public (undocumented) export function isDockerDisabledForTests(): boolean; +// @public +export function setupRequestMockHandlers(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}): void; + // @public export type TestDatabaseId = | 'POSTGRES_13' diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index a12cc67ab4..4a9f8cd903 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -39,6 +39,7 @@ "@backstage/config": "^0.1.15", "@vscode/sqlite3": "^5.0.7", "knex": "^1.0.2", + "msw": "^0.35.0", "mysql2": "^2.2.5", "pg": "^8.3.0", "testcontainers": "^8.1.2", diff --git a/packages/backend-test-utils/src/index.ts b/packages/backend-test-utils/src/index.ts index ee66b230a8..5fef4e6b55 100644 --- a/packages/backend-test-utils/src/index.ts +++ b/packages/backend-test-utils/src/index.ts @@ -21,4 +21,5 @@ */ export * from './database'; +export * from './msw'; export * from './util'; diff --git a/packages/backend-test-utils/src/msw/index.ts b/packages/backend-test-utils/src/msw/index.ts new file mode 100644 index 0000000000..29cbdf854a --- /dev/null +++ b/packages/backend-test-utils/src/msw/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { setupRequestMockHandlers } from './setupRequestMockHandlers'; diff --git a/packages/backend-test-utils/src/msw/setupRequestMockHandlers.ts b/packages/backend-test-utils/src/msw/setupRequestMockHandlers.ts new file mode 100644 index 0000000000..625ef0e0fc --- /dev/null +++ b/packages/backend-test-utils/src/msw/setupRequestMockHandlers.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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. + */ + +/** + * Sets up handlers for request mocking + * @public + * @param worker - service worker + */ +export function setupRequestMockHandlers(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}) { + beforeAll(() => worker.listen({ onUnhandledRequest: 'error' })); + afterAll(() => worker.close()); + afterEach(() => worker.resetHandlers()); +} diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 49cff317e3..104f2e2c95 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -76,8 +76,8 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-test-utils": "^0.1.20", "@backstage/cli": "^0.15.0", - "@backstage/test-utils": "^0.3.0", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/auth-backend/src/providers/microsoft/provider.test.ts b/plugins/auth-backend/src/providers/microsoft/provider.test.ts index 2734878077..3a0fc5ad86 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.test.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.test.ts @@ -20,7 +20,7 @@ import { OAuthResult } from '../../lib/oauth'; import { getVoidLogger } from '@backstage/backend-common'; import { TokenIssuer } from '../../identity/types'; import { CatalogIdentityClient } from '../../lib/catalog'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index da96ae1a25..d9902b600e 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -15,7 +15,7 @@ */ import { Config, ConfigReader } from '@backstage/config'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import express from 'express'; import { Session } from 'express-session'; import { JWK, JWT } from 'jose'; diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 463b0435c8..f9220ac41c 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -47,8 +47,8 @@ }, "devDependencies": { "@backstage/backend-common": "^0.12.0", + "@backstage/backend-test-utils": "^0.1.20", "@backstage/cli": "^0.15.0", - "@backstage/test-utils": "^0.3.0", "@types/lodash": "^4.14.151", "msw": "^0.35.0" }, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts index 94a126e1a7..467b03733e 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts @@ -15,7 +15,7 @@ */ import * as msal from '@azure/msal-node'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { MicrosoftGraphClient } from './client'; diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index abc7f85427..5863f9d56a 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -73,7 +73,6 @@ "@backstage/cli": "^0.15.0", "@backstage/plugin-permission-common": "^0.5.2", "@backstage/plugin-search-backend-node": "0.5.0", - "@backstage/test-utils": "^0.3.0", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", "@types/lodash": "^4.14.151", diff --git a/plugins/catalog-backend/src/modules/azure/lib/azure.test.ts b/plugins/catalog-backend/src/modules/azure/lib/azure.test.ts index 67cc120bab..7be1a12b54 100644 --- a/plugins/catalog-backend/src/modules/azure/lib/azure.test.ts +++ b/plugins/catalog-backend/src/modules/azure/lib/azure.test.ts @@ -15,7 +15,7 @@ */ import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { codeSearch, CodeSearchResponse } from './azure'; diff --git a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts index a96d392c52..dbcb9e34df 100644 --- a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts @@ -19,8 +19,8 @@ import { UrlReader, UrlReaders, } from '@backstage/backend-common'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { diff --git a/plugins/catalog-backend/src/modules/github/lib/github.test.ts b/plugins/catalog-backend/src/modules/github/lib/github.test.ts index b38153a193..fb4caa42c0 100644 --- a/plugins/catalog-backend/src/modules/github/lib/github.test.ts +++ b/plugins/catalog-backend/src/modules/github/lib/github.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { graphql } from '@octokit/graphql'; import { graphql as graphqlMsw } from 'msw'; import { setupServer } from 'msw/node'; diff --git a/plugins/catalog-backend/src/modules/gitlab/lib/client.test.ts b/plugins/catalog-backend/src/modules/gitlab/lib/client.test.ts index a7c0d5161f..e50ad34734 100644 --- a/plugins/catalog-backend/src/modules/gitlab/lib/client.test.ts +++ b/plugins/catalog-backend/src/modules/gitlab/lib/client.test.ts @@ -15,7 +15,7 @@ */ import { ConfigReader } from '@backstage/config'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { readGitLabIntegrationConfig } from '@backstage/integration'; import { getVoidLogger } from '@backstage/backend-common'; import { rest } from 'msw'; diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index 8331c547a3..2854370063 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -50,8 +50,8 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-test-utils": "^0.1.20", "@backstage/cli": "^0.15.0", - "@backstage/test-utils": "^0.3.0", "@types/supertest": "^2.0.8", "msw": "^0.36.3", "supertest": "^6.1.3" diff --git a/plugins/rollbar-backend/src/api/RollbarApi.test.ts b/plugins/rollbar-backend/src/api/RollbarApi.test.ts index 8936bb1839..430fc205c9 100644 --- a/plugins/rollbar-backend/src/api/RollbarApi.test.ts +++ b/plugins/rollbar-backend/src/api/RollbarApi.test.ts @@ -15,7 +15,7 @@ */ import { getRequestHeaders, RollbarApi } from './RollbarApi'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { getVoidLogger } from '@backstage/backend-common'; diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index f0822c27bb..2b96e72831 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -75,8 +75,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { + "@backstage/backend-test-utils": "^0.1.20", "@backstage/cli": "^0.15.0", - "@backstage/test-utils": "^0.3.0", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts index 96fd8184ca..ee989038c2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts @@ -19,7 +19,7 @@ jest.mock('../helpers'); import { createPublishBitbucketAction } from './bitbucket'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index b11e038879..1be2ac1465 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -55,9 +55,9 @@ "winston": "^3.2.1" }, "devDependencies": { + "@backstage/backend-test-utils": "^0.1.20", "@backstage/cli": "^0.15.0", "@backstage/plugin-search-backend-node": "0.5.0", - "@backstage/test-utils": "^0.3.0", "@types/dockerode": "^3.3.0", "msw": "^0.35.0", "supertest": "^6.1.3" diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts index 887dc60b1a..684d5729a2 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts @@ -21,7 +21,7 @@ import { } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { DefaultTechDocsCollator } from './DefaultTechDocsCollator'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { ConfigReader } from '@backstage/config'; diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts index fafaae71b9..291f21a0d1 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts @@ -21,7 +21,7 @@ import { import { Entity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { TestPipeline } from '@backstage/plugin-search-backend-node'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { Readable } from 'stream'; From 66ba5d9023805083519ac1b3df95840039d0fa77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sun, 6 Mar 2022 16:59:20 +0100 Subject: [PATCH 245/353] Create azure and gitlab catalog modules, move related processors into them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/clever-garlics-rescue.md | 20 +++++++ .changeset/rich-ravens-clap.md | 5 ++ .changeset/silent-cats-kneel.md | 5 ++ docs/integrations/gitlab/discovery.md | 16 ++++++ .../catalog-backend-module-azure/.eslintrc.js | 3 + .../catalog-backend-module-azure/README.md | 8 +++ .../api-report.md | 35 ++++++++++++ .../catalog-backend-module-azure/package.json | 56 +++++++++++++++++++ .../AzureDevOpsDiscoveryProcessor.test.ts | 4 +- .../src}/AzureDevOpsDiscoveryProcessor.ts | 4 +- .../catalog-backend-module-azure/src/index.ts | 23 ++++++++ .../src}/lib/azure.test.ts | 2 +- .../src}/lib/azure.ts | 0 .../src}/lib/index.ts | 0 .../src/setupTests.ts} | 4 +- .../.eslintrc.js | 3 + .../catalog-backend-module-gitlab/README.md | 8 +++ .../api-report.md | 30 ++++++++++ .../package.json | 56 +++++++++++++++++++ .../src}/GitLabDiscoveryProcessor.test.ts | 8 +-- .../src}/GitLabDiscoveryProcessor.ts | 26 ++++----- .../src/index.ts | 23 ++++++++ .../src}/lib/client.test.ts | 0 .../src}/lib/client.ts | 0 .../src}/lib/index.ts | 0 .../src}/lib/types.ts | 0 .../src/setupTests.ts} | 4 +- plugins/catalog-backend/api-report.md | 42 -------------- plugins/catalog-backend/src/modules/index.ts | 2 - .../src/service/CatalogBuilder.ts | 4 -- scripts/api-extractor.ts | 2 + 31 files changed, 319 insertions(+), 74 deletions(-) create mode 100644 .changeset/clever-garlics-rescue.md create mode 100644 .changeset/rich-ravens-clap.md create mode 100644 .changeset/silent-cats-kneel.md create mode 100644 plugins/catalog-backend-module-azure/.eslintrc.js create mode 100644 plugins/catalog-backend-module-azure/README.md create mode 100644 plugins/catalog-backend-module-azure/api-report.md create mode 100644 plugins/catalog-backend-module-azure/package.json rename plugins/{catalog-backend/src/modules/azure => catalog-backend-module-azure/src}/AzureDevOpsDiscoveryProcessor.test.ts (99%) rename plugins/{catalog-backend/src/modules/azure => catalog-backend-module-azure/src}/AzureDevOpsDiscoveryProcessor.ts (99%) create mode 100644 plugins/catalog-backend-module-azure/src/index.ts rename plugins/{catalog-backend/src/modules/azure => catalog-backend-module-azure/src}/lib/azure.test.ts (100%) rename plugins/{catalog-backend/src/modules/azure => catalog-backend-module-azure/src}/lib/azure.ts (100%) rename plugins/{catalog-backend/src/modules/azure => catalog-backend-module-azure/src}/lib/index.ts (100%) rename plugins/{catalog-backend/src/modules/gitlab/index.ts => catalog-backend-module-azure/src/setupTests.ts} (83%) create mode 100644 plugins/catalog-backend-module-gitlab/.eslintrc.js create mode 100644 plugins/catalog-backend-module-gitlab/README.md create mode 100644 plugins/catalog-backend-module-gitlab/api-report.md create mode 100644 plugins/catalog-backend-module-gitlab/package.json rename plugins/{catalog-backend/src/modules/gitlab => catalog-backend-module-gitlab/src}/GitLabDiscoveryProcessor.test.ts (99%) rename plugins/{catalog-backend/src/modules/gitlab => catalog-backend-module-gitlab/src}/GitLabDiscoveryProcessor.ts (99%) create mode 100644 plugins/catalog-backend-module-gitlab/src/index.ts rename plugins/{catalog-backend/src/modules/gitlab => catalog-backend-module-gitlab/src}/lib/client.test.ts (100%) rename plugins/{catalog-backend/src/modules/gitlab => catalog-backend-module-gitlab/src}/lib/client.ts (100%) rename plugins/{catalog-backend/src/modules/gitlab => catalog-backend-module-gitlab/src}/lib/index.ts (100%) rename plugins/{catalog-backend/src/modules/gitlab => catalog-backend-module-gitlab/src}/lib/types.ts (100%) rename plugins/{catalog-backend/src/modules/azure/index.ts => catalog-backend-module-gitlab/src/setupTests.ts} (82%) diff --git a/.changeset/clever-garlics-rescue.md b/.changeset/clever-garlics-rescue.md new file mode 100644 index 0000000000..c0ec6550b3 --- /dev/null +++ b/.changeset/clever-garlics-rescue.md @@ -0,0 +1,20 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +**BREAKING**: Removed `GitLabDiscoveryProcessor`, which now instead should be imported from `@backstage/plugin-catalog-backend-module-gitlab`. NOTE THAT this processor was part of the default set of processors in the catalog backend, and if you are a user of discovery on GitLab, you MUST now add it manually in the catalog initialization code of your backend. + +```diff +// In packages/backend/src/plugins/catalog.ts ++import { GitLabDiscoveryProcessor } from '@backstage/plugin-scaffolder-backend-module-gitlab'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + const builder = await CatalogBuilder.create(env); ++ builder.addProcessor( ++ GitLabDiscoveryProcessor.fromConfig(env.config, { logger: env.logger }) ++ ); +``` + +**BREAKING**: Removed `AzureDevOpsDiscoveryProcessor`, which now instead should be imported from `@backstage/plugin-catalog-backend-module-azure`. This processor was not part of the set of default processors. If you were using it, you should already have a reference to it in your backend code and only need to update the import. diff --git a/.changeset/rich-ravens-clap.md b/.changeset/rich-ravens-clap.md new file mode 100644 index 0000000000..913f87085f --- /dev/null +++ b/.changeset/rich-ravens-clap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-azure': minor +--- + +Added package, moving out azure specific functionality from the catalog-backend diff --git a/.changeset/silent-cats-kneel.md b/.changeset/silent-cats-kneel.md new file mode 100644 index 0000000000..f34fc98afc --- /dev/null +++ b/.changeset/silent-cats-kneel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-gitlab': minor +--- + +Added package, moving out gitlab specific functionality from the catalog-backend diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index ade575b74d..65b31b9a3d 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -34,3 +34,19 @@ The target is composed of three parts: a similar variation for catalog files stored in the root directory of each repository. If you want to use the repository's default branch use the `*` wildcard, e.g.: `/blob/*/catalog-info.yaml` + +Finally, you will have to add the processor in the catalog initialization code +of your backend. + +```diff +// In packages/backend/src/plugins/catalog.ts ++import { GitLabDiscoveryProcessor } from '@backstage/plugin-scaffolder-backend-module-gitlab'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + const builder = await CatalogBuilder.create(env); ++ builder.addProcessor( ++ GitLabDiscoveryProcessor.fromConfig(env.config, { logger: env.logger }) ++ ); +``` diff --git a/plugins/catalog-backend-module-azure/.eslintrc.js b/plugins/catalog-backend-module-azure/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/catalog-backend-module-azure/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/catalog-backend-module-azure/README.md b/plugins/catalog-backend-module-azure/README.md new file mode 100644 index 0000000000..f460a7e672 --- /dev/null +++ b/plugins/catalog-backend-module-azure/README.md @@ -0,0 +1,8 @@ +# Catalog Backend Module for Azure + +This is an extension module to the plugin-catalog-backend plugin, providing extensions targeted at Azure cloud offerings. + +## Getting started + +See [Backstage documentation](https://backstage.io/docs/integrations/azure/org) for details on how to install +and configure the plugin. diff --git a/plugins/catalog-backend-module-azure/api-report.md b/plugins/catalog-backend-module-azure/api-report.md new file mode 100644 index 0000000000..f2a9ad7cea --- /dev/null +++ b/plugins/catalog-backend-module-azure/api-report.md @@ -0,0 +1,35 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-azure" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; +import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; +import { Config } from '@backstage/config'; +import { LocationSpec } from '@backstage/plugin-catalog-backend'; +import { Logger as Logger_2 } from 'winston'; +import { ScmIntegrationRegistry } from '@backstage/integration'; + +// @public +export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { + constructor(options: { + integrations: ScmIntegrationRegistry; + logger: Logger_2; + }); + // (undocumented) + static fromConfig( + config: Config, + options: { + logger: Logger_2; + }, + ): AzureDevOpsDiscoveryProcessor; + // (undocumented) + getProcessorName(): string; + // (undocumented) + readLocation( + location: LocationSpec, + _optional: boolean, + emit: CatalogProcessorEmit, + ): Promise; +} +``` diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json new file mode 100644 index 0000000000..a6d9bfbcc9 --- /dev/null +++ b/plugins/catalog-backend-module-azure/package.json @@ -0,0 +1,56 @@ +{ + "name": "@backstage/plugin-catalog-backend-module-azure", + "description": "A Backstage catalog backend module that helps integrate towards Azure", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog-backend-module-azure" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" + }, + "dependencies": { + "@backstage/backend-common": "^0.12.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/integration": "^0.8.0", + "@backstage/plugin-catalog-backend": "^0.23.0", + "@backstage/types": "^0.1.3", + "lodash": "^4.17.21", + "msw": "^0.35.0", + "node-fetch": "^2.6.7", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/backend-test-utils": "^0.1.20", + "@backstage/cli": "^0.15.0", + "@types/lodash": "^4.14.151" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/catalog-backend/src/modules/azure/AzureDevOpsDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-azure/src/AzureDevOpsDiscoveryProcessor.test.ts similarity index 99% rename from plugins/catalog-backend/src/modules/azure/AzureDevOpsDiscoveryProcessor.test.ts rename to plugins/catalog-backend-module-azure/src/AzureDevOpsDiscoveryProcessor.test.ts index 450a0482ed..e6eed8d976 100644 --- a/plugins/catalog-backend/src/modules/azure/AzureDevOpsDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-azure/src/AzureDevOpsDiscoveryProcessor.test.ts @@ -16,12 +16,12 @@ import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { codeSearch } from './lib'; +import { LocationSpec } from '@backstage/plugin-catalog-backend'; import { AzureDevOpsDiscoveryProcessor, parseUrl, } from './AzureDevOpsDiscoveryProcessor'; -import { LocationSpec } from '../../api'; +import { codeSearch } from './lib'; jest.mock('./lib'); const mockCodeSearch = codeSearch as jest.MockedFunction; diff --git a/plugins/catalog-backend/src/modules/azure/AzureDevOpsDiscoveryProcessor.ts b/plugins/catalog-backend-module-azure/src/AzureDevOpsDiscoveryProcessor.ts similarity index 99% rename from plugins/catalog-backend/src/modules/azure/AzureDevOpsDiscoveryProcessor.ts rename to plugins/catalog-backend-module-azure/src/AzureDevOpsDiscoveryProcessor.ts index 8e36e810b8..bdc7255f44 100644 --- a/plugins/catalog-backend/src/modules/azure/AzureDevOpsDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-azure/src/AzureDevOpsDiscoveryProcessor.ts @@ -19,13 +19,13 @@ import { ScmIntegrationRegistry, ScmIntegrations, } from '@backstage/integration'; -import { Logger } from 'winston'; import { CatalogProcessor, CatalogProcessorEmit, LocationSpec, processingResult, -} from '../../api'; +} from '@backstage/plugin-catalog-backend'; +import { Logger } from 'winston'; import { codeSearch } from './lib'; /** diff --git a/plugins/catalog-backend-module-azure/src/index.ts b/plugins/catalog-backend-module-azure/src/index.ts new file mode 100644 index 0000000000..f6c61e52a5 --- /dev/null +++ b/plugins/catalog-backend-module-azure/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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. + */ + +/** + * A Backstage catalog backend module that helps integrate towards Azure + * + * @packageDocumentation + */ + +export { AzureDevOpsDiscoveryProcessor } from './AzureDevOpsDiscoveryProcessor'; diff --git a/plugins/catalog-backend/src/modules/azure/lib/azure.test.ts b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts similarity index 100% rename from plugins/catalog-backend/src/modules/azure/lib/azure.test.ts rename to plugins/catalog-backend-module-azure/src/lib/azure.test.ts index 7be1a12b54..16f5067197 100644 --- a/plugins/catalog-backend/src/modules/azure/lib/azure.test.ts +++ b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; +import { setupServer } from 'msw/node'; import { codeSearch, CodeSearchResponse } from './azure'; describe('azure', () => { diff --git a/plugins/catalog-backend/src/modules/azure/lib/azure.ts b/plugins/catalog-backend-module-azure/src/lib/azure.ts similarity index 100% rename from plugins/catalog-backend/src/modules/azure/lib/azure.ts rename to plugins/catalog-backend-module-azure/src/lib/azure.ts diff --git a/plugins/catalog-backend/src/modules/azure/lib/index.ts b/plugins/catalog-backend-module-azure/src/lib/index.ts similarity index 100% rename from plugins/catalog-backend/src/modules/azure/lib/index.ts rename to plugins/catalog-backend-module-azure/src/lib/index.ts diff --git a/plugins/catalog-backend/src/modules/gitlab/index.ts b/plugins/catalog-backend-module-azure/src/setupTests.ts similarity index 83% rename from plugins/catalog-backend/src/modules/gitlab/index.ts rename to plugins/catalog-backend-module-azure/src/setupTests.ts index f42a901f8f..d3232290a7 100644 --- a/plugins/catalog-backend/src/modules/gitlab/index.ts +++ b/plugins/catalog-backend-module-azure/src/setupTests.ts @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Backstage Authors + * Copyright 2020 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,4 +14,4 @@ * limitations under the License. */ -export { GitLabDiscoveryProcessor } from './GitLabDiscoveryProcessor'; +export {}; diff --git a/plugins/catalog-backend-module-gitlab/.eslintrc.js b/plugins/catalog-backend-module-gitlab/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/catalog-backend-module-gitlab/README.md b/plugins/catalog-backend-module-gitlab/README.md new file mode 100644 index 0000000000..3e1881eb76 --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/README.md @@ -0,0 +1,8 @@ +# Catalog Backend Module for GitLab + +This is an extension module to the plugin-catalog-backend plugin, providing extensions targeted at GitLab offerings. + +## Getting started + +See [Backstage documentation](https://backstage.io/docs/integrations/gitlab/discovery) for details on how to install +and configure the plugin. diff --git a/plugins/catalog-backend-module-gitlab/api-report.md b/plugins/catalog-backend-module-gitlab/api-report.md new file mode 100644 index 0000000000..f3cb961136 --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/api-report.md @@ -0,0 +1,30 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-gitlab" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; +import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; +import { Config } from '@backstage/config'; +import { LocationSpec } from '@backstage/plugin-catalog-backend'; +import { Logger as Logger_2 } from 'winston'; + +// @public +export class GitLabDiscoveryProcessor implements CatalogProcessor { + // (undocumented) + static fromConfig( + config: Config, + options: { + logger: Logger_2; + }, + ): GitLabDiscoveryProcessor; + // (undocumented) + getProcessorName(): string; + // (undocumented) + readLocation( + location: LocationSpec, + _optional: boolean, + emit: CatalogProcessorEmit, + ): Promise; +} +``` diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json new file mode 100644 index 0000000000..fd73d2c879 --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -0,0 +1,56 @@ +{ + "name": "@backstage/plugin-catalog-backend-module-gitlab", + "description": "A Backstage catalog backend module that helps integrate towards GitLab", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog-backend-module-gitlab" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean", + "start": "backstage-cli package start" + }, + "dependencies": { + "@backstage/backend-common": "^0.12.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/config": "^0.1.15", + "@backstage/errors": "^0.2.2", + "@backstage/integration": "^0.8.0", + "@backstage/plugin-catalog-backend": "^0.23.0", + "@backstage/types": "^0.1.3", + "lodash": "^4.17.21", + "msw": "^0.35.0", + "node-fetch": "^2.6.7", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/backend-test-utils": "^0.1.20", + "@backstage/cli": "^0.15.0", + "@types/lodash": "^4.14.151" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/catalog-backend/src/modules/gitlab/GitLabDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts similarity index 99% rename from plugins/catalog-backend/src/modules/gitlab/GitLabDiscoveryProcessor.test.ts rename to plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts index cc0f368aee..dc4ffb7251 100644 --- a/plugins/catalog-backend/src/modules/gitlab/GitLabDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; -import { GitLabDiscoveryProcessor, parseUrl } from './GitLabDiscoveryProcessor'; -import { setupServer } from 'msw/node'; +import { ConfigReader } from '@backstage/config'; +import { LocationSpec } from '@backstage/plugin-catalog-backend'; import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { GitLabDiscoveryProcessor, parseUrl } from './GitLabDiscoveryProcessor'; import { GitLabProject } from './lib'; -import { LocationSpec } from '../../api'; const server = setupServer(); diff --git a/plugins/catalog-backend/src/modules/gitlab/GitLabDiscoveryProcessor.ts b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts similarity index 99% rename from plugins/catalog-backend/src/modules/gitlab/GitLabDiscoveryProcessor.ts rename to plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts index d3916f45d1..01de1f12a0 100644 --- a/plugins/catalog-backend/src/modules/gitlab/GitLabDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.ts @@ -14,24 +14,24 @@ * limitations under the License. */ -import { Config } from '@backstage/config'; -import { - ScmIntegrationRegistry, - ScmIntegrations, -} from '@backstage/integration'; -import { Logger } from 'winston'; -import { - CatalogProcessor, - CatalogProcessorEmit, - LocationSpec, - processingResult, -} from '../../api'; -import { GitLabClient, GitLabProject, paginated } from './lib'; import { CacheClient, CacheManager, PluginCacheManager, } from '@backstage/backend-common'; +import { Config } from '@backstage/config'; +import { + ScmIntegrationRegistry, + ScmIntegrations, +} from '@backstage/integration'; +import { + CatalogProcessor, + CatalogProcessorEmit, + LocationSpec, + processingResult, +} from '@backstage/plugin-catalog-backend'; +import { Logger } from 'winston'; +import { GitLabClient, GitLabProject, paginated } from './lib'; /** * Extracts repositories out of an GitLab instance. diff --git a/plugins/catalog-backend-module-gitlab/src/index.ts b/plugins/catalog-backend-module-gitlab/src/index.ts new file mode 100644 index 0000000000..e2a08509fb --- /dev/null +++ b/plugins/catalog-backend-module-gitlab/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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. + */ + +/** + * A Backstage catalog backend module that helps integrate towards GitLab + * + * @packageDocumentation + */ + +export { GitLabDiscoveryProcessor } from './GitLabDiscoveryProcessor'; diff --git a/plugins/catalog-backend/src/modules/gitlab/lib/client.test.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts similarity index 100% rename from plugins/catalog-backend/src/modules/gitlab/lib/client.test.ts rename to plugins/catalog-backend-module-gitlab/src/lib/client.test.ts diff --git a/plugins/catalog-backend/src/modules/gitlab/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts similarity index 100% rename from plugins/catalog-backend/src/modules/gitlab/lib/client.ts rename to plugins/catalog-backend-module-gitlab/src/lib/client.ts diff --git a/plugins/catalog-backend/src/modules/gitlab/lib/index.ts b/plugins/catalog-backend-module-gitlab/src/lib/index.ts similarity index 100% rename from plugins/catalog-backend/src/modules/gitlab/lib/index.ts rename to plugins/catalog-backend-module-gitlab/src/lib/index.ts diff --git a/plugins/catalog-backend/src/modules/gitlab/lib/types.ts b/plugins/catalog-backend-module-gitlab/src/lib/types.ts similarity index 100% rename from plugins/catalog-backend/src/modules/gitlab/lib/types.ts rename to plugins/catalog-backend-module-gitlab/src/lib/types.ts diff --git a/plugins/catalog-backend/src/modules/azure/index.ts b/plugins/catalog-backend-module-gitlab/src/setupTests.ts similarity index 82% rename from plugins/catalog-backend/src/modules/azure/index.ts rename to plugins/catalog-backend-module-gitlab/src/setupTests.ts index 9c76336804..d3232290a7 100644 --- a/plugins/catalog-backend/src/modules/azure/index.ts +++ b/plugins/catalog-backend-module-gitlab/src/setupTests.ts @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Backstage Authors + * Copyright 2020 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,4 +14,4 @@ * limitations under the License. */ -export { AzureDevOpsDiscoveryProcessor } from './AzureDevOpsDiscoveryProcessor'; +export {}; diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 922e1d2741..5d8c5904a1 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -97,29 +97,6 @@ export class AnnotateScmSlugEntityProcessor implements CatalogProcessor { preProcessEntity(entity: Entity, location: LocationSpec): Promise; } -// @public -export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { - constructor(options: { - integrations: ScmIntegrationRegistry; - logger: Logger_2; - }); - // (undocumented) - static fromConfig( - config: Config, - options: { - logger: Logger_2; - }, - ): AzureDevOpsDiscoveryProcessor; - // (undocumented) - getProcessorName(): string; - // (undocumented) - readLocation( - location: LocationSpec, - _optional: boolean, - emit: CatalogProcessorEmit, - ): Promise; -} - // @public (undocumented) export class BitbucketDiscoveryProcessor implements CatalogProcessor { constructor(options: { @@ -769,25 +746,6 @@ export class GithubOrgReaderProcessor implements CatalogProcessor { ): Promise; } -// @public -export class GitLabDiscoveryProcessor implements CatalogProcessor { - // (undocumented) - static fromConfig( - config: Config, - options: { - logger: Logger_2; - }, - ): GitLabDiscoveryProcessor; - // (undocumented) - getProcessorName(): string; - // (undocumented) - readLocation( - location: LocationSpec, - _optional: boolean, - emit: CatalogProcessorEmit, - ): Promise; -} - // @public @deprecated (undocumented) function inputError( atLocation: LocationSpec, diff --git a/plugins/catalog-backend/src/modules/index.ts b/plugins/catalog-backend/src/modules/index.ts index 1cb7e3007b..c541250c46 100644 --- a/plugins/catalog-backend/src/modules/index.ts +++ b/plugins/catalog-backend/src/modules/index.ts @@ -14,9 +14,7 @@ * limitations under the License. */ -export * from './azure'; export * from './bitbucket'; export * from './codeowners'; export * from './core'; export * from './github'; -export * from './gitlab'; diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index b53335c469..768118404e 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -49,10 +49,8 @@ import { BuiltinKindsEntityProcessor, CodeOwnersProcessor, FileReaderProcessor, - AzureDevOpsDiscoveryProcessor, GithubDiscoveryProcessor, GithubOrgReaderProcessor, - GitLabDiscoveryProcessor, PlaceholderProcessor, PlaceholderResolver, UrlReaderProcessor, @@ -360,7 +358,6 @@ export class CatalogBuilder { return [ new FileReaderProcessor(), BitbucketDiscoveryProcessor.fromConfig(config, { logger }), - AzureDevOpsDiscoveryProcessor.fromConfig(config, { logger }), GithubDiscoveryProcessor.fromConfig(config, { logger, githubCredentialsProvider, @@ -369,7 +366,6 @@ export class CatalogBuilder { logger, githubCredentialsProvider, }), - GitLabDiscoveryProcessor.fromConfig(config, { logger }), new UrlReaderProcessor({ reader, logger }), CodeOwnersProcessor.fromConfig(config, { logger, reader }), new AnnotateLocationEntityProcessor({ integrations }), diff --git a/scripts/api-extractor.ts b/scripts/api-extractor.ts index 153861b875..fd0c4d1947 100644 --- a/scripts/api-extractor.ts +++ b/scripts/api-extractor.ts @@ -223,6 +223,8 @@ const NO_WARNING_PACKAGES = [ 'plugins/auth-node', 'plugins/catalog-backend', 'plugins/catalog-backend-module-aws', + 'plugins/catalog-backend-module-azure', + 'plugins/catalog-backend-module-gitlab', 'plugins/catalog-backend-module-ldap', 'plugins/catalog-backend-module-msgraph', 'plugins/catalog-common', From f9102dc11c8775664e4e422f2afd0508370e0c62 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Mar 2022 04:08:30 +0000 Subject: [PATCH 246/353] build(deps-dev): bump @graphql-codegen/graphql-modules-preset Bumps [@graphql-codegen/graphql-modules-preset](https://github.com/dotansimha/graphql-code-generator/tree/HEAD/packages/presets/graphql-modules) from 2.3.2 to 2.3.5. - [Release notes](https://github.com/dotansimha/graphql-code-generator/releases) - [Changelog](https://github.com/dotansimha/graphql-code-generator/blob/master/packages/presets/graphql-modules/CHANGELOG.md) - [Commits](https://github.com/dotansimha/graphql-code-generator/commits/@graphql-codegen/graphql-modules-preset@2.3.5/packages/presets/graphql-modules) --- updated-dependencies: - dependency-name: "@graphql-codegen/graphql-modules-preset" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/yarn.lock b/yarn.lock index 70bc0ac1a4..8be3f148a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2128,13 +2128,13 @@ tslib "~2.3.0" "@graphql-codegen/graphql-modules-preset@^2.3.2": - version "2.3.2" - resolved "https://registry.npmjs.org/@graphql-codegen/graphql-modules-preset/-/graphql-modules-preset-2.3.2.tgz#df88431e4cff656799b1ab0324e0795606162389" - integrity sha512-O3PPDQejqf3rF9sHqlrl00M+BSIKJAovFWt2zkDr/D3I/XwntR4QdQDKyY9zTotHfPpgJKyxyMzthpdqPD5XUA== + version "2.3.5" + resolved "https://registry.npmjs.org/@graphql-codegen/graphql-modules-preset/-/graphql-modules-preset-2.3.5.tgz#07ef9ef6e66d4fbb76767097a7cc3cace34f8d08" + integrity sha512-y1PcTFr483Imzd346YdmNSvW3FlN4iYj4sRWCRys+WXpiFcXq+D7+1dFZvs75/9bIHoOEeQtusHHuRPy9qJVkA== dependencies: - "@graphql-codegen/plugin-helpers" "^2.3.2" - "@graphql-codegen/visitor-plugin-common" "2.5.2" - "@graphql-tools/utils" "8.5.5" + "@graphql-codegen/plugin-helpers" "^2.4.0" + "@graphql-codegen/visitor-plugin-common" "2.7.1" + "@graphql-tools/utils" "8.6.1" change-case-all "1.0.14" parse-filepath "^1.0.2" tslib "~2.3.0" @@ -2540,10 +2540,10 @@ dependencies: tslib "~2.3.0" -"@graphql-tools/utils@8.5.5": - version "8.5.5" - resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.5.5.tgz#019ddb99719feb19602afdb537c06e463df674a9" - integrity sha512-y7zRXWIUI73X+9/rf/0KzrNFMlpRKFfzLiwdbIeWwgLs+NV9vfUOoVkX8luXX6LwQxhSypHATMiwZGM2ro/wJA== +"@graphql-tools/utils@8.6.1", "@graphql-tools/utils@^8.1.1", "@graphql-tools/utils@^8.3.0", "@graphql-tools/utils@^8.5.1", "@graphql-tools/utils@^8.5.2", "@graphql-tools/utils@^8.5.3", "@graphql-tools/utils@^8.6.0": + version "8.6.1" + resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.6.1.tgz#52c7eb108f2ca2fd01bdba8eef85077ead1bf882" + integrity sha512-uxcfHCocp4ENoIiovPxUWZEHOnbXqj3ekWc0rm7fUhW93a1xheARNHcNKhwMTR+UKXVJbTFQdGI1Rl5XdyvDBg== dependencies: tslib "~2.3.0" @@ -2556,13 +2556,6 @@ camel-case "4.1.2" tslib "~2.2.0" -"@graphql-tools/utils@^8.1.1", "@graphql-tools/utils@^8.3.0", "@graphql-tools/utils@^8.5.1", "@graphql-tools/utils@^8.5.2", "@graphql-tools/utils@^8.5.3", "@graphql-tools/utils@^8.6.0": - version "8.6.0" - resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.6.0.tgz#f424256a1f3b87d1dcf6f9f675739b2d3627be33" - integrity sha512-rnk+RHaOCeWnfekeQGRh5ycXK1ZAI7Nm0pbeLjA3SiysTdqhWyxNCp5ON4Mvtlid84OY/KB253fQq/2rotznCA== - dependencies: - tslib "~2.3.0" - "@graphql-tools/wrap@^7.0.4": version "7.0.8" resolved "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-7.0.8.tgz#ad41e487135ca3ea1ae0ea04bb3f596177fb4f50" From b120f218cd977d2f638c299e9465c93eb2957b5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Mar 2022 08:27:52 +0000 Subject: [PATCH 247/353] build(deps): bump core-js from 3.20.3 to 3.21.1 Bumps [core-js](https://github.com/zloirock/core-js) from 3.20.3 to 3.21.1. - [Release notes](https://github.com/zloirock/core-js/releases) - [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/zloirock/core-js/compare/v3.20.3...v3.21.1) --- updated-dependencies: - dependency-name: core-js dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- yarn.lock | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 70bc0ac1a4..c3a8e0f7b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9666,16 +9666,11 @@ core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.10: resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== -core-js@^3.4.1: +core-js@^3.4.1, core-js@^3.6.5: version "3.21.1" resolved "https://registry.npmjs.org/core-js/-/core-js-3.21.1.tgz#f2e0ddc1fc43da6f904706e8e955bc19d06a0d94" integrity sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig== -core-js@^3.6.5: - version "3.20.3" - resolved "https://registry.npmjs.org/core-js/-/core-js-3.20.3.tgz#c710d0a676e684522f3db4ee84e5e18a9d11d69a" - integrity sha512-vVl8j8ph6tRS3B8qir40H7yw7voy17xL0piAjlbBUsH7WIfzoedL/ZOr1OV9FyZQLWXsayOJyV4tnRyXR85/ag== - core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" From 800a2b6e80bb704f8f30452850da5b0676eebd22 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 17:20:59 +0100 Subject: [PATCH 248/353] chore: removal of the deprecations Signed-off-by: blam --- plugins/scaffolder-common/src/TaskSpec.ts | 37 +--- .../src/Template.v1beta2.schema.json | 182 ------------------ .../src/TemplateEntityV1beta2.test.ts | 161 ---------------- .../src/TemplateEntityV1beta2.ts | 65 ------- plugins/scaffolder-common/src/index.ts | 2 - 5 files changed, 1 insertion(+), 446 deletions(-) delete mode 100644 plugins/scaffolder-common/src/Template.v1beta2.schema.json delete mode 100644 plugins/scaffolder-common/src/TemplateEntityV1beta2.test.ts delete mode 100644 plugins/scaffolder-common/src/TemplateEntityV1beta2.ts diff --git a/plugins/scaffolder-common/src/TaskSpec.ts b/plugins/scaffolder-common/src/TaskSpec.ts index 2c7eb26d5b..326bb60ef6 100644 --- a/plugins/scaffolder-common/src/TaskSpec.ts +++ b/plugins/scaffolder-common/src/TaskSpec.ts @@ -16,17 +16,6 @@ import { JsonValue, JsonObject } from '@backstage/types'; -/** - * Metadata about the Template that was the originator of a scaffolder task, as - * stored in the database. - * - * @public - * @deprecated use templateInfo on the spec instead - */ -export type TemplateMetadata = { - name: string; -}; - /** * Information about a template that is stored on a task specification. * Includes a stringified entityRef, and the baseUrl which is usually the relative path of the template definition @@ -51,26 +40,6 @@ export interface TaskStep { if?: string | boolean; } -/** - * A scaffolder task as stored in the database, generated from a v1beta2 - * apiVersion Template. - * - * @public - * @deprecated Please convert your templates to TaskSpecV1beta3 on apiVersion - * scaffolder.backstage.io/v1beta3 - */ -export interface TaskSpecV1beta2 { - apiVersion: 'backstage.io/v1beta2'; - /** @deprecated use templateInfo.baseUrl instead */ - baseUrl?: string; - values: JsonObject; - steps: TaskStep[]; - output: { [name: string]: string }; - /** @deprecated use templateInfo instead */ - metadata?: TemplateMetadata; - templateInfo?: TemplateInfo; -} - /** * A scaffolder task as stored in the database, generated from a v1beta3 * apiVersion Template. @@ -79,13 +48,9 @@ export interface TaskSpecV1beta2 { */ export interface TaskSpecV1beta3 { apiVersion: 'scaffolder.backstage.io/v1beta3'; - /** @deprecated use templateInfo.baseUrl instead */ - baseUrl?: string; parameters: JsonObject; steps: TaskStep[]; output: { [name: string]: JsonValue }; - /** @deprecated use templateInfo instead */ - metadata?: TemplateMetadata; templateInfo?: TemplateInfo; } @@ -94,4 +59,4 @@ export interface TaskSpecV1beta3 { * * @public */ -export type TaskSpec = TaskSpecV1beta2 | TaskSpecV1beta3; +export type TaskSpec = TaskSpecV1beta3; diff --git a/plugins/scaffolder-common/src/Template.v1beta2.schema.json b/plugins/scaffolder-common/src/Template.v1beta2.schema.json deleted file mode 100644 index d68eac936d..0000000000 --- a/plugins/scaffolder-common/src/Template.v1beta2.schema.json +++ /dev/null @@ -1,182 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "TemplateV1beta2", - "description": "A Template describes a scaffolding task for use with the Scaffolder. It describes the required parameters as well as a series of steps that will be taken to execute the scaffolding task.", - "examples": [ - { - "apiVersion": "backstage.io/v1beta2", - "kind": "Template", - "metadata": { - "name": "react-ssr-template", - "title": "React SSR Template", - "description": "Next.js application skeleton for creating isomorphic web applications.", - "tags": ["recommended", "react"] - }, - "spec": { - "owner": "artist-relations-team", - "type": "website", - "parameters": { - "required": ["name", "description"], - "properties": { - "name": { - "title": "Name", - "type": "string", - "description": "Unique name of the component" - }, - "description": { - "title": "Description", - "type": "string", - "description": "Description of the component" - } - } - }, - "steps": [ - { - "id": "fetch", - "name": "Fetch", - "action": "fetch:plain", - "parameters": { - "url": "./template" - } - }, - { - "id": "publish", - "name": "Publish to GitHub", - "action": "publish:github", - "parameters": { - "repoUrl": "{{ parameters.repoUrl }}" - }, - "if": "{{ parameters.repoUrl }}" - } - ], - "output": { - "catalogInfoUrl": "{{ steps.publish.output.catalogInfoUrl }}" - } - } - } - ], - "allOf": [ - { - "$ref": "Entity" - }, - { - "type": "object", - "required": ["spec"], - "properties": { - "apiVersion": { - "enum": ["backstage.io/v1beta2"] - }, - "kind": { - "enum": ["Template"] - }, - "spec": { - "type": "object", - "required": ["type", "steps"], - "properties": { - "type": { - "type": "string", - "description": "The type of component created by the template. The software catalog accepts any type value, but an organization should take great care to establish a proper taxonomy for these. Tools including Backstage itself may read this field and behave differently depending on its value. For example, a website type component may present tooling in the Backstage interface that is specific to just websites.", - "examples": ["service", "website", "library"], - "minLength": 1 - }, - "parameters": { - "oneOf": [ - { - "type": "object", - "description": "The JSONSchema describing the inputs for the template." - }, - { - "type": "array", - "description": "A list of separate forms to collect parameters.", - "items": { - "type": "object", - "description": "The JSONSchema describing the inputs for the template." - } - } - ] - }, - "steps": { - "type": "array", - "description": "A list of steps to execute.", - "items": { - "type": "object", - "description": "A description of the step to execute.", - "required": ["action"], - "properties": { - "id": { - "type": "string", - "description": "The ID of the step, which can be used to refer to its outputs." - }, - "name": { - "type": "string", - "description": "The name of the step, which will be displayed in the UI during the scaffolding process." - }, - "action": { - "type": "string", - "description": "The name of the action to execute." - }, - "input": { - "type": "object", - "description": "A templated object describing the inputs to the action." - }, - "if": { - "type": ["string", "boolean"], - "description": "A templated condition that skips the step when evaluated to false. If the condition is true or not defined, the step is executed. The condition is true, if the input is not `false`, `undefined`, `null`, `\"\"`, `0`, or `[]`." - } - } - } - }, - "output": { - "type": "object", - "description": "A templated object describing the outputs of the scaffolding task.", - "properties": { - "links": { - "type": "array", - "description": "A list of external hyperlinks, typically pointing to resources created or updated by the template", - "items": { - "type": "object", - "required": [], - "properties": { - "url": { - "type": "string", - "description": "A url in a standard uri format.", - "examples": ["https://github.com/my-org/my-new-repo"], - "minLength": 1 - }, - "entityRef": { - "type": "string", - "description": "An entity reference to an entity in the catalog.", - "examples": ["Component:default/my-app"], - "minLength": 1 - }, - "title": { - "type": "string", - "description": "A user friendly display name for the link.", - "examples": ["View new repo"], - "minLength": 1 - }, - "icon": { - "type": "string", - "description": "A key representing a visual icon to be displayed in the UI.", - "examples": ["dashboard"], - "minLength": 1 - } - } - } - } - }, - "additionalProperties": { - "type": "string" - } - }, - "owner": { - "type": "string", - "description": "The user (or group) owner of the template", - "minLength": 1 - } - } - } - } - } - ] -} diff --git a/plugins/scaffolder-common/src/TemplateEntityV1beta2.test.ts b/plugins/scaffolder-common/src/TemplateEntityV1beta2.test.ts deleted file mode 100644 index f715f3c2b6..0000000000 --- a/plugins/scaffolder-common/src/TemplateEntityV1beta2.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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 { - TemplateEntityV1beta2, - templateEntityV1beta2Validator as validator, -} from './TemplateEntityV1beta2'; - -describe('templateEntityV1beta2Validator', () => { - let entity: TemplateEntityV1beta2; - - beforeEach(() => { - entity = { - apiVersion: 'backstage.io/v1beta2', - kind: 'Template', - metadata: { - name: 'test', - }, - spec: { - type: 'website', - parameters: { - required: ['storePath', 'owner'], - properties: { - owner: { - type: 'string', - title: 'Owner', - description: 'Who is going to own this component', - }, - storePath: { - type: 'string', - title: 'Store path', - description: 'GitHub store path in org/repo format', - }, - }, - }, - steps: [ - { - id: 'fetch', - name: 'Fetch', - action: 'fetch:plan', - input: { - url: './template', - }, - if: '{{ parameters.owner }}', - }, - ], - output: { - fetchUrl: '{{ steps.fetch.output.targetUrl }}', - }, - owner: 'team-b@example.com', - }, - }; - }); - - it('happy path: accepts valid data', async () => { - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('ignores unknown apiVersion', async () => { - (entity as any).apiVersion = 'backstage.io/v1beta0'; - await expect(validator.check(entity)).resolves.toBe(false); - }); - - it('ignores unknown kind', async () => { - (entity as any).kind = 'Wizard'; - await expect(validator.check(entity)).resolves.toBe(false); - }); - - it('rejects missing type', async () => { - delete (entity as any).spec.type; - await expect(validator.check(entity)).rejects.toThrow(/type/); - }); - - it('accepts any other type', async () => { - (entity as any).spec.type = 'hallo'; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('accepts missing parameters', async () => { - delete (entity as any).spec.parameters; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('accepts missing outputs', async () => { - delete (entity as any).spec.outputs; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('rejects empty type', async () => { - (entity as any).spec.type = ''; - await expect(validator.check(entity)).rejects.toThrow(/type/); - }); - - it('rejects missing steps', async () => { - delete (entity as any).spec.steps; - await expect(validator.check(entity)).rejects.toThrow(/steps/); - }); - - it('accepts step with missing id', async () => { - delete (entity as any).spec.steps[0].id; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('accepts step with missing name', async () => { - delete (entity as any).spec.steps[0].name; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('rejects step with missing action', async () => { - delete (entity as any).spec.steps[0].action; - await expect(validator.check(entity)).rejects.toThrow(/action/); - }); - - it('accepts missing owner', async () => { - delete (entity as any).spec.owner; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('rejects empty owner', async () => { - (entity as any).spec.owner = ''; - await expect(validator.check(entity)).rejects.toThrow(/owner/); - }); - - it('rejects wrong type owner', async () => { - (entity as any).spec.owner = 5; - await expect(validator.check(entity)).rejects.toThrow(/owner/); - }); - - it('accepts missing if', async () => { - delete (entity as any).spec.steps[0].if; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('accepts boolean in if', async () => { - (entity as any).spec.steps[0].if = true; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('accepts empty if', async () => { - (entity as any).spec.steps[0].if = ''; - await expect(validator.check(entity)).resolves.toBe(true); - }); - - it('rejects wrong type if', async () => { - (entity as any).spec.steps[0].if = 5; - await expect(validator.check(entity)).rejects.toThrow(/if/); - }); -}); diff --git a/plugins/scaffolder-common/src/TemplateEntityV1beta2.ts b/plugins/scaffolder-common/src/TemplateEntityV1beta2.ts deleted file mode 100644 index 280caec52f..0000000000 --- a/plugins/scaffolder-common/src/TemplateEntityV1beta2.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * 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, - entityKindSchemaValidator, - KindValidator, -} from '@backstage/catalog-model'; -import { JsonObject } from '@backstage/types'; -import schema from './Template.v1beta2.schema.json'; - -/** - * Backstage catalog Template kind Entity. Templates are used by the Scaffolder - * plugin to create new entities, such as Components. - * - * @public - * @deprecated Please convert your templates to TemplateEntityV1beta3 on - * apiVersion scaffolder.backstage.io/v1beta3 - */ -export interface TemplateEntityV1beta2 extends Entity { - apiVersion: 'backstage.io/v1beta2'; - kind: 'Template'; - spec: { - type: string; - parameters?: JsonObject | JsonObject[]; - steps: Array<{ - id?: string; - name?: string; - action: string; - input?: JsonObject; - if?: string | boolean; - }>; - output?: { [name: string]: string }; - owner?: string; - }; -} - -const validator = entityKindSchemaValidator(schema); - -/** - * Entity data validator for {@link TemplateEntityV1beta2}. - * - * @public - * @deprecated Please convert your templates to TemplateEntityV1beta3 on - * apiVersion scaffolder.backstage.io/v1beta3 - */ -export const templateEntityV1beta2Validator: KindValidator = { - // TODO(freben): Emulate the old KindValidator until we fix that type - async check(data: Entity) { - return validator(data) === data; - }, -}; diff --git a/plugins/scaffolder-common/src/index.ts b/plugins/scaffolder-common/src/index.ts index e74a5f988f..521e678ab6 100644 --- a/plugins/scaffolder-common/src/index.ts +++ b/plugins/scaffolder-common/src/index.ts @@ -21,7 +21,5 @@ */ export * from './TaskSpec'; -export { templateEntityV1beta2Validator } from './TemplateEntityV1beta2'; -export type { TemplateEntityV1beta2 } from './TemplateEntityV1beta2'; export { templateEntityV1beta3Validator } from './TemplateEntityV1beta3'; export type { TemplateEntityV1beta3 } from './TemplateEntityV1beta3'; From 23c735d94b5851a10fb3aa9ef06540e01759b6fc Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 17:45:50 +0100 Subject: [PATCH 249/353] chore: fix off the other removals Signed-off-by: blam --- .../core/BuiltinKindsEntityProcessor.test.ts | 34 -- .../core/BuiltinKindsEntityProcessor.ts | 18 - plugins/catalog-react/src/filters.test.ts | 8 +- .../src/scaffolder/actions/types.ts | 9 +- .../tasks/HandlebarsWorkflowRunner.test.ts | 441 ------------------ .../tasks/HandlebarsWorkflowRunner.ts | 308 ------------ .../tasks/NunjucksWorkflowRunner.ts | 4 - .../src/scaffolder/tasks/TaskWorker.test.ts | 46 -- .../src/scaffolder/tasks/TaskWorker.ts | 24 +- .../scaffolder-backend/src/service/helpers.ts | 9 +- .../src/service/router.test.ts | 6 +- .../scaffolder-backend/src/service/router.ts | 100 ++-- plugins/scaffolder/src/components/Router.tsx | 6 +- .../ScaffolderPage/ScaffolderPage.tsx | 4 +- .../src/components/TaskPage/TaskPage.tsx | 5 +- .../components/TemplateCard/TemplateCard.tsx | 6 +- .../components/TemplateList/TemplateList.tsx | 6 +- 17 files changed, 65 insertions(+), 969 deletions(-) delete mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts delete mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts diff --git a/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts index 2532293acf..7ae4b6b5a0 100644 --- a/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts @@ -23,7 +23,6 @@ import { SystemEntity, UserEntity, } from '@backstage/catalog-model'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; import { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor'; describe('BuiltinKindsEntityProcessor', () => { @@ -539,38 +538,5 @@ describe('BuiltinKindsEntityProcessor', () => { }, }); }); - it('generates relations for template entities', async () => { - const entity: TemplateEntityV1beta2 = { - apiVersion: 'backstage.io/v1beta2', - kind: 'Template', - metadata: { name: 'n' }, - spec: { - parameters: {}, - steps: [], - type: 'service', - owner: 'o', - }, - }; - - await processor.postProcessEntity(entity, location, emit); - - expect(emit).toBeCalledTimes(2); - expect(emit).toBeCalledWith({ - type: 'relation', - relation: { - source: { kind: 'Group', namespace: 'default', name: 'o' }, - type: 'ownerOf', - target: { kind: 'Template', namespace: 'default', name: 'n' }, - }, - }); - expect(emit).toBeCalledWith({ - type: 'relation', - relation: { - source: { kind: 'Template', namespace: 'default', name: 'n' }, - type: 'ownedBy', - target: { kind: 'Group', namespace: 'default', name: 'o' }, - }, - }); - }); }); }); diff --git a/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts index 9e420b9d65..4e8cf3fef7 100644 --- a/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts @@ -48,10 +48,6 @@ import { UserEntity, userEntityV1alpha1Validator, } from '@backstage/catalog-model'; -import { - TemplateEntityV1beta2, - templateEntityV1beta2Validator, -} from '@backstage/plugin-scaffolder-common'; import { CatalogProcessor, CatalogProcessorEmit, @@ -67,7 +63,6 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { resourceEntityV1alpha1Validator, groupEntityV1alpha1Validator, locationEntityV1alpha1Validator, - templateEntityV1beta2Validator, userEntityV1alpha1Validator, systemEntityV1alpha1Validator, domainEntityV1alpha1Validator, @@ -135,19 +130,6 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { } } - /* - * Emit relations for the Template kind - */ - if (entity.kind === 'Template') { - const template = entity as TemplateEntityV1beta2; - doEmit( - template.spec.owner, - { defaultKind: 'Group', defaultNamespace: selfRef.namespace }, - RELATION_OWNED_BY, - RELATION_OWNER_OF, - ); - } - /* * Emit relations for the Component kind */ diff --git a/plugins/catalog-react/src/filters.test.ts b/plugins/catalog-react/src/filters.test.ts index cdc2ce2150..ba31e21122 100644 --- a/plugins/catalog-react/src/filters.test.ts +++ b/plugins/catalog-react/src/filters.test.ts @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { EntityTextFilter } from './filters'; const entities: Entity[] = [ @@ -37,9 +37,9 @@ const entities: Entity[] = [ }, ]; -const templates: TemplateEntityV1beta2[] = [ +const templates: TemplateEntityV1beta3[] = [ { - apiVersion: 'backstage.io/v1beta2', + apiVersion: 'scaffolder.backstage.io/v1beta3', kind: 'Template', metadata: { name: 'react-app', @@ -52,7 +52,7 @@ const templates: TemplateEntityV1beta2[] = [ }, }, { - apiVersion: 'backstage.io/v1beta2', + apiVersion: 'scaffolder.backstage.io/v1beta3', kind: 'Template', metadata: { name: 'gRPC service', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts index 55ef68423e..f73c4dce3f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts @@ -19,10 +19,7 @@ import { Writable } from 'stream'; import { JsonValue, JsonObject } from '@backstage/types'; import { Schema } from 'jsonschema'; import { TaskSecrets } from '../tasks/types'; -import { - TemplateInfo, - TemplateMetadata, -} from '@backstage/plugin-scaffolder-common'; +import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; /** * ActionContext is passed into scaffolder actions. @@ -47,10 +44,6 @@ export type ActionContext = { */ createTemporaryDirectory(): Promise; - /** - * @deprecated please use templateInfo instead - */ - metadata?: TemplateMetadata; templateInfo?: TemplateInfo; }; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts deleted file mode 100644 index dcad1fc5d0..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts +++ /dev/null @@ -1,441 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * 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 mockFs from 'mock-fs'; -import * as winston from 'winston'; -import { createTemplateAction, TemplateActionRegistry } from '../actions'; -import { ScmIntegrations } from '@backstage/integration'; -import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; -import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner'; -import { TaskContext } from './types'; -import { RepoSpec } from '../actions/builtin/publish/util'; -import { TaskSpec } from '@backstage/plugin-scaffolder-common'; - -describe('LegacyWorkflowRunner', () => { - let runner: HandlebarsWorkflowRunner; - const logger = getVoidLogger(); - let actionRegistry = new TemplateActionRegistry(); - let fakeActionHandler: jest.Mock; - - const integrations = ScmIntegrations.fromConfig( - new ConfigReader({ - integrations: { - github: [{ host: 'github.com', token: 'token' }], - }, - }), - ); - - const createMockTaskWithSpec = (spec: TaskSpec): TaskContext => ({ - spec, - complete: async () => {}, - done: false, - emitLog: async () => {}, - getWorkspaceName: () => Promise.resolve('test-workspace'), - }); - - beforeEach(() => { - winston.format.simple(); // put logform the require cache before mocking fs - mockFs({ - '/tmp': mockFs.directory(), - }); - - actionRegistry = new TemplateActionRegistry(); - actionRegistry.register({ - id: 'test-action', - handler: async ctx => { - ctx.output('testOutput', 'mockOutputData'); - ctx.output('badOutput', false); - }, - }); - fakeActionHandler = jest.fn(); - actionRegistry.register({ - id: 'test-metadata-action', - handler: fakeActionHandler, - }); - - runner = new HandlebarsWorkflowRunner({ - actionRegistry, - integrations, - workingDirectory: '/tmp', - logger, - }); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it('should fail when the action does not exist', async () => { - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [{ id: 'test', name: 'test', action: 'not-found-action' }], - output: { - result: '{{ steps.test.output.testOutput }}', - }, - values: {}, - }); - - await expect(() => runner.execute(task)).rejects.toThrow( - /Template action with ID 'not-found-action' is not registered/, - ); - }); - - it('should pass metadata through', async () => { - const entityRef = `template:default/templateName`; - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { - id: 'test', - name: 'name', - action: 'test-metadata-action', - input: { foo: 1 }, - }, - ], - output: {}, - values: {}, - templateInfo: { entityRef }, - }); - - await runner.execute(task); - - expect(fakeActionHandler.mock.calls[0][0].templateInfo).toEqual({ - entityRef, - }); - }); - - describe('templating', () => { - it('should template the output', async () => { - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [{ id: 'test', name: 'test', action: 'test-action' }], - output: { - result: '{{ steps.test.output.testOutput }}', - }, - values: {}, - }); - - const { output } = await runner.execute(task); - - expect(output.result).toBe('mockOutputData'); - }); - - it('should template the input', async () => { - const inputAction = createTemplateAction<{ - name: string; - }>({ - id: 'test-input', - schema: { - input: { - type: 'object', - required: ['name'], - properties: { - name: { - title: 'name', - description: 'Enter name', - type: 'string', - }, - }, - }, - }, - async handler(ctx) { - if (ctx.input.name !== 'mockOutputData') { - throw new Error( - `expected name to be "mockOutputData" got ${ctx.input.name}`, - ); - } - }, - }); - actionRegistry.register(inputAction); - - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { id: 'test', name: 'test', action: 'test-action' }, - { - id: 'test-input', - name: 'test-input', - action: 'test-input', - input: { - name: '{{ steps.test.output.testOutput }}', - }, - }, - ], - output: { - result: '{{ steps.test.output.testOutput }}', - }, - values: {}, - }); - - const { output } = await runner.execute(task); - - expect(output.result).toBe('mockOutputData'); - }); - }); - - describe('conditionals', () => { - it('should execute steps conditionally', async () => { - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { id: 'test', name: 'test', action: 'test-action' }, - { - id: 'conditional', - name: 'conditional', - action: 'test-action', - if: '{{ steps.test.output.testOutput }}', - }, - ], - output: { - result: '{{ steps.conditional.output.testOutput }}', - }, - values: {}, - }); - - const { output } = await runner.execute(task); - - expect(output.result).toBe('mockOutputData'); - }); - - it('should execute steps conditionally with eq helper', async () => { - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { id: 'test', name: 'test', action: 'test-action' }, - { - id: 'conditional', - name: 'conditional', - action: 'test-action', - if: '{{ eq steps.test.output.testOutput "mockOutputData" }}', - }, - ], - output: { - result: '{{ steps.conditional.output.testOutput }}', - }, - values: {}, - }); - - const { output } = await runner.execute(task); - - expect(output.result).toBe('mockOutputData'); - }); - - it('should skip test conditionally', async () => { - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { id: 'test', name: 'test', action: 'test-action' }, - { - id: 'conditional', - name: 'conditional', - action: 'test-action', - if: '{{ steps.test.output.badOutput }}', - }, - ], - output: { - result: '{{ steps.conditional.output.testOutput }}', - }, - values: {}, - }); - - const { output } = await runner.execute(task); - - expect(output.result).toBeUndefined(); - }); - }); - - describe('parsing', () => { - it('should parse strings as objects if possible', async () => { - const inputAction = createTemplateAction<{ - address: { line1: string }; - list: string[]; - address2: string; - }>({ - id: 'test-input', - schema: { - input: { - type: 'object', - required: ['address'], - properties: { - address: { - title: 'address', - description: 'Enter name', - type: 'object', - properties: { - line1: { - type: 'string', - }, - }, - }, - address2: { - type: 'string', - }, - list: { - type: 'array', - items: { - type: 'string', - }, - }, - }, - }, - }, - async handler(ctx) { - if (ctx.input.list.length !== 1) { - throw new Error( - `expected list to have length "1" got ${ctx.input.list.length}`, - ); - } - if (ctx.input.address.line1 !== 'line 1') { - throw new Error( - `expected address.line1 to be "line 1" got ${ctx.input.address.line1}`, - ); - } - - if (ctx.input.address2 !== '{"not valid"}') { - throw new Error( - `expected address2 to be "{"not valid"}" got ${ctx.input.address2}`, - ); - } - ctx.output('address', ctx.input.address.line1); - }, - }); - actionRegistry.register(inputAction); - - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { - id: 'test-input', - name: 'test-input', - action: 'test-input', - input: { - address: JSON.stringify({ line1: 'line 1' }), - list: JSON.stringify(['hey!']), - address2: '{"not valid"}', - }, - }, - ], - output: { - result: '{{ steps.test-input.output.address }}', - }, - values: {}, - }); - - const { output } = await runner.execute(task); - - expect(output.result).toBe('line 1'); - }); - - it('should provide a parseRepoUrl helper', async () => { - const inputAction = createTemplateAction<{ - destination: RepoSpec; - }>({ - id: 'test-input', - schema: { - input: { - type: 'object', - required: ['destination'], - properties: { - destination: { - title: 'destination', - type: 'object', - properties: { - repo: { - type: 'string', - }, - host: { - type: 'string', - }, - owner: { - type: 'string', - }, - organization: { - type: 'string', - }, - workspace: { - type: 'string', - }, - project: { - type: 'string', - }, - }, - }, - }, - }, - }, - async handler(ctx) { - ctx.output('host', ctx.input.destination.host); - ctx.output('repo', ctx.input.destination.repo); - - if (ctx.input.destination.owner) { - ctx.output('owner', ctx.input.destination.owner); - } - - if (ctx.input.destination.host !== 'github.com') { - throw new Error( - `expected host to be "github.com" got ${ctx.input.destination.host}`, - ); - } - - if (ctx.input.destination.repo !== 'repo') { - throw new Error( - `expected repo to be "repo" got ${ctx.input.destination.repo}`, - ); - } - - if ( - ctx.input.destination.owner && - ctx.input.destination.owner !== 'owner' - ) { - throw new Error( - `expected repo to be "owner" got ${ctx.input.destination.owner}`, - ); - } - }, - }); - actionRegistry.register(inputAction); - - const task = createMockTaskWithSpec({ - apiVersion: 'backstage.io/v1beta2', - steps: [ - { - id: 'test-input', - name: 'test-input', - action: 'test-input', - input: { - destination: '{{ parseRepoUrl parameters.repoUrl }}', - }, - }, - ], - output: { - host: '{{ steps.test-input.output.host }}', - repo: '{{ steps.test-input.output.repo }}', - owner: '{{ steps.test-input.output.owner }}', - }, - values: { - repoUrl: 'github.com?repo=repo&owner=owner', - }, - }); - - const { output } = await runner.execute(task); - - expect(output.host).toBe('github.com'); - expect(output.repo).toBe('repo'); - expect(output.owner).toBe('owner'); - }); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts deleted file mode 100644 index c82da51ead..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts +++ /dev/null @@ -1,308 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * 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 { TaskContext, WorkflowRunner, WorkflowResponse } from './types'; -import * as Handlebars from 'handlebars'; -import { TemplateActionRegistry } from '..'; -import { ScmIntegrations } from '@backstage/integration'; -import { parseRepoUrl } from '../actions/builtin/publish/util'; -import { isTruthy } from './helper'; -import { PassThrough } from 'stream'; -import * as winston from 'winston'; -import { Logger } from 'winston'; -import path from 'path'; -import fs from 'fs-extra'; -import { validate as validateJsonSchema } from 'jsonschema'; -import { JsonObject, JsonValue } from '@backstage/types'; -import { InputError } from '@backstage/errors'; -import { TaskSpec, TaskSpecV1beta2 } from '@backstage/plugin-scaffolder-common'; - -type Options = { - workingDirectory: string; - actionRegistry: TemplateActionRegistry; - integrations: ScmIntegrations; - logger: Logger; -}; - -const isValidTaskSpec = (taskSpec: TaskSpec): taskSpec is TaskSpecV1beta2 => - taskSpec.apiVersion === 'backstage.io/v1beta2'; -/** - * This is the legacy workflow runner, which supports handlebars. This entire implementation will be replaced - * with the default workflow runner interface in the future so this entire thing can go bye bye. - */ -export class HandlebarsWorkflowRunner implements WorkflowRunner { - private readonly handlebars: typeof Handlebars; - - constructor(private readonly options: Options) { - this.handlebars = Handlebars.create(); - - // TODO(blam): this should be a public facing API but it's a little - // scary right now, so we're going to lock it off like the component API is - // in the frontend until we can work out a nice way to do it. - this.handlebars.registerHelper('parseRepoUrl', repoUrl => { - return JSON.stringify(parseRepoUrl(repoUrl, this.options.integrations)); - }); - - this.handlebars.registerHelper('projectSlug', repoUrl => { - const { owner, repo } = parseRepoUrl(repoUrl, this.options.integrations); - return `${owner}/${repo}`; - }); - - this.handlebars.registerHelper('json', obj => JSON.stringify(obj)); - - this.handlebars.registerHelper('not', value => !isTruthy(value)); - - this.handlebars.registerHelper('eq', (a, b) => a === b); - } - - async execute(task: TaskContext): Promise { - if (!isValidTaskSpec(task.spec)) { - throw new InputError(`Task spec is not a valid v1beta2 task spec`); - } - - const { actionRegistry } = this.options; - - const workspacePath = path.join( - this.options.workingDirectory, - await task.getWorkspaceName(), - ); - try { - await fs.ensureDir(workspacePath); - await task.emitLog( - `Starting up task with ${task.spec.steps.length} steps`, - ); - - const templateCtx: { - parameters: JsonObject; - steps: { - [stepName: string]: { output: { [outputName: string]: JsonValue } }; - }; - } = { parameters: task.spec.values, steps: {} }; - - for (const step of task.spec.steps) { - const metadata = { stepId: step.id }; - try { - const taskLogger = winston.createLogger({ - level: process.env.LOG_LEVEL || 'info', - format: winston.format.combine( - winston.format.colorize(), - winston.format.timestamp(), - winston.format.simple(), - ), - defaultMeta: {}, - }); - - const stream = new PassThrough(); - stream.on('data', async data => { - const message = data.toString().trim(); - if (message?.length > 1) { - await task.emitLog(message, metadata); - } - }); - - taskLogger.add(new winston.transports.Stream({ stream })); - - if (step.if !== undefined) { - // Support passing values like false to disable steps - let skip = !step.if; - - // Evaluate strings as handlebar templates - if (typeof step.if === 'string') { - const condition = JSON.parse( - JSON.stringify(step.if), - (_key, value) => { - if (typeof value === 'string') { - const templated = this.handlebars.compile(value, { - noEscape: true, - data: false, - preventIndent: true, - })(templateCtx); - - // If it's just an empty string, treat it as undefined - if (templated === '') { - return undefined; - } - - try { - return JSON.parse(templated); - } catch { - return templated; - } - } - - return value; - }, - ); - - skip = !isTruthy(condition); - } - - if (skip) { - await task.emitLog(`Skipped step ${step.name}`, { - ...metadata, - status: 'skipped', - }); - continue; - } - } - - await task.emitLog(`Beginning step ${step.name}`, { - ...metadata, - status: 'processing', - }); - - const action = actionRegistry.get(step.action); - if (!action) { - throw new Error(`Action '${step.action}' does not exist`); - } - - const input = - step.input && - JSON.parse(JSON.stringify(step.input), (_key, value) => { - if (typeof value === 'string') { - const templated = this.handlebars.compile(value, { - noEscape: true, - data: false, - preventIndent: true, - })(templateCtx); - - // If it smells like a JSON object then give it a parse as an object and if it fails return the string - if ( - (templated.startsWith('"') && templated.endsWith('"')) || - (templated.startsWith('{') && templated.endsWith('}')) || - (templated.startsWith('[') && templated.endsWith(']')) - ) { - try { - // Don't recursively JSON parse the values of this string. - // Shouldn't need to, don't want to encourage the use of returning handlebars from somewhere else - return JSON.parse(templated); - } catch { - return templated; - } - } - return templated; - } - - return value; - }); - - if (action.schema?.input) { - const validateResult = validateJsonSchema( - input, - action.schema.input, - ); - if (!validateResult.valid) { - const errors = validateResult.errors.join(', '); - throw new InputError( - `Invalid input passed to action ${action.id}, ${errors}`, - ); - } - } - - const stepOutputs: { [name: string]: JsonValue } = {}; - - // Keep track of all tmp dirs that are created by the action so we can remove them after - const tmpDirs = new Array(); - - this.options.logger.debug(`Running ${action.id} with input`, { - input: JSON.stringify(input, null, 2), - }); - - await action.handler({ - // deprecated in favor of templateInfo.baseUrl - baseUrl: task.spec.baseUrl, - logger: taskLogger, - logStream: stream, - input, - secrets: task.secrets ?? {}, - workspacePath, - async createTemporaryDirectory() { - const tmpDir = await fs.mkdtemp( - `${workspacePath}_step-${step.id}-`, - ); - tmpDirs.push(tmpDir); - return tmpDir; - }, - output(name: string, value: JsonValue) { - stepOutputs[name] = value; - }, - // deprecated in favor of templateInfo - metadata: task.spec.metadata, - templateInfo: task.spec.templateInfo, - }); - - // Remove all temporary directories that were created when executing the action - for (const tmpDir of tmpDirs) { - await fs.remove(tmpDir); - } - - templateCtx.steps[step.id] = { output: stepOutputs }; - - await task.emitLog(`Finished step ${step.name}`, { - ...metadata, - status: 'completed', - }); - } catch (error) { - await task.emitLog(String(error.stack), { - ...metadata, - status: 'failed', - }); - throw error; - } - } - - const output = JSON.parse( - JSON.stringify(task.spec.output), - (_key, value) => { - if (typeof value === 'string') { - const templated = this.handlebars.compile(value, { - noEscape: true, - data: false, - preventIndent: true, - })(templateCtx); - - // If it's just an empty string, treat it as undefined - if (templated === '') { - return undefined; - } - - // If it smells like a JSON object then give it a parse as an object and if it fails return the string - if ( - (templated.startsWith('"') && templated.endsWith('"')) || - (templated.startsWith('{') && templated.endsWith('}')) || - (templated.startsWith('[') && templated.endsWith(']')) - ) { - try { - // Don't recursively JSON parse the values of this string. - // Shouldn't need to, don't want to encourage the use of returning handlebars from somewhere else - return JSON.parse(templated); - } catch { - return templated; - } - } - return templated; - } - return value; - }, - ); - - return { output }; - } finally { - if (workspacePath) { - await fs.remove(workspacePath); - } - } - } -} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index fb9da56c22..0bd5a3c840 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -257,8 +257,6 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { const stepOutput: { [outputName: string]: JsonValue } = {}; await action.handler({ - // deprecated in favourof templateInfo.baseUrl - baseUrl: task.spec.baseUrl, input, secrets: task.secrets ?? {}, logger: taskLogger, @@ -274,8 +272,6 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { output(name: string, value: JsonValue) { stepOutput[name] = value; }, - // deprecated in favour of templateInfo - metadata: task.spec.metadata, templateInfo: task.spec.templateInfo, }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts index 9ff6b9c8a1..1a099e4c53 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts @@ -19,16 +19,10 @@ import { ConfigReader } from '@backstage/config'; import { DatabaseTaskStore } from './DatabaseTaskStore'; import { StorageTaskBroker } from './StorageTaskBroker'; import { TaskWorker } from './TaskWorker'; -import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateActionRegistry } from '../actions'; import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; -jest.mock('./HandlebarsWorkflowRunner'); -const MockedHandlebarsWorkflowRunner = - HandlebarsWorkflowRunner as jest.Mock; -MockedHandlebarsWorkflowRunner.mockImplementation(); - jest.mock('./NunjucksWorkflowRunner'); const MockedNunjucksWorkflowRunner = NunjucksWorkflowRunner as jest.Mock; @@ -58,10 +52,6 @@ describe('TaskWorker', () => { const actionRegistry: TemplateActionRegistry = {} as TemplateActionRegistry; const workingDirectory = '/tmp/scaffolder'; - const handlebarsWorkflowRunner: HandlebarsWorkflowRunner = { - execute: jest.fn(), - } as unknown as HandlebarsWorkflowRunner; - const workflowRunner: NunjucksWorkflowRunner = { execute: jest.fn(), } as unknown as NunjucksWorkflowRunner; @@ -72,47 +62,11 @@ describe('TaskWorker', () => { beforeEach(() => { jest.resetAllMocks(); - MockedHandlebarsWorkflowRunner.mockImplementation( - () => handlebarsWorkflowRunner, - ); MockedNunjucksWorkflowRunner.mockImplementation(() => workflowRunner); }); const logger = getVoidLogger(); - it('should call the legacy workflow runner when the apiVersion is not beta3', async () => { - const broker = new StorageTaskBroker(storage, logger); - const taskWorker = await TaskWorker.create({ - logger, - workingDirectory, - integrations, - taskBroker: broker, - actionRegistry, - }); - - await broker.dispatch({ - spec: { - apiVersion: 'backstage.io/v1beta2', - steps: [{ id: 'test', name: 'test', action: 'not-found-action' }], - output: { - result: '{{ steps.test.output.testOutput }}', - }, - values: {}, - }, - }); - - const task = await broker.claim(); - await taskWorker.runOneTask(task); - - expect(MockedHandlebarsWorkflowRunner).toBeCalledWith({ - actionRegistry, - integrations, - logger, - workingDirectory, - }); - expect(handlebarsWorkflowRunner.execute).toHaveBeenCalled(); - }); - it('should call the default workflow runner when the apiVersion is beta3', async () => { const broker = new StorageTaskBroker(storage, logger); const taskWorker = await TaskWorker.create({ diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index 8de9359806..9defd71c49 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -15,7 +15,6 @@ */ import { TaskContext, TaskBroker, WorkflowRunner } from './types'; -import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner'; import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; import { Logger } from 'winston'; import { TemplateActionRegistry } from '../actions'; @@ -31,7 +30,6 @@ import { TemplateFilter } from '../../lib/templating/SecureTemplater'; export type TaskWorkerOptions = { taskBroker: TaskBroker; runners: { - legacyWorkflowRunner: HandlebarsWorkflowRunner; workflowRunner: WorkflowRunner; }; }; @@ -68,13 +66,6 @@ export class TaskWorker { additionalTemplateFilters, } = options; - const legacyWorkflowRunner = new HandlebarsWorkflowRunner({ - logger, - actionRegistry, - integrations, - workingDirectory, - }); - const workflowRunner = new NunjucksWorkflowRunner({ actionRegistry, integrations, @@ -85,7 +76,7 @@ export class TaskWorker { return new TaskWorker({ taskBroker: taskBroker, - runners: { legacyWorkflowRunner, workflowRunner }, + runners: { workflowRunner }, }); } @@ -100,10 +91,15 @@ export class TaskWorker { async runOneTask(task: TaskContext) { try { - const { output } = - task.spec.apiVersion === 'scaffolder.backstage.io/v1beta3' - ? await this.options.runners.workflowRunner.execute(task) - : await this.options.runners.legacyWorkflowRunner.execute(task); + if (task.spec.apiVersion !== 'scaffolder.backstage.io/v1beta3') { + throw new Error( + `Unsupported Template apiVersion ${task.spec.apiVersion}`, + ); + } + + const { output } = await this.options.runners.workflowRunner.execute( + task, + ); await task.complete('completed', { output }); } catch (error) { diff --git a/plugins/scaffolder-backend/src/service/helpers.ts b/plugins/scaffolder-backend/src/service/helpers.ts index a7148dac40..23200d6aac 100644 --- a/plugins/scaffolder-backend/src/service/helpers.ts +++ b/plugins/scaffolder-backend/src/service/helpers.ts @@ -26,10 +26,7 @@ import { } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { assertError, InputError, NotFoundError } from '@backstage/errors'; -import { - TemplateEntityV1beta2, - TemplateEntityV1beta3, -} from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import fs from 'fs-extra'; import os from 'os'; import { Logger } from 'winston'; @@ -95,7 +92,7 @@ export async function findTemplate(options: { entityRef: CompoundEntityRef; token?: string; catalogApi: CatalogApi; -}): Promise { +}): Promise { const { entityRef, token, catalogApi } = options; if (entityRef.namespace.toLocaleLowerCase('en-US') !== DEFAULT_NAMESPACE) { @@ -114,5 +111,5 @@ export async function findTemplate(options: { ); } - return template as TemplateEntityV1beta3 | TemplateEntityV1beta2; + return template as TemplateEntityV1beta3; } diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 1c57153fb0..41bc73c7ae 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -35,7 +35,7 @@ import { UrlReaders, } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { ConfigReader } from '@backstage/config'; import ObservableImpl from 'zen-observable'; import express from 'express'; @@ -76,8 +76,8 @@ const mockUrlReader = UrlReaders.default({ describe('createRouter', () => { let app: express.Express; let taskBroker: TaskBroker; - const template: TemplateEntityV1beta2 = { - apiVersion: 'backstage.io/v1beta2', + const template: TemplateEntityV1beta3 = { + apiVersion: 'scaffolder.backstage.io/v1beta3', kind: 'Template', metadata: { description: 'Create a new CRA website project', diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index f7e375b0b2..cf073b551b 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -22,11 +22,8 @@ import { Config } from '@backstage/config'; import { InputError, NotFoundError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import { - TemplateEntityV1beta2, TemplateEntityV1beta3, - TaskSpecV1beta3, TaskSpec, - TaskSpecV1beta2, } from '@backstage/plugin-scaffolder-common'; import express from 'express'; import Router from 'express-promise-router'; @@ -61,13 +58,8 @@ export interface RouterOptions { additionalTemplateFilters?: Record; } -function isSupportedTemplate( - entity: TemplateEntityV1beta2 | TemplateEntityV1beta3, -) { - return ( - entity.apiVersion === 'backstage.io/v1beta2' || - entity.apiVersion === 'scaffolder.backstage.io/v1beta3' - ); +function isSupportedTemplate(entity: TemplateEntityV1beta3) { + return entity.apiVersion === 'scaffolder.backstage.io/v1beta3'; } /** @public */ @@ -181,64 +173,7 @@ export async function createRouter( token: getBearerToken(req.headers.authorization), }); - let taskSpec: TaskSpec; - - if (isSupportedTemplate(template)) { - if (template.apiVersion === 'backstage.io/v1beta2') { - logger.warn( - `Scaffolding ${stringifyEntityRef( - template, - )} with deprecated apiVersion ${ - template.apiVersion - }. Please migrate the template to backstage.io/v1beta3. https://backstage.io/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3`, - ); - } - - for (const parameters of [template.spec.parameters ?? []].flat()) { - const result = validate(values, parameters); - if (!result.valid) { - res.status(400).json({ errors: result.errors }); - return; - } - } - - const baseUrl = getEntityBaseUrl(template); - - const baseTaskSpec = { - baseUrl, - steps: template.spec.steps.map((step, index) => ({ - ...step, - id: step.id ?? `step-${index + 1}`, - name: step.name ?? step.action, - })), - output: template.spec.output ?? {}, - - // deprecated in favour of templateInfo - metadata: { name: template.metadata?.name }, - - templateInfo: { - entityRef: stringifyEntityRef({ - kind, - namespace, - name: template.metadata?.name, - }), - baseUrl, - }, - }; - - taskSpec = - template.apiVersion === 'backstage.io/v1beta2' - ? ({ - ...baseTaskSpec, - apiVersion: template.apiVersion, - values, - } as TaskSpecV1beta2) - : ({ - ...baseTaskSpec, - apiVersion: template.apiVersion, - parameters: values, - } as TaskSpecV1beta3); - } else { + if (!isSupportedTemplate(template)) { throw new InputError( `Unsupported apiVersion field in schema entity, ${ (template as Entity).apiVersion @@ -246,6 +181,35 @@ export async function createRouter( ); } + for (const parameters of [template.spec.parameters ?? []].flat()) { + const result = validate(values, parameters); + if (!result.valid) { + res.status(400).json({ errors: result.errors }); + return; + } + } + + const baseUrl = getEntityBaseUrl(template); + + const taskSpec: TaskSpec = { + apiVersion: template.apiVersion, + steps: template.spec.steps.map((step, index) => ({ + ...step, + id: step.id ?? `step-${index + 1}`, + name: step.name ?? step.action, + })), + output: template.spec.output ?? {}, + parameters: values, + templateInfo: { + entityRef: stringifyEntityRef({ + kind, + namespace, + name: template.metadata?.name, + }), + baseUrl, + }, + }; + const result = await taskBroker.dispatch({ spec: taskSpec, secrets: { diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index a456f2bdd3..b675fdb357 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -17,7 +17,7 @@ import React, { ComponentType } from 'react'; import { Routes, Route, useOutlet } from 'react-router'; import { Entity } from '@backstage/catalog-model'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { ScaffolderPage } from './ScaffolderPage'; import { TemplatePage } from './TemplatePage'; import { TaskPage } from './TaskPage'; @@ -35,14 +35,14 @@ import { useElementFilter } from '@backstage/core-plugin-api'; export type RouterProps = { /** @deprecated use components.TemplateCardComponent instead */ TemplateCardComponent?: - | ComponentType<{ template: TemplateEntityV1beta2 }> + | ComponentType<{ template: TemplateEntityV1beta3 }> | undefined; /** @deprecated use component.TaskPageComponent instead */ TaskPageComponent?: ComponentType<{}>; components?: { TemplateCardComponent?: - | ComponentType<{ template: TemplateEntityV1beta2 }> + | ComponentType<{ template: TemplateEntityV1beta3 }> | undefined; TaskPageComponent?: ComponentType<{}>; }; diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index da9f1f74b8..0b3fccc07c 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -24,7 +24,7 @@ import { SupportButton, } from '@backstage/core-components'; import { Entity } from '@backstage/catalog-model'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { useRouteRef } from '@backstage/core-plugin-api'; import { EntityKindPicker, @@ -52,7 +52,7 @@ const useStyles = makeStyles(theme => ({ export type ScaffolderPageProps = { TemplateCardComponent?: - | ComponentType<{ template: TemplateEntityV1beta2 }> + | ComponentType<{ template: TemplateEntityV1beta3 }> | undefined; groups?: Array<{ title?: React.ReactNode; diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index aab5e8348a..f73e32189c 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -298,10 +298,7 @@ export const TaskPage = ({ loadingText }: TaskPageProps) => { return; } - const formData = - taskStream.task!.spec.apiVersion === 'backstage.io/v1beta2' - ? taskStream.task!.spec.values - : taskStream.task!.spec.parameters; + const formData = taskStream.task!.spec.parameters; const { name } = parseEntityRef( taskStream.task!.spec.templateInfo?.entityRef, diff --git a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx index 7275aa3a9e..54f8188e3b 100644 --- a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { ScmIntegrationIcon, scmIntegrationsApiRef, @@ -99,7 +99,7 @@ const useDeprecationStyles = makeStyles(theme => ({ })); export type TemplateCardProps = { - template: TemplateEntityV1beta2; + template: TemplateEntityV1beta3; deprecated?: boolean; }; @@ -112,7 +112,7 @@ type TemplateProps = { }; const getTemplateCardProps = ( - template: TemplateEntityV1beta2, + template: TemplateEntityV1beta3, ): TemplateProps & { key: string } => { return { key: template.metadata.uid!, diff --git a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx index 3d9da542de..e3cf6eac05 100644 --- a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx +++ b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx @@ -16,7 +16,7 @@ import React, { ComponentType } from 'react'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { Content, ContentHeader, @@ -34,7 +34,7 @@ import { TemplateCard } from '../TemplateCard'; */ export type TemplateListProps = { TemplateCardComponent?: - | ComponentType<{ template: TemplateEntityV1beta2 }> + | ComponentType<{ template: TemplateEntityV1beta3 }> | undefined; group?: { title?: React.ReactNode; @@ -106,7 +106,7 @@ export const TemplateList = ({ maybeFilteredEntities.map((template: Entity) => ( ))} From bf231ecc7f75f2033e67afef18cb6beaf16f9e61 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 17:50:06 +0100 Subject: [PATCH 250/353] chore: updating api-report Signed-off-by: blam --- packages/backend-common/api-report.md | 26 ++++----- packages/backend-tasks/api-report.md | 6 +- plugins/airbrake-backend/api-report.md | 4 +- plugins/app-backend/api-report.md | 4 +- plugins/auth-backend/api-report.md | 8 +-- plugins/azure-devops-backend/api-report.md | 6 +- plugins/bazaar-backend/api-report.md | 4 +- .../catalog-backend-module-ldap/api-report.md | 16 ++--- .../api-report.md | 12 ++-- plugins/catalog-graphql/api-report.md | 4 +- plugins/code-coverage-backend/api-report.md | 4 +- plugins/graphql-backend/api-report.md | 4 +- plugins/jenkins-backend/api-report.md | 4 +- plugins/kafka-backend/api-report.md | 2 +- plugins/kubernetes-backend/api-report.md | 8 +-- plugins/permission-backend/api-report.md | 4 +- plugins/proxy-backend/api-report.md | 2 +- plugins/rollbar-backend/api-report.md | 6 +- plugins/scaffolder-backend/api-report.md | 15 +++-- plugins/scaffolder-common/api-report.md | 58 +------------------ plugins/scaffolder/api-report.md | 8 +-- plugins/search-backend/api-report.md | 4 +- .../api-report.md | 6 +- plugins/tech-insights-backend/api-report.md | 6 +- plugins/tech-insights-node/api-report.md | 4 +- plugins/techdocs-backend/api-report.md | 10 ++-- plugins/todo-backend/api-report.md | 4 +- 27 files changed, 90 insertions(+), 149 deletions(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index f9fd164787..1b901ea5ac 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -24,7 +24,7 @@ import { isChildPath } from '@backstage/cli-common'; import { JsonValue } from '@backstage/types'; import { Knex } from 'knex'; import { LoadConfigOptionsRemote } from '@backstage/config-loader'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { MergeResult } from 'isomorphic-git'; import { PushResult } from 'isomorphic-git'; import { Readable } from 'stream'; @@ -135,7 +135,7 @@ export class CacheManager { // @public export type CacheManagerOptions = { - logger?: Logger_2; + logger?: Logger; onError?: (err: Error) => void; }; @@ -187,7 +187,7 @@ export function createServiceBuilder(_module: NodeModule): ServiceBuilder; // @public export function createStatusCheckRouter(options: { - logger: Logger_2; + logger: Logger; path?: string; statusCheck?: StatusCheck; }): Promise; @@ -227,7 +227,7 @@ export function errorHandler( // @public export type ErrorHandlerOptions = { showStackTraces?: boolean; - logger?: Logger_2; + logger?: Logger; logClientErrors?: boolean; }; @@ -297,7 +297,7 @@ export class Git { static fromAuth: (options: { username?: string; password?: string; - logger?: Logger_2; + logger?: Logger; }) => Git; // (undocumented) init(options: { dir: string; defaultBranch?: string }): Promise; @@ -373,7 +373,7 @@ export function isDatabaseConflictError(e: unknown): boolean; // @public export function loadBackendConfig(options: { - logger: Logger_2; + logger: Logger; remote?: LoadConfigOptionsRemote; argv: string[]; }): Promise; @@ -403,7 +403,7 @@ export type PluginEndpointDiscovery = { // @public export type ReaderFactory = (options: { config: Config; - logger: Logger_2; + logger: Logger; treeResponseFactory: ReadTreeResponseFactory; }) => UrlReaderPredicateTuple[]; @@ -480,12 +480,10 @@ export type ReadUrlResponse = { }; // @public -export function requestLoggingHandler(logger?: Logger_2): RequestHandler; +export function requestLoggingHandler(logger?: Logger): RequestHandler; // @public -export type RequestLoggingHandlerFactory = ( - logger?: Logger_2, -) => RequestHandler; +export type RequestLoggingHandlerFactory = (logger?: Logger) => RequestHandler; // @public export function resolvePackagePath(name: string, ...paths: string[]): string; @@ -531,7 +529,7 @@ export class ServerTokenManager implements TokenManager { static fromConfig( config: Config, options: { - logger: Logger_2; + logger: Logger; }, ): ServerTokenManager; // (undocumented) @@ -547,7 +545,7 @@ export type ServiceBuilder = { loadConfig(config: Config): ServiceBuilder; setPort(port: number): ServiceBuilder; setHost(host: string): ServiceBuilder; - setLogger(logger: Logger_2): ServiceBuilder; + setLogger(logger: Logger): ServiceBuilder; enableCors(options: cors.CorsOptions): ServiceBuilder; setHttpsSettings(settings: { certificate: @@ -631,7 +629,7 @@ export class UrlReaders { // @public export type UrlReadersOptions = { config: Config; - logger: Logger_2; + logger: Logger; factories?: ReaderFactory[]; }; diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md index eeb6412840..82b2ca9b0c 100644 --- a/packages/backend-tasks/api-report.md +++ b/packages/backend-tasks/api-report.md @@ -7,7 +7,7 @@ import { AbortSignal as AbortSignal_2 } from 'node-abort-controller'; import { Config } from '@backstage/config'; import { DatabaseManager } from '@backstage/backend-common'; import { Duration } from 'luxon'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; // @public export interface PluginTaskScheduler { @@ -31,14 +31,14 @@ export type TaskFunction = // @public export class TaskScheduler { - constructor(databaseManager: DatabaseManager, logger: Logger_2); + constructor(databaseManager: DatabaseManager, logger: Logger); forPlugin(pluginId: string): PluginTaskScheduler; // (undocumented) static fromConfig( config: Config, options?: { databaseManager?: DatabaseManager; - logger?: Logger_2; + logger?: Logger; }, ): TaskScheduler; } diff --git a/plugins/airbrake-backend/api-report.md b/plugins/airbrake-backend/api-report.md index b9e9a50e0b..3ad74e2506 100644 --- a/plugins/airbrake-backend/api-report.md +++ b/plugins/airbrake-backend/api-report.md @@ -5,7 +5,7 @@ ```ts import { Config } from '@backstage/config'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; // @public export interface AirbrakeConfig { @@ -21,6 +21,6 @@ export function extractAirbrakeConfig(config: Config): AirbrakeConfig; // @public export interface RouterOptions { airbrakeConfig: AirbrakeConfig; - logger: Logger_2; + logger: Logger; } ``` diff --git a/plugins/app-backend/api-report.md b/plugins/app-backend/api-report.md index 50d1b7bc34..e1b0c63469 100644 --- a/plugins/app-backend/api-report.md +++ b/plugins/app-backend/api-report.md @@ -5,7 +5,7 @@ ```ts import { Config } from '@backstage/config'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -23,7 +23,7 @@ export interface RouterOptions { database?: PluginDatabaseManager; disableConfigInjection?: boolean; // (undocumented) - logger: Logger_2; + logger: Logger; staticFallbackHandler?: express.Handler; } ``` diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 416da59b46..4227f1db0a 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -11,7 +11,7 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import express from 'express'; import { JsonValue } from '@backstage/types'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { Profile } from 'passport'; @@ -85,7 +85,7 @@ export type AuthProviderFactoryOptions = { providerId: string; globalConfig: AuthProviderConfig; config: Config; - logger: Logger_2; + logger: Logger; tokenManager: TokenManager; tokenIssuer: TokenIssuer; discovery: PluginEndpointDiscovery; @@ -106,7 +106,7 @@ export interface AuthProviderRouteHandlers { export type AuthResolverContext = { tokenIssuer: TokenIssuer; catalogIdentityClient: CatalogIdentityClient; - logger: Logger_2; + logger: Logger; }; // Warning: (ae-missing-release-tag) "AuthResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -642,7 +642,7 @@ export interface RouterOptions { // (undocumented) discovery: PluginEndpointDiscovery; // (undocumented) - logger: Logger_2; + logger: Logger; // Warning: (ae-forgotten-export) The symbol "ProviderFactories" needs to be exported by the entry point index.d.ts // // (undocumented) diff --git a/plugins/azure-devops-backend/api-report.md b/plugins/azure-devops-backend/api-report.md index 579367e2dd..f9b8b75b82 100644 --- a/plugins/azure-devops-backend/api-report.md +++ b/plugins/azure-devops-backend/api-report.md @@ -10,7 +10,7 @@ import { Config } from '@backstage/config'; import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; import express from 'express'; import { GitRepository } from 'azure-devops-node-api/interfaces/GitInterfaces'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PullRequest } from '@backstage/plugin-azure-devops-common'; import { PullRequestOptions } from '@backstage/plugin-azure-devops-common'; import { RepoBuild } from '@backstage/plugin-azure-devops-common'; @@ -22,7 +22,7 @@ import { WebApi } from 'azure-devops-node-api'; // // @public (undocumented) export class AzureDevOpsApi { - constructor(logger: Logger_2, webApi: WebApi); + constructor(logger: Logger, webApi: WebApi); // (undocumented) getAllTeams(): Promise; // (undocumented) @@ -96,7 +96,7 @@ export interface RouterOptions { // (undocumented) config: Config; // (undocumented) - logger: Logger_2; + logger: Logger; } // (No @packageDocumentation comment for this package) diff --git a/plugins/bazaar-backend/api-report.md b/plugins/bazaar-backend/api-report.md index 89f6da5305..b50994b15d 100644 --- a/plugins/bazaar-backend/api-report.md +++ b/plugins/bazaar-backend/api-report.md @@ -5,7 +5,7 @@ ```ts import { Config } from '@backstage/config'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -22,7 +22,7 @@ export interface RouterOptions { // (undocumented) database: PluginDatabaseManager; // (undocumented) - logger: Logger_2; + logger: Logger; } // (No @packageDocumentation comment for this package) diff --git a/plugins/catalog-backend-module-ldap/api-report.md b/plugins/catalog-backend-module-ldap/api-report.md index f6b2475e65..79646aab01 100644 --- a/plugins/catalog-backend-module-ldap/api-report.md +++ b/plugins/catalog-backend-module-ldap/api-report.md @@ -12,7 +12,7 @@ import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; import { GroupEntity } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/types'; import { LocationSpec } from '@backstage/plugin-catalog-backend'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { SearchEntry } from 'ldapjs'; import { SearchOptions } from 'ldapjs'; import { UserEntity } from '@backstage/catalog-model'; @@ -75,10 +75,10 @@ export const LDAP_UUID_ANNOTATION = 'backstage.io/ldap-uuid'; // @public export class LdapClient { - constructor(client: Client, logger: Logger_2); + constructor(client: Client, logger: Logger); // (undocumented) static create( - logger: Logger_2, + logger: Logger, target: string, bind?: BindConfig, ): Promise; @@ -97,7 +97,7 @@ export class LdapOrgEntityProvider implements EntityProvider { constructor(options: { id: string; provider: LdapProviderConfig; - logger: Logger_2; + logger: Logger; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; }); @@ -111,7 +111,7 @@ export class LdapOrgEntityProvider implements EntityProvider { target: string; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; - logger: Logger_2; + logger: Logger; }, ): LdapOrgEntityProvider; // (undocumented) @@ -123,7 +123,7 @@ export class LdapOrgEntityProvider implements EntityProvider { export class LdapOrgReaderProcessor implements CatalogProcessor { constructor(options: { providers: LdapProviderConfig[]; - logger: Logger_2; + logger: Logger; groupTransformer?: GroupTransformer; userTransformer?: UserTransformer; }); @@ -131,7 +131,7 @@ export class LdapOrgReaderProcessor implements CatalogProcessor { static fromConfig( config: Config, options: { - logger: Logger_2; + logger: Logger; groupTransformer?: GroupTransformer; userTransformer?: UserTransformer; }, @@ -180,7 +180,7 @@ export function readLdapOrg( options: { groupTransformer?: GroupTransformer; userTransformer?: UserTransformer; - logger: Logger_2; + logger: Logger; }, ): Promise<{ users: UserEntity[]; diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index 8df06f54b1..f7d0f112ad 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -10,7 +10,7 @@ import { EntityProvider } from '@backstage/plugin-catalog-backend'; import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; import { GroupEntity } from '@backstage/catalog-model'; import { LocationSpec } from '@backstage/plugin-catalog-backend'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; import * as msal from '@azure/msal-node'; import { Response as Response_2 } from 'node-fetch'; @@ -100,7 +100,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { constructor(options: { id: string; provider: MicrosoftGraphProviderConfig; - logger: Logger_2; + logger: Logger; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; @@ -113,7 +113,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { options: { id: string; target: string; - logger: Logger_2; + logger: Logger; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; @@ -128,7 +128,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { constructor(options: { providers: MicrosoftGraphProviderConfig[]; - logger: Logger_2; + logger: Logger; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; @@ -137,7 +137,7 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { static fromConfig( config: Config, options: { - logger: Logger_2; + logger: Logger; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; @@ -205,7 +205,7 @@ export function readMicrosoftGraphOrg( userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; - logger: Logger_2; + logger: Logger; }, ): Promise<{ users: UserEntity[]; diff --git a/plugins/catalog-graphql/api-report.md b/plugins/catalog-graphql/api-report.md index 019f9c279f..e14fd988d0 100644 --- a/plugins/catalog-graphql/api-report.md +++ b/plugins/catalog-graphql/api-report.md @@ -4,7 +4,7 @@ ```ts import { Config } from '@backstage/config'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { Module } from 'graphql-modules'; // Warning: (ae-missing-release-tag) "createModule" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -19,6 +19,6 @@ export interface ModuleOptions { // (undocumented) config: Config; // (undocumented) - logger: Logger_2; + logger: Logger; } ``` diff --git a/plugins/code-coverage-backend/api-report.md b/plugins/code-coverage-backend/api-report.md index 9e6a84f542..437c48227f 100644 --- a/plugins/code-coverage-backend/api-report.md +++ b/plugins/code-coverage-backend/api-report.md @@ -5,7 +5,7 @@ ```ts import { Config } from '@backstage/config'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { UrlReader } from '@backstage/backend-common'; @@ -39,7 +39,7 @@ export interface RouterOptions { // (undocumented) discovery: PluginEndpointDiscovery; // (undocumented) - logger: Logger_2; + logger: Logger; // (undocumented) urlReader: UrlReader; } diff --git a/plugins/graphql-backend/api-report.md b/plugins/graphql-backend/api-report.md index e6f292c20a..f1707288b7 100644 --- a/plugins/graphql-backend/api-report.md +++ b/plugins/graphql-backend/api-report.md @@ -5,7 +5,7 @@ ```ts import { Config } from '@backstage/config'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -19,6 +19,6 @@ export interface RouterOptions { // (undocumented) config: Config; // (undocumented) - logger: Logger_2; + logger: Logger; } ``` diff --git a/plugins/jenkins-backend/api-report.md b/plugins/jenkins-backend/api-report.md index 264d3912c8..fb155bafe2 100644 --- a/plugins/jenkins-backend/api-report.md +++ b/plugins/jenkins-backend/api-report.md @@ -7,7 +7,7 @@ import { CatalogApi } from '@backstage/catalog-client'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -94,7 +94,7 @@ export interface RouterOptions { // (undocumented) jenkinsInfoProvider: JenkinsInfoProvider; // (undocumented) - logger: Logger_2; + logger: Logger; // (undocumented) permissions?: PermissionAuthorizer; } diff --git a/plugins/kafka-backend/api-report.md b/plugins/kafka-backend/api-report.md index 26a3a44ee4..e73d8e9e03 100644 --- a/plugins/kafka-backend/api-report.md +++ b/plugins/kafka-backend/api-report.md @@ -5,7 +5,7 @@ ```ts import { Config } from '@backstage/config'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; // Warning: (ae-forgotten-export) The symbol "RouterOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index ea273d7632..8a3b64bd93 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -9,7 +9,7 @@ import type { FetchResponse } from '@backstage/plugin-kubernetes-common'; import type { JsonObject } from '@backstage/types'; import type { KubernetesFetchError } from '@backstage/plugin-kubernetes-common'; import type { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import type { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common'; import { PodStatus } from '@kubernetes/client-node/dist/top'; @@ -160,7 +160,7 @@ export interface KubernetesEnvironment { // (undocumented) config: Config; // (undocumented) - logger: Logger_2; + logger: Logger; } // Warning: (ae-missing-release-tag) "KubernetesFetcher" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -197,7 +197,7 @@ export interface KubernetesObjectsProviderOptions { // (undocumented) fetcher: KubernetesFetcher; // (undocumented) - logger: Logger_2; + logger: Logger; // (undocumented) objectTypesToFetch?: ObjectToFetch[]; // (undocumented) @@ -275,7 +275,7 @@ export interface RouterOptions { // (undocumented) config: Config; // (undocumented) - logger: Logger_2; + logger: Logger; } // Warning: (ae-missing-release-tag) "ServiceAccountClusterDetails" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/permission-backend/api-report.md b/plugins/permission-backend/api-report.md index 2b59113535..3c606e3cb9 100644 --- a/plugins/permission-backend/api-report.md +++ b/plugins/permission-backend/api-report.md @@ -6,7 +6,7 @@ import { Config } from '@backstage/config'; import express from 'express'; import { IdentityClient } from '@backstage/plugin-auth-node'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PermissionPolicy } from '@backstage/plugin-permission-node'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -22,7 +22,7 @@ export interface RouterOptions { // (undocumented) identity: IdentityClient; // (undocumented) - logger: Logger_2; + logger: Logger; // (undocumented) policy: PermissionPolicy; } diff --git a/plugins/proxy-backend/api-report.md b/plugins/proxy-backend/api-report.md index b71783cabf..45ef63f4d9 100644 --- a/plugins/proxy-backend/api-report.md +++ b/plugins/proxy-backend/api-report.md @@ -5,7 +5,7 @@ ```ts import { Config } from '@backstage/config'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; // Warning: (ae-forgotten-export) The symbol "RouterOptions" needs to be exported by the entry point index.d.ts diff --git a/plugins/rollbar-backend/api-report.md b/plugins/rollbar-backend/api-report.md index d51292121f..1c0800a393 100644 --- a/plugins/rollbar-backend/api-report.md +++ b/plugins/rollbar-backend/api-report.md @@ -5,7 +5,7 @@ ```ts import { Config } from '@backstage/config'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -25,7 +25,7 @@ export function getRequestHeaders(token: string): { // // @public (undocumented) export class RollbarApi { - constructor(accessToken: string, logger: Logger_2); + constructor(accessToken: string, logger: Logger); // (undocumented) getActivatedCounts( projectName: string, @@ -107,7 +107,7 @@ export interface RouterOptions { // (undocumented) config: Config; // (undocumented) - logger: Logger_2; + logger: Logger; // (undocumented) rollbarApi?: RollbarApi; } diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 612ddedecc..d57cd4b2b8 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -17,7 +17,7 @@ import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { Knex } from 'knex'; import { LocationSpec } from '@backstage/plugin-catalog-backend'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { Observable } from '@backstage/types'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { Schema } from 'jsonschema'; @@ -25,22 +25,21 @@ import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmIntegrations } from '@backstage/integration'; import { SpawnOptionsWithoutStdio } from 'child_process'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; +import { TaskSpecV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; -import { TemplateMetadata } from '@backstage/plugin-scaffolder-common'; import { UrlReader } from '@backstage/backend-common'; import { Writable } from 'stream'; // @public export type ActionContext = { baseUrl?: string; - logger: Logger_2; + logger: Logger; logStream: Writable; secrets?: TaskSecrets; workspacePath: string; input: Input; output(name: string, value: JsonValue): void; createTemporaryDirectory(): Promise; - metadata?: TemplateMetadata; templateInfo?: TemplateInfo; }; @@ -297,7 +296,7 @@ export type CreateWorkerOptions = { actionRegistry: TemplateActionRegistry; integrations: ScmIntegrations; workingDirectory: string; - logger: Logger_2; + logger: Logger; additionalTemplateFilters?: Record; }; @@ -404,7 +403,7 @@ export interface RouterOptions { // (undocumented) database: PluginDatabaseManager; // (undocumented) - logger: Logger_2; + logger: Logger; // (undocumented) reader: UrlReader; // (undocumented) @@ -519,7 +518,7 @@ export class TaskManager implements TaskContext { static create( task: CurrentClaimedTask, storage: TaskStore, - logger: Logger_2, + logger: Logger, ): TaskManager; // (undocumented) get done(): boolean; @@ -530,7 +529,7 @@ export class TaskManager implements TaskContext { // (undocumented) get secrets(): TaskSecrets | undefined; // (undocumented) - get spec(): TaskSpec; + get spec(): TaskSpecV1beta3; } // @public diff --git a/plugins/scaffolder-common/api-report.md b/plugins/scaffolder-common/api-report.md index 1872aa3ed8..6148440e6d 100644 --- a/plugins/scaffolder-common/api-report.md +++ b/plugins/scaffolder-common/api-report.md @@ -9,36 +9,12 @@ import { JsonValue } from '@backstage/types'; import { KindValidator } from '@backstage/catalog-model'; // @public -export type TaskSpec = TaskSpecV1beta2 | TaskSpecV1beta3; - -// @public @deprecated -export interface TaskSpecV1beta2 { - // (undocumented) - apiVersion: 'backstage.io/v1beta2'; - // @deprecated (undocumented) - baseUrl?: string; - // @deprecated (undocumented) - metadata?: TemplateMetadata; - // (undocumented) - output: { - [name: string]: string; - }; - // (undocumented) - steps: TaskStep[]; - // (undocumented) - templateInfo?: TemplateInfo; - // (undocumented) - values: JsonObject; -} +export type TaskSpec = TaskSpecV1beta3; // @public export interface TaskSpecV1beta3 { // (undocumented) apiVersion: 'scaffolder.backstage.io/v1beta3'; - // @deprecated (undocumented) - baseUrl?: string; - // @deprecated (undocumented) - metadata?: TemplateMetadata; // (undocumented) output: { [name: string]: JsonValue; @@ -65,33 +41,6 @@ export interface TaskStep { name: string; } -// @public @deprecated -export interface TemplateEntityV1beta2 extends Entity { - // (undocumented) - apiVersion: 'backstage.io/v1beta2'; - // (undocumented) - kind: 'Template'; - // (undocumented) - spec: { - type: string; - parameters?: JsonObject | JsonObject[]; - steps: Array<{ - id?: string; - name?: string; - action: string; - input?: JsonObject; - if?: string | boolean; - }>; - output?: { - [name: string]: string; - }; - owner?: string; - }; -} - -// @public @deprecated -export const templateEntityV1beta2Validator: KindValidator; - // @public export interface TemplateEntityV1beta3 extends Entity { // (undocumented) @@ -124,9 +73,4 @@ export type TemplateInfo = { entityRef: string; baseUrl?: string; }; - -// @public @deprecated -export type TemplateMetadata = { - name: string; -}; ``` diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 9326a61fd5..cc6fc0f3df 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -24,7 +24,7 @@ import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; -import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common'; +import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; // @public export function createScaffolderFieldExtension< @@ -222,14 +222,14 @@ export interface RepoUrlPickerUiOptions { export type RouterProps = { TemplateCardComponent?: | ComponentType<{ - template: TemplateEntityV1beta2; + template: TemplateEntityV1beta3; }> | undefined; TaskPageComponent?: ComponentType<{}>; components?: { TemplateCardComponent?: | ComponentType<{ - template: TemplateEntityV1beta2; + template: TemplateEntityV1beta3; }> | undefined; TaskPageComponent?: ComponentType<{}>; @@ -427,7 +427,7 @@ export const TemplateList: ({ export type TemplateListProps = { TemplateCardComponent?: | ComponentType<{ - template: TemplateEntityV1beta2; + template: TemplateEntityV1beta3; }> | undefined; group?: { diff --git a/plugins/search-backend/api-report.md b/plugins/search-backend/api-report.md index d9d408395f..3c7f9c2754 100644 --- a/plugins/search-backend/api-report.md +++ b/plugins/search-backend/api-report.md @@ -6,7 +6,7 @@ import { Config } from '@backstage/config'; import { DocumentTypeInfo } from '@backstage/plugin-search-common'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; import { SearchEngine } from '@backstage/plugin-search-backend-node'; @@ -23,6 +23,6 @@ export type RouterOptions = { types: Record; permissions: PermissionAuthorizer; config: Config; - logger: Logger_2; + logger: Logger; }; ``` diff --git a/plugins/tech-insights-backend-module-jsonfc/api-report.md b/plugins/tech-insights-backend-module-jsonfc/api-report.md index 2e62e7d6a0..69e4054170 100644 --- a/plugins/tech-insights-backend-module-jsonfc/api-report.md +++ b/plugins/tech-insights-backend-module-jsonfc/api-report.md @@ -7,7 +7,7 @@ import { BooleanCheckResult } from '@backstage/plugin-tech-insights-common'; import { CheckResponse } from '@backstage/plugin-tech-insights-common'; import { CheckValidationResponse } from '@backstage/plugin-tech-insights-node'; import { FactChecker } from '@backstage/plugin-tech-insights-node'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { Operator } from 'json-rules-engine'; import { TechInsightCheck } from '@backstage/plugin-tech-insights-node'; import { TechInsightCheckRegistry } from '@backstage/plugin-tech-insights-node'; @@ -79,7 +79,7 @@ export class JsonRulesEngineFactCheckerFactory { // @public export type JsonRulesEngineFactCheckerFactoryOptions = { checks: TechInsightJsonRuleCheck[]; - logger: Logger_2; + logger: Logger; checkRegistry?: TechInsightCheckRegistry; operators?: Operator[]; }; @@ -88,7 +88,7 @@ export type JsonRulesEngineFactCheckerFactoryOptions = { export type JsonRulesEngineFactCheckerOptions = { checks: TechInsightJsonRuleCheck[]; repository: TechInsightsStore; - logger: Logger_2; + logger: Logger; checkRegistry?: TechInsightCheckRegistry; operators?: Operator[]; }; diff --git a/plugins/tech-insights-backend/api-report.md b/plugins/tech-insights-backend/api-report.md index 1d19123a00..d56e3c8082 100644 --- a/plugins/tech-insights-backend/api-report.md +++ b/plugins/tech-insights-backend/api-report.md @@ -11,7 +11,7 @@ import { FactCheckerFactory } from '@backstage/plugin-tech-insights-node'; import { FactLifecycle } from '@backstage/plugin-tech-insights-node'; import { FactRetriever } from '@backstage/plugin-tech-insights-node'; import { FactRetrieverRegistration } from '@backstage/plugin-tech-insights-node'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { TechInsightCheck } from '@backstage/plugin-tech-insights-node'; @@ -63,7 +63,7 @@ export interface RouterOptions< > { config: Config; factChecker?: FactChecker; - logger: Logger_2; + logger: Logger; persistenceContext: PersistenceContext; } @@ -93,7 +93,7 @@ export interface TechInsightsOptions< factCheckerFactory?: FactCheckerFactory; factRetrievers: FactRetrieverRegistration[]; // (undocumented) - logger: Logger_2; + logger: Logger; } // (No @packageDocumentation comment for this package) diff --git a/plugins/tech-insights-node/api-report.md b/plugins/tech-insights-node/api-report.md index 77a9d77871..45426587e3 100644 --- a/plugins/tech-insights-node/api-report.md +++ b/plugins/tech-insights-node/api-report.md @@ -7,7 +7,7 @@ import { CheckResult } from '@backstage/plugin-tech-insights-common'; import { Config } from '@backstage/config'; import { DateTime } from 'luxon'; import { DurationLike } from 'luxon'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; // @public @@ -56,7 +56,7 @@ export interface FactRetriever { export type FactRetrieverContext = { config: Config; discovery: PluginEndpointDiscovery; - logger: Logger_2; + logger: Logger; entityFilter?: | Record[] | Record; diff --git a/plugins/techdocs-backend/api-report.md b/plugins/techdocs-backend/api-report.md index f306ce4f35..157f049953 100644 --- a/plugins/techdocs-backend/api-report.md +++ b/plugins/techdocs-backend/api-report.md @@ -12,7 +12,7 @@ import { Entity } from '@backstage/catalog-model'; import express from 'express'; import { GeneratorBuilder } from '@backstage/plugin-techdocs-node'; import { Knex } from 'knex'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { Permission } from '@backstage/plugin-permission-common'; import { PluginCacheManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -71,7 +71,7 @@ export type OutOfTheBoxDeploymentOptions = { preparers: PreparerBuilder; generators: GeneratorBuilder; publisher: PublisherBase; - logger: Logger_2; + logger: Logger; discovery: PluginEndpointDiscovery; database?: Knex; config: Config; @@ -82,7 +82,7 @@ export type OutOfTheBoxDeploymentOptions = { // @public export type RecommendedDeploymentOptions = { publisher: PublisherBase; - logger: Logger_2; + logger: Logger; discovery: PluginEndpointDiscovery; config: Config; cache: PluginCacheManager; @@ -102,7 +102,7 @@ export type ShouldBuildParameters = { // @public export type TechDocsCollatorFactoryOptions = { discovery: PluginEndpointDiscovery; - logger: Logger_2; + logger: Logger; tokenManager: TokenManager; locationTemplate?: string; catalogClient?: CatalogApi; @@ -113,7 +113,7 @@ export type TechDocsCollatorFactoryOptions = { // @public export type TechDocsCollatorOptions = { discovery: PluginEndpointDiscovery; - logger: Logger_2; + logger: Logger; tokenManager: TokenManager; locationTemplate?: string; catalogClient?: CatalogApi; diff --git a/plugins/todo-backend/api-report.md b/plugins/todo-backend/api-report.md index f3f2af3fb9..277c87fea9 100644 --- a/plugins/todo-backend/api-report.md +++ b/plugins/todo-backend/api-report.md @@ -7,7 +7,7 @@ import { CatalogApi } from '@backstage/catalog-client'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { ScmIntegrations } from '@backstage/integration'; import { UrlReader } from '@backstage/backend-common'; @@ -127,7 +127,7 @@ export class TodoScmReader implements TodoReader { // @public (undocumented) export type TodoScmReaderOptions = { - logger: Logger_2; + logger: Logger; reader: UrlReader; integrations: ScmIntegrations; parser?: TodoParser; From c00cb930db13d217fce7f8f8cb59477184a5f94f Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 17:51:14 +0100 Subject: [PATCH 251/353] chore: updating handlebars Signed-off-by: blam --- plugins/scaffolder-backend/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 2b96e72831..83726abdb7 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -56,7 +56,6 @@ "fs-extra": "10.0.0", "git-url-parse": "^11.6.0", "globby": "^11.0.0", - "handlebars": "^4.7.6", "isbinaryfile": "^4.0.8", "isomorphic-git": "^1.8.0", "jsonschema": "^1.2.6", From b6ee925d0547bd5287470a1192bc0b74f2a189f9 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 18:23:48 +0100 Subject: [PATCH 252/353] chore: last of the removals for scaffolder-backend + scaffolder-common Signed-off-by: blam --- .../actions/builtin/createBuiltinActions.ts | 1 + .../src/scaffolder/actions/builtin/helpers.ts | 7 ------ .../src/scaffolder/actions/builtin/index.ts | 2 +- .../src/scaffolder/actions/types.ts | 6 ----- .../src/scaffolder/tasks/StorageTaskBroker.ts | 8 ------- .../src/scaffolder/tasks/index.ts | 5 +--- .../src/scaffolder/tasks/types.ts | 24 ------------------- 7 files changed, 3 insertions(+), 50 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 7405dc4de8..d7f15cdf13 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -78,6 +78,7 @@ export const createBuiltinActions = ( config, additionalTemplateFilters, } = options; + const githubCredentialsProvider: GithubCredentialsProvider = DefaultGithubCredentialsProvider.fromIntegrations(integrations); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts index 79537ff8dc..75e22522d5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts @@ -71,13 +71,6 @@ export const executeShellCommand = async (options: RunCommandOptions) => { }); }; -/** - * Run a command in a sub-process, normally a shell command. - * @public - * @deprecated use {@link executeShellCommand} instead - */ -export const runCommand = executeShellCommand; - export async function initRepoAndPush({ dir, remoteUrl, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts index 553481ba40..7846ff96fc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts @@ -22,5 +22,5 @@ export * from './filesystem'; export * from './publish'; export * from './github'; -export { runCommand, executeShellCommand } from './helpers'; +export { executeShellCommand } from './helpers'; export type { RunCommandOptions } from './helpers'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts index f73c4dce3f..9a4ae8b2bf 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts @@ -26,12 +26,6 @@ import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; * @public */ export type ActionContext = { - /** - * Base URL for the location of the task spec, typically the url of the source entity file. - * @deprecated please use templateInfo.baseUrl instead - */ - baseUrl?: string; - logger: Logger; logStream: Writable; secrets?: TaskSecrets; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 0fd593f2f3..1399c25118 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -120,14 +120,6 @@ export interface CurrentClaimedTask { secrets?: TaskSecrets; } -/** - * TaskState - * - * @public - * @deprecated use CurrentClaimedTask instead - */ -export type TaskState = CurrentClaimedTask; - function defer() { let resolve = () => {}; const promise = new Promise(_resolve => { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts index b108a3308d..457c32d9b5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts @@ -16,24 +16,21 @@ export { DatabaseTaskStore } from './DatabaseTaskStore'; export type { DatabaseTaskStoreOptions } from './DatabaseTaskStore'; export { TaskManager } from './StorageTaskBroker'; -export type { CurrentClaimedTask, TaskState } from './StorageTaskBroker'; +export type { CurrentClaimedTask } from './StorageTaskBroker'; export { TaskWorker } from './TaskWorker'; export type { CreateWorkerOptions } from './TaskWorker'; export type { TaskSecrets, TaskCompletionState, - CompletedTaskState, TaskStoreEmitOptions, TaskStoreListEventsOptions, SerializedTask, SerializedTaskEvent, - Status, TaskStatus, TaskEventType, TaskBroker, TaskContext, TaskStore, - DispatchResult, TaskBrokerDispatchResult, TaskBrokerDispatchOptions, TaskStoreCreateTaskOptions, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 57b92e0222..2af98b111b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -29,14 +29,6 @@ export type TaskStatus = | 'cancelled' | 'completed'; -/** - * The status of each step of the Task - * - * @public - * @deprecated use TaskStatus instead - */ -export type Status = TaskStatus; - /** * The state of a completed task. * @@ -44,14 +36,6 @@ export type Status = TaskStatus; */ export type TaskCompletionState = 'failed' | 'completed'; -/** - * The state of a completed task. - * - * @public - * @deprecated use TaskCompletionState instead - */ -export type CompletedTaskState = TaskCompletionState; - /** * SerializedTask * @@ -115,14 +99,6 @@ export type TaskBrokerDispatchOptions = { secrets?: TaskSecrets; }; -/** - * DispatchResult - * - * @public - * @deprecated use TaskBrokerDispatchResult instead - */ -export type DispatchResult = TaskBrokerDispatchResult; - /** * Task * From 8cd27668b57ad1b744ca5a924dac9fbf2286b66f Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 18:26:45 +0100 Subject: [PATCH 253/353] chore: updating api-report for scaffolder backend Signed-off-by: blam --- plugins/scaffolder-backend/api-report.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index d57cd4b2b8..6934f2ef83 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -32,7 +32,6 @@ import { Writable } from 'stream'; // @public export type ActionContext = { - baseUrl?: string; logger: Logger; logStream: Writable; secrets?: TaskSecrets; @@ -43,9 +42,6 @@ export type ActionContext = { templateInfo?: TemplateInfo; }; -// @public @deprecated -export type CompletedTaskState = TaskCompletionState; - // @public export const createBuiltinActions: ( options: CreateBuiltInActionsOptions, @@ -359,9 +355,6 @@ export type DatabaseTaskStoreOptions = { database: Knex; }; -// @public @deprecated -export type DispatchResult = TaskBrokerDispatchResult; - // @public export const executeShellCommand: (options: RunCommandOptions) => Promise; @@ -412,9 +405,6 @@ export interface RouterOptions { taskWorkers?: number; } -// @public @deprecated -export const runCommand: (options: RunCommandOptions) => Promise; - // @public (undocumented) export type RunCommandOptions = { command: string; @@ -456,9 +446,6 @@ export type SerializedTaskEvent = { createdAt: string; }; -// @public @deprecated -export type Status = TaskStatus; - // @public export interface TaskBroker { // (undocumented) @@ -537,9 +524,6 @@ export type TaskSecrets = Record & { backstageToken?: string; }; -// @public @deprecated -export type TaskState = CurrentClaimedTask; - // @public export type TaskStatus = | 'open' From 310e90599869e0ce2cf853edf866166311d2f205 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 3 Mar 2022 18:30:48 +0100 Subject: [PATCH 254/353] chore: added changeset, needs a better description Signed-off-by: blam --- .changeset/fair-roses-hide.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/fair-roses-hide.md diff --git a/.changeset/fair-roses-hide.md b/.changeset/fair-roses-hide.md new file mode 100644 index 0000000000..e45bf86b39 --- /dev/null +++ b/.changeset/fair-roses-hide.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-scaffolder': minor +'@backstage/plugin-scaffolder-backend': minor +'@backstage/plugin-scaffolder-common': minor +'@backstage/create-app': patch +'@backstage/plugin-catalog-backend': patch +--- + +deprecations From 571319d4e2cc2d17a5d90136470b61e1d0800804 Mon Sep 17 00:00:00 2001 From: blam Date: Sat, 5 Mar 2022 17:25:20 +0000 Subject: [PATCH 255/353] chore: finishg off some other api-report fixing Signed-off-by: blam --- .../api-report.md | 6 ++--- plugins/search-backend-node/api-report.md | 10 ++++----- plugins/techdocs-node/api-report.md | 22 +++++++++---------- yarn.lock | 2 +- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/plugins/search-backend-module-elasticsearch/api-report.md b/plugins/search-backend-module-elasticsearch/api-report.md index 50e5a61a14..265fdda686 100644 --- a/plugins/search-backend-module-elasticsearch/api-report.md +++ b/plugins/search-backend-module-elasticsearch/api-report.md @@ -10,7 +10,7 @@ import { Client } from '@elastic/elasticsearch'; import { Config } from '@backstage/config'; import type { ConnectionOptions } from 'tls'; import { IndexableDocument } from '@backstage/plugin-search-common'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { SearchEngine } from '@backstage/plugin-search-common'; import { SearchQuery } from '@backstage/plugin-search-common'; import { SearchResultSet } from '@backstage/plugin-search-common'; @@ -104,7 +104,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { elasticSearchClientOptions: ElasticSearchClientOptions, aliasPostfix: string, indexPrefix: string, - logger: Logger_2, + logger: Logger, ); // Warning: (ae-forgotten-export) The symbol "ElasticSearchOptions" needs to be exported by the entry point index.d.ts // @@ -153,7 +153,7 @@ export type ElasticSearchSearchEngineIndexerOptions = { indexPrefix: string; indexSeparator: string; alias: string; - logger: Logger_2; + logger: Logger; elasticSearchClient: Client; }; ``` diff --git a/plugins/search-backend-node/api-report.md b/plugins/search-backend-node/api-report.md index 44e2d0c92d..e4359eafac 100644 --- a/plugins/search-backend-node/api-report.md +++ b/plugins/search-backend-node/api-report.md @@ -9,7 +9,7 @@ import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; import { DocumentDecoratorFactory } from '@backstage/plugin-search-common'; import { DocumentTypeInfo } from '@backstage/plugin-search-common'; import { IndexableDocument } from '@backstage/plugin-search-common'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { default as lunr_2 } from 'lunr'; import { QueryTranslator } from '@backstage/plugin-search-common'; import { Readable } from 'stream'; @@ -69,7 +69,7 @@ export class IndexBuilder { // @beta (undocumented) export type IndexBuilderOptions = { searchEngine: SearchEngine; - logger: Logger_2; + logger: Logger; }; // @beta (undocumented) @@ -77,13 +77,13 @@ export type LunrQueryTranslator = (query: SearchQuery) => ConcreteLunrQuery; // @beta (undocumented) export class LunrSearchEngine implements SearchEngine { - constructor({ logger }: { logger: Logger_2 }); + constructor({ logger }: { logger: Logger }); // (undocumented) protected docStore: Record; // (undocumented) getIndexer(type: string): Promise; // (undocumented) - protected logger: Logger_2; + protected logger: Logger; // (undocumented) protected lunrIndices: Record; // (undocumented) @@ -122,7 +122,7 @@ export interface RegisterDecoratorParameters { // @beta (undocumented) export class Scheduler { - constructor({ logger }: { logger: Logger_2 }); + constructor({ logger }: { logger: Logger }); addToSchedule(task: Function, interval: number): void; start(): void; stop(): void; diff --git a/plugins/techdocs-node/api-report.md b/plugins/techdocs-node/api-report.md index 768865896b..4c88ab0fc0 100644 --- a/plugins/techdocs-node/api-report.md +++ b/plugins/techdocs-node/api-report.md @@ -11,7 +11,7 @@ import { ContainerRunner } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import express from 'express'; import { IndexableDocument } from '@backstage/plugin-search-common'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { UrlReader } from '@backstage/backend-common'; @@ -20,7 +20,7 @@ import { Writable } from 'stream'; // @public export class DirectoryPreparer implements PreparerBase { // @deprecated - constructor(config: Config, _logger: Logger_2 | null, reader: UrlReader); + constructor(config: Config, _logger: Logger | null, reader: UrlReader); static fromConfig( config: Config, { logger, reader }: PreparerConfig, @@ -45,7 +45,7 @@ export type GeneratorBuilder = { // @public export type GeneratorOptions = { containerRunner: ContainerRunner; - logger: Logger_2; + logger: Logger; }; // @public @@ -54,7 +54,7 @@ export type GeneratorRunOptions = { outputDir: string; parsedLocationAnnotation?: ParsedLocationAnnotation; etag?: string; - logger: Logger_2; + logger: Logger; logStream?: Writable; }; @@ -63,7 +63,7 @@ export class Generators implements GeneratorBuilder { static fromConfig( config: Config, options: { - logger: Logger_2; + logger: Logger; containerRunner: ContainerRunner; }, ): Promise; @@ -78,7 +78,7 @@ export const getDocFilesFromRepository: ( opts?: | { etag?: string | undefined; - logger?: Logger_2 | undefined; + logger?: Logger | undefined; } | undefined, ) => Promise; @@ -120,13 +120,13 @@ export type PreparerBuilder = { // @public export type PreparerConfig = { - logger: Logger_2; + logger: Logger; reader: UrlReader; }; // @public export type PreparerOptions = { - logger?: Logger_2; + logger?: Logger; etag?: ETag; }; @@ -168,7 +168,7 @@ export interface PublisherBase { // @public export type PublisherFactory = { - logger: Logger_2; + logger: Logger; discovery: PluginEndpointDiscovery; }; @@ -216,7 +216,7 @@ export interface TechDocsDocument extends IndexableDocument { // @public export class TechdocsGenerator implements GeneratorBase { constructor(options: { - logger: Logger_2; + logger: Logger; containerRunner: ContainerRunner; config: Config; scmIntegrations: ScmIntegrationRegistry; @@ -251,7 +251,7 @@ export const transformDirLocation: ( // @public export class UrlPreparer implements PreparerBase { // @deprecated - constructor(reader: UrlReader, logger: Logger_2); + constructor(reader: UrlReader, logger: Logger); static fromConfig({ reader, logger }: PreparerConfig): UrlPreparer; prepare(entity: Entity, options?: PreparerOptions): Promise; } diff --git a/yarn.lock b/yarn.lock index a463e71d89..b9140d19b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13539,7 +13539,7 @@ handle-thing@^2.0.0: resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@^4.7.3, handlebars@^4.7.6, handlebars@^4.7.7: +handlebars@^4.7.3, handlebars@^4.7.7: version "4.7.7" resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== From 6b8d918eb7d1631105e0db9e5b6d2c7d57ec8cd2 Mon Sep 17 00:00:00 2001 From: blam Date: Sat, 5 Mar 2022 17:36:28 +0000 Subject: [PATCH 256/353] chore: added better changeset Signed-off-by: blam --- .changeset/fair-roses-hide.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.changeset/fair-roses-hide.md b/.changeset/fair-roses-hide.md index e45bf86b39..8f2c7a6660 100644 --- a/.changeset/fair-roses-hide.md +++ b/.changeset/fair-roses-hide.md @@ -2,8 +2,20 @@ '@backstage/plugin-scaffolder': minor '@backstage/plugin-scaffolder-backend': minor '@backstage/plugin-scaffolder-common': minor -'@backstage/create-app': patch -'@backstage/plugin-catalog-backend': patch --- -deprecations +The following deprecations are now breaking and have been removed: + +- **BREAKING**: Support for `backstage.io/v1beta2` Software Templates has been removed. Please migrate your legacy templates to the new `scaffolder.backstage.io/v1beta3` `apiVersion` by following the [migration guide](https://backstage.io/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3) + +- **BREAKING**: Removed the deprecated `TemplateMetadata`. Please use `TemplateInfo` instead. + +- **BREAKING**: Removed the deprecated `context.baseUrl`. It's now available on `context.templateInfo.baseUrl`. + +- **BREAKING**: Removed the deprecated `DispatchResult`, use `TaskBrokerDispatchResult` instead. + +- **BREAKING**: Removed the deprecated `runCommand`, use `executeShellCommond` instead. + +- **BREAKING**: Removed the deprecated `Status` in favour of `TaskStatus` instead. + +- **BREAKING**: Removed the deprecated `TaskState` in favour of `CurrentClaimedTask` instead. From 55150919ed9f68f12c877dc824cc5e681cd8f2f6 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 7 Mar 2022 10:15:01 +0000 Subject: [PATCH 257/353] chore: added another changeset for more visibility Signed-off-by: blam --- .changeset/something-else-here.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/something-else-here.md diff --git a/.changeset/something-else-here.md b/.changeset/something-else-here.md new file mode 100644 index 0000000000..46e5efcb07 --- /dev/null +++ b/.changeset/something-else-here.md @@ -0,0 +1,6 @@ +--- +'@backstage/create-app': patch +'@backstage/plugin-catalog-backend': minor +--- + +- **BREAKING**: Support for `backstage.io/v1beta2` Software Templates has been removed. Please migrate your legacy templates to the new `scaffolder.backstage.io/v1beta3` `apiVersion` by following the [migration guide](https://backstage.io/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3) From 4b88f8dac9798054667912b3a8ac737bb467de78 Mon Sep 17 00:00:00 2001 From: blam Date: Mon, 7 Mar 2022 10:44:37 +0000 Subject: [PATCH 258/353] chore: last update of the api-reports \ Signed-off-by: blam --- .../catalog-backend-module-aws/api-report.md | 4 +- .../api-report.md | 6 +-- .../api-report.md | 4 +- plugins/catalog-backend/api-report.md | 40 +++++++++---------- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/plugins/catalog-backend-module-aws/api-report.md b/plugins/catalog-backend-module-aws/api-report.md index ef58210e11..876562fc8d 100644 --- a/plugins/catalog-backend-module-aws/api-report.md +++ b/plugins/catalog-backend-module-aws/api-report.md @@ -8,7 +8,7 @@ import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; import { CatalogProcessorParser } from '@backstage/plugin-catalog-backend'; import { Config } from '@backstage/config'; import { LocationSpec } from '@backstage/plugin-catalog-backend'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { UrlReader } from '@backstage/backend-common'; // @public @@ -17,7 +17,7 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { static fromConfig( config: Config, options: { - logger: Logger_2; + logger: Logger; }, ): AwsOrganizationCloudAccountProcessor; // (undocumented) diff --git a/plugins/catalog-backend-module-azure/api-report.md b/plugins/catalog-backend-module-azure/api-report.md index f2a9ad7cea..139db6aab5 100644 --- a/plugins/catalog-backend-module-azure/api-report.md +++ b/plugins/catalog-backend-module-azure/api-report.md @@ -7,20 +7,20 @@ import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; import { Config } from '@backstage/config'; import { LocationSpec } from '@backstage/plugin-catalog-backend'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { ScmIntegrationRegistry } from '@backstage/integration'; // @public export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrationRegistry; - logger: Logger_2; + logger: Logger; }); // (undocumented) static fromConfig( config: Config, options: { - logger: Logger_2; + logger: Logger; }, ): AzureDevOpsDiscoveryProcessor; // (undocumented) diff --git a/plugins/catalog-backend-module-gitlab/api-report.md b/plugins/catalog-backend-module-gitlab/api-report.md index f3cb961136..ec10f80939 100644 --- a/plugins/catalog-backend-module-gitlab/api-report.md +++ b/plugins/catalog-backend-module-gitlab/api-report.md @@ -7,7 +7,7 @@ import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; import { Config } from '@backstage/config'; import { LocationSpec } from '@backstage/plugin-catalog-backend'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; // @public export class GitLabDiscoveryProcessor implements CatalogProcessor { @@ -15,7 +15,7 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { static fromConfig( config: Config, options: { - logger: Logger_2; + logger: Logger; }, ): GitLabDiscoveryProcessor; // (undocumented) diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 5d8c5904a1..1ec53aa9e7 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -22,7 +22,7 @@ import { GitHubIntegrationConfig } from '@backstage/integration'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { Location as Location_2 } from '@backstage/catalog-client'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; import { Permission } from '@backstage/plugin-permission-common'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; import { PermissionCondition } from '@backstage/plugin-permission-common'; @@ -104,9 +104,9 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor { parser?: (options: { integration: BitbucketIntegration; target: string; - logger: Logger_2; + logger: Logger; }) => AsyncIterable; - logger: Logger_2; + logger: Logger; }); // (undocumented) static fromConfig( @@ -115,9 +115,9 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor { parser?: (options: { integration: BitbucketIntegration; target: string; - logger: Logger_2; + logger: Logger; }) => AsyncIterable; - logger: Logger_2; + logger: Logger; }, ): BitbucketDiscoveryProcessor; // (undocumented) @@ -134,7 +134,7 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor { export type BitbucketRepositoryParser = (options: { integration: BitbucketIntegration; target: string; - logger: Logger_2; + logger: Logger; }) => AsyncIterable; // @public (undocumented) @@ -222,7 +222,7 @@ export type CatalogEntityDocument = CatalogEntityDocument_2; // @public (undocumented) export type CatalogEnvironment = { - logger: Logger_2; + logger: Logger; database: PluginDatabaseManager; config: Config; reader: UrlReader; @@ -337,14 +337,14 @@ export type CatalogRulesEnforcer = { export class CodeOwnersProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrationRegistry; - logger: Logger_2; + logger: Logger; reader: UrlReader; }); // (undocumented) static fromConfig( config: Config, options: { - logger: Logger_2; + logger: Logger; reader: UrlReader; }, ): CodeOwnersProcessor; @@ -452,7 +452,7 @@ export class DefaultCatalogProcessingOrchestrator constructor(options: { processors: CatalogProcessor[]; integrations: ScmIntegrationRegistry; - logger: Logger_2; + logger: Logger; parser: CatalogProcessorParser; policy: EntityPolicy; rulesEnforcer: CatalogRulesEnforcer; @@ -640,14 +640,14 @@ function generalError( export class GithubDiscoveryProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrationRegistry; - logger: Logger_2; + logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; }); // (undocumented) static fromConfig( config: Config, options: { - logger: Logger_2; + logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; }, ): GithubDiscoveryProcessor; @@ -672,7 +672,7 @@ export type GithubMultiOrgConfig = Array<{ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrationRegistry; - logger: Logger_2; + logger: Logger; orgs: GithubMultiOrgConfig; githubCredentialsProvider?: GithubCredentialsProvider; }); @@ -680,7 +680,7 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { static fromConfig( config: Config, options: { - logger: Logger_2; + logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; }, ): GithubMultiOrgReaderProcessor; @@ -700,7 +700,7 @@ export class GitHubOrgEntityProvider implements EntityProvider { id: string; orgUrl: string; gitHubConfig: GitHubIntegrationConfig; - logger: Logger_2; + logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; }); // (undocumented) @@ -711,7 +711,7 @@ export class GitHubOrgEntityProvider implements EntityProvider { options: { id: string; orgUrl: string; - logger: Logger_2; + logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; }, ): GitHubOrgEntityProvider; @@ -725,14 +725,14 @@ export class GitHubOrgEntityProvider implements EntityProvider { export class GithubOrgReaderProcessor implements CatalogProcessor { constructor(options: { integrations: ScmIntegrationRegistry; - logger: Logger_2; + logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; }); // (undocumented) static fromConfig( config: Config, options: { - logger: Logger_2; + logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; }, ): GithubOrgReaderProcessor; @@ -1007,7 +1007,7 @@ export interface RouterOptions { // (undocumented) locationService: LocationService; // (undocumented) - logger: Logger_2; + logger: Logger; // (undocumented) permissionIntegrationRouter?: express.Router; // (undocumented) @@ -1016,7 +1016,7 @@ export interface RouterOptions { // @public (undocumented) export class UrlReaderProcessor implements CatalogProcessor { - constructor(options: { reader: UrlReader; logger: Logger_2 }); + constructor(options: { reader: UrlReader; logger: Logger }); // (undocumented) getProcessorName(): string; // (undocumented) From 402e94f590c1b87d3c22d8df5265c5afe8d1553c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 7 Mar 2022 13:17:13 +0100 Subject: [PATCH 259/353] add lowercase gitlab to make vale less idiotic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .github/styles/vocab.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 6ed539530d..aa94c3d953 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -103,6 +103,7 @@ Firestore Fiverr gitbeaker GitHub +gitlab GitLab Gource Grafana From 9461f73643f9b5cee593420505bfeacd8de3009d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sat, 5 Mar 2022 16:58:28 +0100 Subject: [PATCH 260/353] try a convenience thing for scheduling providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/new-books-protect.md | 30 ++++ .changeset/silent-cats-kneel.md | 2 +- .changeset/ten-queens-dance.md | 7 + docs/integrations/ldap/org.md | 68 +++------- packages/backend-tasks/api-report.md | 34 +++-- .../src/tasks/PluginTaskSchedulerImpl.test.ts | 27 ++++ .../src/tasks/PluginTaskSchedulerImpl.ts | 19 ++- packages/backend-tasks/src/tasks/index.ts | 4 +- packages/backend-tasks/src/tasks/types.ts | 81 ++++++++--- .../catalog-backend-module-ldap/api-report.md | 21 +-- .../catalog-backend-module-ldap/package.json | 2 + .../src/processors/LdapOrgEntityProvider.ts | 128 +++++++++++++----- .../src/processors/index.ts | 1 + 13 files changed, 301 insertions(+), 123 deletions(-) create mode 100644 .changeset/new-books-protect.md create mode 100644 .changeset/ten-queens-dance.md diff --git a/.changeset/new-books-protect.md b/.changeset/new-books-protect.md new file mode 100644 index 0000000000..2a561b5e5c --- /dev/null +++ b/.changeset/new-books-protect.md @@ -0,0 +1,30 @@ +--- +'@backstage/plugin-catalog-backend-module-ldap': minor +--- + +**BREAKING**: Added a `schedule` field to `LdapOrgEntityProvider.fromConfig`, which is required. If you want to retain the old behavior of scheduling the provider manually, you can set it to the string value `'manual'`. But you may want to leverage the ability to instead pass in the recurring task schedule information directly. This will allow you to simplify your backend setup code to not need an intermediate variable and separate scheduling code at the bottom. + +All things said, a typical setup might now look as follows: + +```diff + // packages/backend/src/plugins/catalog.ts ++import { Duration } from 'luxon'; ++import { LdapOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-ldap'; + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + const builder = await CatalogBuilder.create(env); ++ // The target parameter below needs to match the ldap.providers.target ++ // value specified in your app-config. ++ builder.addEntityProvider( ++ LdapOrgEntityProvider.fromConfig(env.config, { ++ id: 'our-ldap-master', ++ target: 'ldaps://ds.example.net', ++ logger: env.logger, ++ schedule: env.scheduler.createTaskSchedule({ ++ frequency: Duration.fromObject({ minutes: 60 }), ++ timeout: Duration.fromObject({ minutes: 15 }), ++ }), ++ }), ++ ); +``` diff --git a/.changeset/silent-cats-kneel.md b/.changeset/silent-cats-kneel.md index f34fc98afc..9bd9cce0e0 100644 --- a/.changeset/silent-cats-kneel.md +++ b/.changeset/silent-cats-kneel.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-backend-module-gitlab': minor --- -Added package, moving out gitlab specific functionality from the catalog-backend +Added package, moving out GitLab specific functionality from the catalog-backend diff --git a/.changeset/ten-queens-dance.md b/.changeset/ten-queens-dance.md new file mode 100644 index 0000000000..947675aa46 --- /dev/null +++ b/.changeset/ten-queens-dance.md @@ -0,0 +1,7 @@ +--- +'@backstage/backend-tasks': minor +--- + +**BREAKING**: The `TaskDefinition` type has been removed, and replaced by the equal pair `TaskScheduleDefinition` and `TaskInvocationDefinition`. The interface for `PluginTaskScheduler.scheduleTask` stays effectively unchanged, so this only affects you if you use the actual types directly. + +Added the method `PluginTaskScheduler.createTaskSchedule`, which returns a `TaskSchedule` wrapper that is convenient to pass down into classes that want to control their task invocations while the caller wants to retain control of the actual schedule chosen. diff --git a/docs/integrations/ldap/org.md b/docs/integrations/ldap/org.md index 645524493b..b0fa2ca76d 100644 --- a/docs/integrations/ldap/org.md +++ b/docs/integrations/ldap/org.md @@ -32,55 +32,29 @@ yarn add @backstage/plugin-catalog-backend-module-ldap Update the catalog plugin initialization in your backend to add the provider and schedule it: -```ts -// packages/backend/src/plugins/catalog.ts -import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; -import { Router } from 'express'; -import { PluginEnvironment } from '../types'; -import { Duration } from 'luxon'; -import { LdapOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-ldap'; +```diff + // packages/backend/src/plugins/catalog.ts ++import { Duration } from 'luxon'; ++import { LdapOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-ldap'; -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - // The target parameter below needs to match the ldap.providers.target - // value specified in your app-config - const ldapEntityProvider = LdapOrgEntityProvider.fromConfig(env.config, { - id: 'our-ldap-master', - target: 'ldaps://ds.example.net', - logger: env.logger, - }); + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + const builder = await CatalogBuilder.create(env); - const builder = await CatalogBuilder.create(env); - builder.addEntityProvider(ldapEntityProvider); - - // You can change the refresh interval for the other catalog entries - // independently, or just leave the line below out to use the default - // refresh interval. Note that this interval does NOT at all affect - // the LDAP refresh when using the provider method, which is good! - builder.setRefreshIntervalSeconds(100); - - const { processingEngine, router } = await builder.build(); - await processingEngine.start(); - - // Only perform this scheduling after starting the processing engine - await env.scheduler.scheduleTask({ - id: 'refresh_ldap', - // frequency sets how often you want to ingest users and groups from - // LDAP, in this case every 60 minutes - frequency: Duration.fromObject({ minutes: 60 }), - timeout: Duration.fromObject({ minutes: 15 }), - fn: async () => { - try { - await ldapEntityProvider.read(); - } catch (error) { - env.logger.error(error); - } - }, - }); - - return router; -} ++ // The target parameter below needs to match the ldap.providers.target ++ // value specified in your app-config. ++ builder.addEntityProvider( ++ LdapOrgEntityProvider.fromConfig(env.config, { ++ id: 'our-ldap-master', ++ target: 'ldaps://ds.example.net', ++ logger: env.logger, ++ schedule: env.scheduler.createTaskSchedule({ ++ frequency: Duration.fromObject({ minutes: 60 }), ++ timeout: Duration.fromObject({ minutes: 15 }), ++ }), ++ }), ++ ); ``` After this, you also have to add some configuration in your app-config that diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md index 82b2ca9b0c..db4dee0c65 100644 --- a/packages/backend-tasks/api-report.md +++ b/packages/backend-tasks/api-report.md @@ -11,17 +11,10 @@ import { Logger } from 'winston'; // @public export interface PluginTaskScheduler { - scheduleTask(task: TaskDefinition): Promise; -} - -// @public -export interface TaskDefinition { - fn: TaskFunction; - frequency: Duration; - id: string; - initialDelay?: Duration; - signal?: AbortSignal_2; - timeout: Duration; + createTaskSchedule(schedule: TaskScheduleDefinition): TaskSchedule; + scheduleTask( + task: TaskScheduleDefinition & TaskInvocationDefinition, + ): Promise; } // @public @@ -29,6 +22,25 @@ export type TaskFunction = | ((abortSignal: AbortSignal_2) => void | Promise) | (() => void | Promise); +// @public +export interface TaskInvocationDefinition { + fn: TaskFunction; + id: string; + signal?: AbortSignal_2; +} + +// @public +export interface TaskSchedule { + run(task: TaskInvocationDefinition): Promise; +} + +// @public +export interface TaskScheduleDefinition { + frequency: Duration; + initialDelay?: Duration; + timeout: Duration; +} + // @public export class TaskScheduler { constructor(databaseManager: DatabaseManager, logger: Logger); diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts index e387b85413..0a811a5b88 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts @@ -59,4 +59,31 @@ describe('PluginTaskManagerImpl', () => { 60_000, ); }); + + // This is just to test the wrapper code; most of the actual tests are in + // TaskWorker.test.ts + describe('createTaskSchedule', () => { + it.each(databases.eachSupportedId())( + 'can run the happy path, %p', + async databaseId => { + const { manager } = await init(databaseId); + + const fn = jest.fn(); + await manager + .createTaskSchedule({ + timeout: Duration.fromMillis(5000), + frequency: Duration.fromMillis(5000), + }) + .run({ + id: 'task1', + fn, + }); + + await waitForExpect(() => { + expect(fn).toBeCalled(); + }); + }, + 60_000, + ); + }); }); diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts index 93975bc327..b410e4d3b5 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts @@ -17,7 +17,12 @@ import { Knex } from 'knex'; import { Logger } from 'winston'; import { TaskWorker } from './TaskWorker'; -import { PluginTaskScheduler, TaskDefinition } from './types'; +import { + PluginTaskScheduler, + TaskInvocationDefinition, + TaskSchedule, + TaskScheduleDefinition, +} from './types'; import { validateId } from './util'; /** @@ -29,7 +34,9 @@ export class PluginTaskSchedulerImpl implements PluginTaskScheduler { private readonly logger: Logger, ) {} - async scheduleTask(task: TaskDefinition): Promise { + async scheduleTask( + task: TaskScheduleDefinition & TaskInvocationDefinition, + ): Promise { validateId(task.id); const knex = await this.databaseFactory(); @@ -47,4 +54,12 @@ export class PluginTaskSchedulerImpl implements PluginTaskScheduler { }, ); } + + createTaskSchedule(schedule: TaskScheduleDefinition): TaskSchedule { + return { + run: async task => { + await this.scheduleTask({ ...task, ...schedule }); + }, + }; + } } diff --git a/packages/backend-tasks/src/tasks/index.ts b/packages/backend-tasks/src/tasks/index.ts index 9e0a06f71c..d925f089e8 100644 --- a/packages/backend-tasks/src/tasks/index.ts +++ b/packages/backend-tasks/src/tasks/index.ts @@ -17,6 +17,8 @@ export { TaskScheduler } from './TaskScheduler'; export type { PluginTaskScheduler, - TaskDefinition, TaskFunction, + TaskInvocationDefinition, + TaskSchedule, + TaskScheduleDefinition, } from './types'; diff --git a/packages/backend-tasks/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts index 4693af7ef3..3e46acf55e 100644 --- a/packages/backend-tasks/src/tasks/types.ts +++ b/packages/backend-tasks/src/tasks/types.ts @@ -31,27 +31,11 @@ export type TaskFunction = | (() => void | Promise); /** - * Options that apply to the invocation of a given task. + * Options that control the scheduling of a task. * * @public */ -export interface TaskDefinition { - /** - * A unique ID (within the scope of the plugin) for the task. - */ - id: string; - - /** - * The actual task function to be invoked regularly. - */ - fn: TaskFunction; - - /** - * An abort signal that, when triggered, will stop the recurring execution of - * the task. - */ - signal?: AbortSignal; - +export interface TaskScheduleDefinition { /** * The maximum amount of time that a single task invocation can take, before * it's considered timed out and gets "released" such that a new invocation @@ -91,6 +75,43 @@ export interface TaskDefinition { initialDelay?: Duration; } +/** + * Options that apply to the invocation of a given task. + * + * @public + */ +export interface TaskInvocationDefinition { + /** + * A unique ID (within the scope of the plugin) for the task. + */ + id: string; + + /** + * The actual task function to be invoked regularly. + */ + fn: TaskFunction; + + /** + * An abort signal that, when triggered, will stop the recurring execution of + * the task. + */ + signal?: AbortSignal; +} + +/** + * A previously prepared task schedule, ready to be invoked. + * + * @public + */ +export interface TaskSchedule { + /** + * Takes the schedule and executes an actual task using it. + * + * @param task - The actual runtime properties of the task + */ + run(task: TaskInvocationDefinition): Promise; +} + /** * Deals with the scheduling of distributed tasks, for a given plugin. * @@ -99,15 +120,33 @@ export interface TaskDefinition { export interface PluginTaskScheduler { /** * Schedules a task function for coordinated exclusive invocation across - * workers. + * workers. This convenience method performs both the scheduling and + * invocation in one go. + * + * @remarks * * If the task was already scheduled since before by us or by another party, * its options are just overwritten with the given options, and things * continue from there. * - * @param definition - The task definition + * @param task - The task definition */ - scheduleTask(task: TaskDefinition): Promise; + scheduleTask( + task: TaskScheduleDefinition & TaskInvocationDefinition, + ): Promise; + + /** + * Creates a task schedule, ready to be invoked at a later time. + * + * @remarks + * + * This method is useful for pre-creating a schedule in outer code to be + * passed into an inner implementation, such that the outer code controls + * scheduling while inner code controls implementation. + * + * @param schedule - The task schedule + */ + createTaskSchedule(schedule: TaskScheduleDefinition): TaskSchedule; } function isValidOptionalDurationString(d: string | undefined): boolean { diff --git a/plugins/catalog-backend-module-ldap/api-report.md b/plugins/catalog-backend-module-ldap/api-report.md index 79646aab01..25f479f73c 100644 --- a/plugins/catalog-backend-module-ldap/api-report.md +++ b/plugins/catalog-backend-module-ldap/api-report.md @@ -15,6 +15,7 @@ import { LocationSpec } from '@backstage/plugin-catalog-backend'; import { Logger } from 'winston'; import { SearchEntry } from 'ldapjs'; import { SearchOptions } from 'ldapjs'; +import { TaskSchedule } from '@backstage/backend-tasks'; import { UserEntity } from '@backstage/catalog-model'; // @public @@ -106,17 +107,21 @@ export class LdapOrgEntityProvider implements EntityProvider { // (undocumented) static fromConfig( configRoot: Config, - options: { - id: string; - target: string; - userTransformer?: UserTransformer; - groupTransformer?: GroupTransformer; - logger: Logger; - }, + options: LdapOrgEntityProviderOptions, ): LdapOrgEntityProvider; // (undocumented) getProviderName(): string; - read(): Promise; + read(options?: { logger?: Logger }): Promise; +} + +// @public +export interface LdapOrgEntityProviderOptions { + groupTransformer?: GroupTransformer; + id: string; + logger: Logger; + schedule: 'manual' | TaskSchedule; + target: string; + userTransformer?: UserTransformer; } // @public diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 8f05dceb4e..d1c9e764fd 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -33,6 +33,7 @@ "start": "backstage-cli package start" }, "dependencies": { + "@backstage/backend-tasks": "^0.1.10", "@backstage/catalog-model": "^0.12.0", "@backstage/config": "^0.1.15", "@backstage/errors": "^0.2.2", @@ -41,6 +42,7 @@ "@types/ldapjs": "^2.2.0", "ldapjs": "^2.2.0", "lodash": "^4.17.21", + "uuid": "^8.0.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts index 4c5bd2e3bd..9c483c7b94 100644 --- a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { TaskSchedule } from '@backstage/backend-tasks'; import { ANNOTATION_LOCATION, ANNOTATION_ORIGIN_LOCATION, @@ -25,6 +26,7 @@ import { EntityProviderConnection, } from '@backstage/plugin-catalog-backend'; import { merge } from 'lodash'; +import * as uuid from 'uuid'; import { Logger } from 'winston'; import { GroupTransformer, @@ -36,6 +38,54 @@ import { UserTransformer, } from '../ldap'; +/** + * Options for {@link LdapOrgEntityProvider}. + * + * @public + */ +export interface LdapOrgEntityProviderOptions { + /** + * A unique, stable identifier for this provider. + * + * @example "production" + */ + id: string; + + /** + * The target that this provider should consume. + * + * Should exactly match the "target" field of one of the "ldap.providers" + * configuration entries. + * + * @example "ldaps://ds-read.example.net" + */ + target: string; + + /** + * The logger to use. + */ + logger: Logger; + + /** + * The refresh schedule to use. + * + * If you pass in 'manual', you are responsible for calling the `read` + * method manually at some interval. If not, it will be automatically + * called regularly with the given schedule using the scheduler. + */ + schedule: 'manual' | TaskSchedule; + + /** + * The function that transforms a user entry in LDAP to an entity. + */ + userTransformer?: UserTransformer; + + /** + * The function that transforms a group entry in LDAP to an entity. + */ + groupTransformer?: GroupTransformer; +} + /** * Reads user and group entries out of an LDAP service, and provides them as * User and Group entities for the catalog. @@ -49,35 +99,11 @@ import { */ export class LdapOrgEntityProvider implements EntityProvider { private connection?: EntityProviderConnection; + private scheduleFn?: () => Promise; static fromConfig( configRoot: Config, - options: { - /** - * A unique, stable identifier for this provider. - * - * @example "production" - */ - id: string; - /** - * The target that this provider should consume. - * - * Should exactly match the "target" field of one of the "ldap.providers" - * configuration entries. - * - * @example "ldaps://ds-read.example.net" - */ - target: string; - /** - * The function that transforms a user entry in LDAP to an entity. - */ - userTransformer?: UserTransformer; - /** - * The function that transforms a group entry in LDAP to an entity. - */ - groupTransformer?: GroupTransformer; - logger: Logger; - }, + options: LdapOrgEntityProviderOptions, ): LdapOrgEntityProvider { // TODO(freben): Deprecate the old catalog.processors.ldapOrg config const config = @@ -101,13 +127,17 @@ export class LdapOrgEntityProvider implements EntityProvider { target: options.target, }); - return new LdapOrgEntityProvider({ + const result = new LdapOrgEntityProvider({ id: options.id, provider, userTransformer: options.userTransformer, groupTransformer: options.groupTransformer, logger, }); + + result.schedule(options.schedule); + + return result; } constructor( @@ -128,18 +158,20 @@ export class LdapOrgEntityProvider implements EntityProvider { /** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.connect} */ async connect(connection: EntityProviderConnection) { this.connection = connection; + await this.scheduleFn?.(); } /** - * Runs one complete ingestion loop. Call this method regularly at some - * appropriate cadence. + * Runs one single complete ingestion. This is only necessary if you use + * manual scheduling. */ - async read() { + async read(options?: { logger?: Logger }) { if (!this.connection) { throw new Error('Not initialized'); } - const { markReadComplete } = trackProgress(this.options.logger); + const logger = options?.logger ?? this.options.logger; + const { markReadComplete } = trackProgress(logger); // Be lazy and create the client each time; even though it's pretty // inefficient, we usually only do this once per entire refresh loop and @@ -157,7 +189,7 @@ export class LdapOrgEntityProvider implements EntityProvider { { groupTransformer: this.options.groupTransformer, userTransformer: this.options.userTransformer, - logger: this.options.logger, + logger, }, ); @@ -173,6 +205,38 @@ export class LdapOrgEntityProvider implements EntityProvider { markCommitComplete(); } + + private schedule(schedule: LdapOrgEntityProviderOptions['schedule']) { + if (schedule === 'manual') { + return; + } + + this.scheduleFn = async () => { + const id = this.getScheduledTaskId(); + await schedule.run({ + id, + fn: async () => { + const logger = this.options.logger.child({ + class: LdapOrgEntityProvider.prototype.constructor.name, + taskId: id, + taskInstanceId: uuid.v4(), + }); + + try { + await this.read({ logger }); + } catch (error) { + logger.error(error); + } + }, + }); + }; + } + + // Gets a suitable scheduler task ID for this provider instance + private getScheduledTaskId(): string { + const rawId = `refresh_${this.getProviderName()}`; + return rawId.toLocaleLowerCase('en-US').replace(/[^a-z0-9]/g, '_'); + } } // Helps wrap the timing and logging behaviors diff --git a/plugins/catalog-backend-module-ldap/src/processors/index.ts b/plugins/catalog-backend-module-ldap/src/processors/index.ts index 96e1a49cbb..5ed0095c3e 100644 --- a/plugins/catalog-backend-module-ldap/src/processors/index.ts +++ b/plugins/catalog-backend-module-ldap/src/processors/index.ts @@ -15,4 +15,5 @@ */ export { LdapOrgEntityProvider } from './LdapOrgEntityProvider'; +export type { LdapOrgEntityProviderOptions } from './LdapOrgEntityProvider'; export { LdapOrgReaderProcessor } from './LdapOrgReaderProcessor'; From 4a6c14a708015faab2b4fbb5f9676e0d10e7a716 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 7 Mar 2022 11:46:10 +0100 Subject: [PATCH 261/353] cli: added new config/eslint-factory module for creating ESLint configuration based on package role Signed-off-by: Patrik Oldsberg --- packages/cli/config/eslint-factory.js | 286 ++++++++++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 packages/cli/config/eslint-factory.js diff --git a/packages/cli/config/eslint-factory.js b/packages/cli/config/eslint-factory.js new file mode 100644 index 0000000000..67f30ac359 --- /dev/null +++ b/packages/cli/config/eslint-factory.js @@ -0,0 +1,286 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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. + */ + +const { join: joinPath } = require('path'); + +/** + * Creates a ESLint configuration that extends the base Backstage configuration. + * In addition to the standard ESLint configuration options, the `extraConfig` + * parameter also accepts the following keys: + * + * - `tsRules`: Additional ESLint rules to apply to TypeScript + * - `testRules`: Additional ESLint rules to apply to tests + * - `restrictedImports`: Additional paths to add to no-restricted-imports + * - `restrictedSrcImports`: Additional paths to add to no-restricted-imports in src files + * - `restrictedTestImports`: Additional paths to add to no-restricted-imports in test files + * - `restrictedSyntax`: Additional patterns to add to no-restricted-syntax + * - `restrictedSrcSyntax`: Additional patterns to add to no-restricted-syntax in src files + * - `restrictedTestSyntax`: Additional patterns to add to no-restricted-syntax in test files + */ +function createConfig(dir, extraConfig = {}) { + const { + extends: extraExtends, + plugins, + env, + parserOptions, + ignorePatterns, + + rules, + tsRules, + testRules, + + restrictedImports, + restrictedSrcImports, + restrictedTestImports, + restrictedSyntax, + restrictedSrcSyntax, + restrictedTestSyntax, + + ...otherConfig + } = extraConfig; + + return { + ...otherConfig, + extends: [ + '@spotify/eslint-config-base', + '@spotify/eslint-config-typescript', + 'prettier', + 'plugin:jest/recommended', + 'plugin:monorepo/recommended', + ...(extraExtends ?? []), + ], + parser: '@typescript-eslint/parser', + plugins: ['import', ...(plugins ?? [])], + env: { + jest: true, + ...env, + }, + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + lib: require('./tsconfig.json').compilerOptions.lib, + ...parserOptions, + }, + ignorePatterns: [ + '.eslintrc.js', + '**/dist/**', + '**/dist-types/**', + ...(ignorePatterns ?? []), + ], + rules: { + 'no-shadow': 'off', + 'no-redeclare': 'off', + '@typescript-eslint/no-shadow': 'error', + '@typescript-eslint/no-redeclare': 'error', + 'no-undef': 'off', + 'import/newline-after-import': 'error', + 'import/no-extraneous-dependencies': [ + 'error', + { + devDependencies: dir + ? [ + `!${joinPath(dir, 'src/**')}`, + joinPath(dir, 'src/**/*.test.*'), + joinPath(dir, 'src/**/*.stories.*'), + joinPath(dir, 'src/setupTests.*'), + ] + : [ + // Legacy config for packages that don't provide a dir + '**/*.test.*', + '**/*.stories.*', + '**/src/setupTests.*', + '**/dev/**', + ], + optionalDependencies: true, + peerDependencies: true, + bundledDependencies: true, + }, + ], + 'no-unused-expressions': 'off', + '@typescript-eslint/no-unused-expressions': 'error', + '@typescript-eslint/consistent-type-assertions': 'error', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + vars: 'all', + args: 'after-used', + ignoreRestSiblings: true, + argsIgnorePattern: '^_', + }, + ], + + 'no-restricted-imports': [ + 2, + { + paths: [ + ...(restrictedImports ?? []), + ...(restrictedSrcImports ?? []), + ], + // Avoid cross-package imports + patterns: ['**/../../**/*/src/**', '**/../../**/*/src'], + }, + ], + + 'no-restricted-syntax': [ + 'error', + ...(restrictedSyntax ?? []), + ...(restrictedSrcSyntax ?? []), + ], + + ...rules, + }, + overrides: [ + { + files: ['**/*.ts?(x)'], + rules: { + '@typescript-eslint/no-unused-vars': 'off', + 'no-undef': 'off', + ...tsRules, + }, + }, + { + files: ['**/*.test.*', '**/*.stories.*', 'src/setupTests.*', '!src/**'], + rules: { + ...testRules, + 'no-restricted-syntax': [ + 'error', + ...(restrictedSyntax ?? []), + ...(restrictedTestSyntax ?? []), + ], + 'no-restricted-imports': [ + 2, + { + paths: [ + ...(restrictedImports ?? []), + ...(restrictedTestImports ?? []), + ], + // Avoid cross-package imports + patterns: ['**/../../**/*/src/**', '**/../../**/*/src'], + }, + ], + }, + }, + ], + }; +} + +/** + * Creates a ESLint configuration for the given package role. + * The `extraConfig` parameter accepts the same keys as for the `createConfig` function. + */ +function createConfigForRole(dir, role, extraConfig = {}) { + switch (role) { + case 'common-library': + return createConfig(dir, extraConfig); + + case 'web-library': + case 'frontend': + case 'frontend-plugin': + case 'frontend-plugin-module': + return createConfig(dir, { + ...extraConfig, + extends: [ + '@spotify/eslint-config-react', + ...(extraConfig.extends ?? []), + ], + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ...extraConfig.parserOptions, + }, + settings: { + react: { + version: 'detect', + }, + ...extraConfig.settings, + }, + restrictedImports: [ + { + // Importing the entire MUI icons packages kills build performance as the list of icons is huge. + name: '@material-ui/icons', + message: "Please import '@material-ui/icons/' instead.", + }, + { + name: '@material-ui/icons/', // because this is possible too ._. + message: "Please import '@material-ui/icons/' instead.", + }, + ...require('module').builtinModules, + ...(extraConfig.restrictedImports ?? []), + ], + tsRules: { + 'react/prop-types': 0, + ...extraConfig.tsRules, + }, + }); + + case 'cli': + case 'node-library': + case 'backend': + case 'backend-plugin': + case 'backend-plugin-module': + return createConfig(dir, { + ...extraConfig, + globals: { + __non_webpack_require__: 'readonly', + ...extraConfig.globals, + }, + rules: { + 'no-console': 0, // Permitted in console programs + 'new-cap': ['error', { capIsNew: false }], // Because Express constructs things e.g. like 'const r = express.Router()' + ...extraConfig.rules, + }, + restrictedSyntax: [ + { + message: + 'Default import from winston is not allowed, import `* as winston` instead.', + selector: + 'ImportDeclaration[source.value="winston"] ImportDefaultSpecifier', + }, + ...(extraConfig.restrictedSyntax ?? []), + ], + restrictedSrcSyntax: [ + { + message: + "`__dirname` doesn't refer to the same dir in production builds, try `resolvePackagePath()` from `@backstage/backend-common` instead.", + selector: 'Identifier[name="__dirname"]', + }, + ...(extraConfig.restrictedSrcSyntax ?? []), + ], + }); + default: + throw new Error(`Unknown package role ${role}`); + } +} + +/** + * Creates a ESLint configuration for the package in the given directory. + * The `extraConfig` parameter accepts the same keys as for the `createConfig` function. + */ +function createPackageConfig(dir, extraConfig) { + const pkg = require(joinPath(dir, 'package.json')); + const role = pkg.backstage?.role; + if (!role) { + throw new Error(`Package ${pkg.name} does not have a backstage.role`); + } + + return createConfigForRole(dir, role, extraConfig); +} + +module.exports = createPackageConfig; // Alias +module.exports.createConfig = createConfig; +module.exports.createConfigForRole = createConfigForRole; +module.exports.createPackageConfig = createPackageConfig; From 37818baf644dc8d448e3f65db487bb45e81c7521 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 7 Mar 2022 13:52:10 +0100 Subject: [PATCH 262/353] cli: switch existing eslint configs to use factory Signed-off-by: Patrik Oldsberg --- packages/cli/config/eslint.backend.js | 91 ++----------------------- packages/cli/config/eslint.js | 97 ++------------------------- 2 files changed, 8 insertions(+), 180 deletions(-) diff --git a/packages/cli/config/eslint.backend.js b/packages/cli/config/eslint.backend.js index 519b388b96..1851312be5 100644 --- a/packages/cli/config/eslint.backend.js +++ b/packages/cli/config/eslint.backend.js @@ -14,90 +14,7 @@ * limitations under the License. */ -const globalRestrictedSyntax = [ - { - message: - 'Default import from winston is not allowed, import `* as winston` instead.', - selector: - 'ImportDeclaration[source.value="winston"] ImportDefaultSpecifier', - }, -]; - -module.exports = { - extends: [ - '@spotify/eslint-config-base', - '@spotify/eslint-config-typescript', - 'prettier', - 'plugin:jest/recommended', - 'plugin:monorepo/recommended', - ], - parser: '@typescript-eslint/parser', - plugins: ['import'], - env: { - jest: true, - }, - globals: { - __non_webpack_require__: 'readonly', - }, - parserOptions: { - ecmaVersion: 2018, - sourceType: 'module', - lib: require('./tsconfig.json').compilerOptions.lib, - }, - ignorePatterns: ['.eslintrc.js', '**/dist/**', '**/dist-types/**'], - rules: { - 'no-shadow': 'off', - 'no-redeclare': 'off', - '@typescript-eslint/no-shadow': 'error', - '@typescript-eslint/no-redeclare': 'error', - - 'no-console': 0, // Permitted in console programs - 'new-cap': ['error', { capIsNew: false }], // Because Express constructs things e.g. like 'const r = express.Router()' - 'import/newline-after-import': 'error', - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: ['**/*.test.*', 'src/setupTests.*', 'dev/**'], - optionalDependencies: true, - peerDependencies: true, - bundledDependencies: true, - }, - ], - 'no-unused-expressions': 'off', - '@typescript-eslint/no-unused-expressions': 'error', - '@typescript-eslint/no-unused-vars': [ - 'warn', - { vars: 'all', args: 'after-used', ignoreRestSiblings: true }, - ], - // Avoid cross-package imports - 'no-restricted-imports': [ - 2, - { patterns: ['**/../../**/*/src/**', '**/../../**/*/src'] }, - ], - // Avoid default import from winston as it breaks at runtime - 'no-restricted-syntax': [ - 'error', - { - message: - "`__dirname` doesn't refer to the same dir in production builds, try `resolvePackagePath()` from `@backstage/backend-common` instead.", - selector: 'Identifier[name="__dirname"]', - }, - ...globalRestrictedSyntax, - ], - }, - overrides: [ - { - files: ['**/*.ts?(x)'], - rules: { - '@typescript-eslint/no-unused-vars': 'off', - 'no-undef': 'off', - }, - }, - { - files: ['*.test.*', 'src/setupTests.*', 'dev/**'], - rules: { - 'no-restricted-syntax': ['error', ...globalRestrictedSyntax], - }, - }, - ], -}; +module.exports = require('./eslint-factory').createConfigForRole( + undefined, + 'node-library', +); diff --git a/packages/cli/config/eslint.js b/packages/cli/config/eslint.js index 5555dad451..9258454837 100644 --- a/packages/cli/config/eslint.js +++ b/packages/cli/config/eslint.js @@ -14,96 +14,7 @@ * limitations under the License. */ -module.exports = { - extends: [ - '@spotify/eslint-config-base', - '@spotify/eslint-config-react', - '@spotify/eslint-config-typescript', - 'prettier', - 'plugin:jest/recommended', - 'plugin:monorepo/recommended', - ], - parser: '@typescript-eslint/parser', - plugins: ['import'], - env: { - jest: true, - }, - parserOptions: { - ecmaVersion: 2018, - ecmaFeatures: { - jsx: true, - }, - sourceType: 'module', - lib: require('./tsconfig.json').compilerOptions.lib, - }, - settings: { - react: { - version: 'detect', - }, - }, - ignorePatterns: ['.eslintrc.js', '**/dist/**', '**/dist-types/**'], - rules: { - 'no-shadow': 'off', - 'no-redeclare': 'off', - '@typescript-eslint/no-shadow': 'error', - '@typescript-eslint/no-redeclare': 'error', - 'no-undef': 'off', - 'import/newline-after-import': 'error', - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: [ - '**/*.test.*', - '**/*.stories.*', - '**/src/setupTests.*', - '**/dev/**', - ], - optionalDependencies: true, - peerDependencies: true, - bundledDependencies: true, - }, - ], - 'no-unused-expressions': 'off', - '@typescript-eslint/no-unused-expressions': 'error', - '@typescript-eslint/consistent-type-assertions': 'error', - '@typescript-eslint/no-unused-vars': [ - 'warn', - { - vars: 'all', - args: 'after-used', - ignoreRestSiblings: true, - argsIgnorePattern: '^_', - }, - ], - 'no-restricted-imports': [ - 2, - { - paths: [ - { - // Importing the entire MUI icons packages kills build performance as the list of icons is huge. - name: '@material-ui/icons', - message: "Please import '@material-ui/icons/' instead.", - }, - { - name: '@material-ui/icons/', // because this is possible too ._. - message: "Please import '@material-ui/icons/' instead.", - }, - ...require('module').builtinModules, - ], - // Avoid cross-package imports - patterns: ['**/../../**/*/src/**', '**/../../**/*/src'], - }, - ], - }, - overrides: [ - { - files: ['**/*.ts?(x)'], - rules: { - // Default to not enforcing prop-types in typescript - 'react/prop-types': 0, - '@typescript-eslint/no-unused-vars': 'off', - 'no-undef': 'off', - }, - }, - ], -}; +module.exports = require('./eslint-factory').createConfigForRole( + undefined, + 'web-library', +); From ff745f5405b25048e83200892e68a5238f8473bb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 7 Mar 2022 14:29:09 +0100 Subject: [PATCH 263/353] cli: add migrate package-lint-configs command Signed-off-by: Patrik Oldsberg --- packages/cli/src/commands/index.ts | 9 ++ .../commands/migrate/packageLintConfigs.ts | 89 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 packages/cli/src/commands/migrate/packageLintConfigs.ts diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 5cb02c82b4..a79f9fe81b 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -165,6 +165,15 @@ export function registerMigrateCommand(program: CommanderStatic) { .action( lazy(() => import('./migrate/packageScripts').then(m => m.command)), ); + + command + .command('package-lint-configs') + .description( + 'Migrates all packages to use @backstage/cli/config/eslint-factory', + ) + .action( + lazy(() => import('./migrate/packageLintConfigs').then(m => m.command)), + ); } export function registerCommands(program: CommanderStatic) { diff --git a/packages/cli/src/commands/migrate/packageLintConfigs.ts b/packages/cli/src/commands/migrate/packageLintConfigs.ts new file mode 100644 index 0000000000..c0007ef59d --- /dev/null +++ b/packages/cli/src/commands/migrate/packageLintConfigs.ts @@ -0,0 +1,89 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; +import { PackageGraph } from '../../lib/monorepo'; +import { runPlain } from '../../lib/run'; + +const PREFIX = `module.exports = require('@backstage/cli/config/eslint-factory')`; + +export async function command() { + const packages = await PackageGraph.listTargetPackages(); + + const oldConfigs = [ + require.resolve('@backstage/cli/config/eslint.js'), + require.resolve('@backstage/cli/config/eslint.backend.js'), + ]; + + const configPaths = new Array(); + await Promise.all( + packages.map(async ({ dir, packageJson }) => { + const configPath = resolvePath(dir, '.eslintrc.js'); + if (!(await fs.pathExists(configPath))) { + console.log(`Skipping ${packageJson.name}, missing .eslintrc.js`); + return; + } + let existingConfig: Record; + try { + existingConfig = require(configPath); + } catch (error) { + console.log( + `Skipping ${packageJson.name}, failed to load .eslintrc.js, ${error}`, + ); + return; + } + + // Only transform configs that extend the old config files, and + // we remove that entry from the extends array. + const extendsArray = (existingConfig.extends as string[]) ?? []; + const extendIndex = extendsArray.findIndex(p => oldConfigs.includes(p)); + if (extendIndex === -1) { + console.log( + `Skipping ${packageJson.name}, .eslintrc.js does not extend the legacy config`, + ); + return; + } + extendsArray.splice(extendIndex, 1); + if (extendsArray.length === 0) { + delete existingConfig.extends; + } + + if (Object.keys(existingConfig).length > 0) { + await fs.writeFile( + configPath, + `${PREFIX}(__dirname, ${JSON.stringify(existingConfig, null, 2)});\n`, + ); + } else { + await fs.writeFile(configPath, `${PREFIX}(__dirname);\n`); + } + configPaths.push(configPath); + }), + ); + + // If prettier is present, then we run that too + let hasPrettier = false; + try { + require.resolve('prettier'); + hasPrettier = true; + } catch { + /* ignore */ + } + + if (hasPrettier) { + await runPlain('prettier', '--write', ...configPaths); + } +} From 09b37063f2b1d05fbc87a87553cbb064b299287b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 7 Mar 2022 14:37:14 +0100 Subject: [PATCH 264/353] packages,plugins: migrate to new lint config Signed-off-by: Patrik Oldsberg --- packages/app-defaults/.eslintrc.js | 4 +- packages/app/.eslintrc.js | 5 +- packages/backend-common/.eslintrc.js | 4 +- packages/backend-tasks/.eslintrc.js | 4 +- packages/backend-test-utils/.eslintrc.js | 4 +- packages/backend/.eslintrc.js | 4 +- packages/catalog-client/.eslintrc.js | 4 +- packages/catalog-model/.eslintrc.js | 4 +- packages/cli-common/.eslintrc.js | 4 +- packages/cli/.eslintrc.js | 5 +- packages/codemods/.eslintrc.js | 5 +- packages/config-loader/.eslintrc.js | 4 +- packages/config/.eslintrc.js | 5 +- packages/core-app-api/.eslintrc.js | 6 +- packages/core-components/.eslintrc.js | 77 ++++++++++++++++--- packages/core-plugin-api/.eslintrc.js | 6 +- packages/create-app/.eslintrc.js | 5 +- packages/dev-utils/.eslintrc.js | 4 +- packages/e2e-test/.eslintrc.js | 5 +- packages/errors/.eslintrc.js | 4 +- packages/integration-react/.eslintrc.js | 4 +- packages/integration/.eslintrc.js | 4 +- packages/release-manifests/.eslintrc.js | 4 +- packages/search-common/.eslintrc.js | 4 +- .../techdocs-cli-embedded-app/.eslintrc.js | 4 +- packages/techdocs-cli/.eslintrc.js | 5 +- packages/techdocs-common/.eslintrc.js | 4 +- packages/test-utils/.eslintrc.js | 4 +- packages/theme/.eslintrc.js | 4 +- packages/types/.eslintrc.js | 4 +- packages/version-bridge/.eslintrc.js | 4 +- plugins/airbrake-backend/.eslintrc.js | 4 +- plugins/airbrake/.eslintrc.js | 4 +- plugins/allure/.eslintrc.js | 4 +- plugins/analytics-module-ga/.eslintrc.js | 4 +- plugins/apache-airflow/.eslintrc.js | 4 +- plugins/api-docs/.eslintrc.js | 4 +- plugins/app-backend/.eslintrc.js | 4 +- plugins/auth-backend/.eslintrc.js | 4 +- plugins/auth-node/.eslintrc.js | 4 +- plugins/azure-devops-backend/.eslintrc.js | 4 +- plugins/azure-devops-common/.eslintrc.js | 4 +- plugins/azure-devops/.eslintrc.js | 4 +- plugins/badges-backend/.eslintrc.js | 4 +- plugins/badges/.eslintrc.js | 4 +- plugins/bazaar-backend/.eslintrc.js | 4 +- plugins/bazaar/.eslintrc.js | 4 +- plugins/bitrise/.eslintrc.js | 4 +- .../catalog-backend-module-aws/.eslintrc.js | 4 +- .../catalog-backend-module-ldap/.eslintrc.js | 4 +- .../.eslintrc.js | 4 +- plugins/catalog-backend/.eslintrc.js | 4 +- plugins/catalog-common/.eslintrc.js | 4 +- plugins/catalog-graph/.eslintrc.js | 4 +- plugins/catalog-graphql/.eslintrc.js | 4 +- plugins/catalog-import/.eslintrc.js | 4 +- plugins/catalog-react/.eslintrc.js | 4 +- plugins/catalog/.eslintrc.js | 4 +- plugins/cicd-statistics/.eslintrc.js | 4 +- plugins/circleci/.eslintrc.js | 4 +- plugins/cloudbuild/.eslintrc.js | 4 +- plugins/code-climate/.eslintrc.js | 4 +- plugins/code-coverage-backend/.eslintrc.js | 4 +- plugins/code-coverage/.eslintrc.js | 4 +- plugins/config-schema/.eslintrc.js | 4 +- plugins/cost-insights/.eslintrc.js | 4 +- plugins/explore-react/.eslintrc.js | 4 +- plugins/explore/.eslintrc.js | 4 +- plugins/firehydrant/.eslintrc.js | 4 +- plugins/fossa/.eslintrc.js | 4 +- plugins/gcp-projects/.eslintrc.js | 4 +- plugins/git-release-manager/.eslintrc.js | 4 +- plugins/github-actions/.eslintrc.js | 4 +- plugins/github-deployments/.eslintrc.js | 4 +- plugins/gitops-profiles/.eslintrc.js | 4 +- plugins/gocd/.eslintrc.js | 4 +- plugins/graphiql/.eslintrc.js | 6 +- plugins/graphql-backend/.eslintrc.js | 4 +- plugins/home/.eslintrc.js | 4 +- plugins/ilert/.eslintrc.js | 5 +- plugins/jenkins-backend/.eslintrc.js | 4 +- plugins/jenkins-common/.eslintrc.js | 4 +- plugins/jenkins/.eslintrc.js | 4 +- plugins/kafka-backend/.eslintrc.js | 4 +- plugins/kafka/.eslintrc.js | 4 +- plugins/kubernetes-backend/.eslintrc.js | 4 +- plugins/kubernetes-common/.eslintrc.js | 4 +- plugins/kubernetes/.eslintrc.js | 4 +- plugins/lighthouse/.eslintrc.js | 4 +- plugins/newrelic-dashboard/.eslintrc.js | 4 +- plugins/newrelic/.eslintrc.js | 4 +- plugins/org/.eslintrc.js | 4 +- plugins/pagerduty/.eslintrc.js | 4 +- plugins/permission-backend/.eslintrc.js | 4 +- plugins/permission-common/.eslintrc.js | 4 +- plugins/permission-node/.eslintrc.js | 4 +- plugins/permission-react/.eslintrc.js | 4 +- plugins/proxy-backend/.eslintrc.js | 4 +- plugins/rollbar-backend/.eslintrc.js | 4 +- plugins/rollbar/.eslintrc.js | 4 +- .../.eslintrc.js | 4 +- .../.eslintrc.js | 4 +- .../.eslintrc.js | 4 +- plugins/scaffolder-backend/.eslintrc.js | 42 +++++++--- plugins/scaffolder-common/.eslintrc.js | 4 +- plugins/scaffolder/.eslintrc.js | 4 +- .../.eslintrc.js | 4 +- plugins/search-backend-module-pg/.eslintrc.js | 4 +- plugins/search-backend-node/.eslintrc.js | 4 +- plugins/search-backend/.eslintrc.js | 4 +- plugins/search-common/.eslintrc.js | 4 +- plugins/search/.eslintrc.js | 4 +- plugins/sentry/.eslintrc.js | 4 +- plugins/shortcuts/.eslintrc.js | 4 +- plugins/sonarqube/.eslintrc.js | 4 +- plugins/splunk-on-call/.eslintrc.js | 4 +- .../.eslintrc.js | 4 +- plugins/tech-insights-backend/.eslintrc.js | 5 +- plugins/tech-insights-common/.eslintrc.js | 4 +- plugins/tech-insights-node/.eslintrc.js | 4 +- plugins/tech-insights/.eslintrc.js | 4 +- plugins/tech-radar/.eslintrc.js | 4 +- plugins/techdocs-backend/.eslintrc.js | 5 +- plugins/techdocs-node/.eslintrc.js | 4 +- plugins/techdocs/.eslintrc.js | 4 +- plugins/todo-backend/.eslintrc.js | 4 +- plugins/todo/.eslintrc.js | 4 +- plugins/user-settings/.eslintrc.js | 4 +- plugins/xcmetrics/.eslintrc.js | 4 +- 129 files changed, 237 insertions(+), 406 deletions(-) diff --git a/packages/app-defaults/.eslintrc.js b/packages/app-defaults/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/app-defaults/.eslintrc.js +++ b/packages/app-defaults/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/app/.eslintrc.js b/packages/app/.eslintrc.js index 930c98ee1b..7441f46811 100644 --- a/packages/app/.eslintrc.js +++ b/packages/app/.eslintrc.js @@ -1,5 +1,4 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { overrides: [ { files: ['**/*.ts?(x)'], @@ -8,4 +7,4 @@ module.exports = { }, }, ], -}; +}); diff --git a/packages/backend-common/.eslintrc.js b/packages/backend-common/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/backend-common/.eslintrc.js +++ b/packages/backend-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/backend-tasks/.eslintrc.js b/packages/backend-tasks/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/backend-tasks/.eslintrc.js +++ b/packages/backend-tasks/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/backend-test-utils/.eslintrc.js b/packages/backend-test-utils/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/backend-test-utils/.eslintrc.js +++ b/packages/backend-test-utils/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/backend/.eslintrc.js b/packages/backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/backend/.eslintrc.js +++ b/packages/backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/catalog-client/.eslintrc.js b/packages/catalog-client/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/catalog-client/.eslintrc.js +++ b/packages/catalog-client/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/catalog-model/.eslintrc.js b/packages/catalog-model/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/catalog-model/.eslintrc.js +++ b/packages/catalog-model/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli-common/.eslintrc.js b/packages/cli-common/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/cli-common/.eslintrc.js +++ b/packages/cli-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli/.eslintrc.js b/packages/cli/.eslintrc.js index 69bec6cd2a..e9ea2f5bfb 100644 --- a/packages/cli/.eslintrc.js +++ b/packages/cli/.eslintrc.js @@ -1,7 +1,6 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { ignorePatterns: ['templates/**'], rules: { 'no-console': 0, }, -}; +}); diff --git a/packages/codemods/.eslintrc.js b/packages/codemods/.eslintrc.js index 7c8a092081..f9fb2e27c9 100644 --- a/packages/codemods/.eslintrc.js +++ b/packages/codemods/.eslintrc.js @@ -1,5 +1,4 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { 'no-console': 0, 'import/no-extraneous-dependencies': [ @@ -12,4 +11,4 @@ module.exports = { }, ], }, -}; +}); diff --git a/packages/config-loader/.eslintrc.js b/packages/config-loader/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/config-loader/.eslintrc.js +++ b/packages/config-loader/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/config/.eslintrc.js b/packages/config/.eslintrc.js index 54e1fff915..e358722664 100644 --- a/packages/config/.eslintrc.js +++ b/packages/config/.eslintrc.js @@ -1,6 +1,5 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { 'jest/expect-expect': 0, }, -}; +}); diff --git a/packages/core-app-api/.eslintrc.js b/packages/core-app-api/.eslintrc.js index d592a653c8..3c32e018c8 100644 --- a/packages/core-app-api/.eslintrc.js +++ b/packages/core-app-api/.eslintrc.js @@ -1,8 +1,6 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { - // TODO: add prop types to JS and remove 'react/prop-types': 0, 'jest/expect-expect': 0, }, -}; +}); diff --git a/packages/core-components/.eslintrc.js b/packages/core-components/.eslintrc.js index 65a93d3660..65bfa69f59 100644 --- a/packages/core-components/.eslintrc.js +++ b/packages/core-components/.eslintrc.js @@ -1,25 +1,82 @@ -const base = require('@backstage/cli/config/eslint'); -const [, baseRestrictedImports] = base.rules['no-restricted-imports']; - -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { - // TODO: add prop types to JS and remove 'react/prop-types': 0, 'jest/expect-expect': 0, 'no-restricted-imports': [ 2, { - ...baseRestrictedImports, paths: [ { - // Importing the entire MUI icons packages kills build performance as the list of icons is huge. name: '@material-ui/core', message: "Please import '@material-ui/core/...' instead.", }, - ...baseRestrictedImports.paths, + { + name: '@material-ui/icons', + message: "Please import '@material-ui/icons/' instead.", + }, + { + name: '@material-ui/icons/', + message: "Please import '@material-ui/icons/' instead.", + }, + '_http_agent', + '_http_client', + '_http_common', + '_http_incoming', + '_http_outgoing', + '_http_server', + '_stream_duplex', + '_stream_passthrough', + '_stream_readable', + '_stream_transform', + '_stream_wrap', + '_stream_writable', + '_tls_common', + '_tls_wrap', + 'assert', + 'async_hooks', + 'buffer', + 'child_process', + 'cluster', + 'console', + 'constants', + 'crypto', + 'dgram', + 'diagnostics_channel', + 'dns', + 'domain', + 'events', + 'fs', + 'fs/promises', + 'http', + 'http2', + 'https', + 'inspector', + 'module', + 'net', + 'os', + 'path', + 'perf_hooks', + 'process', + 'punycode', + 'querystring', + 'readline', + 'repl', + 'stream', + 'string_decoder', + 'sys', + 'timers', + 'tls', + 'trace_events', + 'tty', + 'url', + 'util', + 'v8', + 'vm', + 'worker_threads', + 'zlib', ], + patterns: ['**/../../**/*/src/**', '**/../../**/*/src'], }, ], }, -}; +}); diff --git a/packages/core-plugin-api/.eslintrc.js b/packages/core-plugin-api/.eslintrc.js index d592a653c8..3c32e018c8 100644 --- a/packages/core-plugin-api/.eslintrc.js +++ b/packages/core-plugin-api/.eslintrc.js @@ -1,8 +1,6 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { - // TODO: add prop types to JS and remove 'react/prop-types': 0, 'jest/expect-expect': 0, }, -}; +}); diff --git a/packages/create-app/.eslintrc.js b/packages/create-app/.eslintrc.js index 69bec6cd2a..e9ea2f5bfb 100644 --- a/packages/create-app/.eslintrc.js +++ b/packages/create-app/.eslintrc.js @@ -1,7 +1,6 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { ignorePatterns: ['templates/**'], rules: { 'no-console': 0, }, -}; +}); diff --git a/packages/dev-utils/.eslintrc.js b/packages/dev-utils/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/dev-utils/.eslintrc.js +++ b/packages/dev-utils/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/e2e-test/.eslintrc.js b/packages/e2e-test/.eslintrc.js index 7c8a092081..f9fb2e27c9 100644 --- a/packages/e2e-test/.eslintrc.js +++ b/packages/e2e-test/.eslintrc.js @@ -1,5 +1,4 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { 'no-console': 0, 'import/no-extraneous-dependencies': [ @@ -12,4 +11,4 @@ module.exports = { }, ], }, -}; +}); diff --git a/packages/errors/.eslintrc.js b/packages/errors/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/errors/.eslintrc.js +++ b/packages/errors/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/integration-react/.eslintrc.js b/packages/integration-react/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/integration-react/.eslintrc.js +++ b/packages/integration-react/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/integration/.eslintrc.js b/packages/integration/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/integration/.eslintrc.js +++ b/packages/integration/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/release-manifests/.eslintrc.js b/packages/release-manifests/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/release-manifests/.eslintrc.js +++ b/packages/release-manifests/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/search-common/.eslintrc.js b/packages/search-common/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/search-common/.eslintrc.js +++ b/packages/search-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/techdocs-cli-embedded-app/.eslintrc.js b/packages/techdocs-cli-embedded-app/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/techdocs-cli-embedded-app/.eslintrc.js +++ b/packages/techdocs-cli-embedded-app/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/techdocs-cli/.eslintrc.js b/packages/techdocs-cli/.eslintrc.js index 884f559c0f..8b5adc6794 100644 --- a/packages/techdocs-cli/.eslintrc.js +++ b/packages/techdocs-cli/.eslintrc.js @@ -1,5 +1,4 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { overrides: [ { files: ['**/*.ts?(x)'], @@ -8,4 +7,4 @@ module.exports = { }, }, ], -}; +}); diff --git a/packages/techdocs-common/.eslintrc.js b/packages/techdocs-common/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/packages/techdocs-common/.eslintrc.js +++ b/packages/techdocs-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/test-utils/.eslintrc.js b/packages/test-utils/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/test-utils/.eslintrc.js +++ b/packages/test-utils/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/theme/.eslintrc.js b/packages/theme/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/theme/.eslintrc.js +++ b/packages/theme/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/types/.eslintrc.js b/packages/types/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/types/.eslintrc.js +++ b/packages/types/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/version-bridge/.eslintrc.js b/packages/version-bridge/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/packages/version-bridge/.eslintrc.js +++ b/packages/version-bridge/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/airbrake-backend/.eslintrc.js b/plugins/airbrake-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/airbrake-backend/.eslintrc.js +++ b/plugins/airbrake-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/airbrake/.eslintrc.js b/plugins/airbrake/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/airbrake/.eslintrc.js +++ b/plugins/airbrake/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/allure/.eslintrc.js b/plugins/allure/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/allure/.eslintrc.js +++ b/plugins/allure/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/analytics-module-ga/.eslintrc.js b/plugins/analytics-module-ga/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/analytics-module-ga/.eslintrc.js +++ b/plugins/analytics-module-ga/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/apache-airflow/.eslintrc.js b/plugins/apache-airflow/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/apache-airflow/.eslintrc.js +++ b/plugins/apache-airflow/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/api-docs/.eslintrc.js b/plugins/api-docs/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/api-docs/.eslintrc.js +++ b/plugins/api-docs/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/app-backend/.eslintrc.js b/plugins/app-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/app-backend/.eslintrc.js +++ b/plugins/app-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-backend/.eslintrc.js b/plugins/auth-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/auth-backend/.eslintrc.js +++ b/plugins/auth-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-node/.eslintrc.js b/plugins/auth-node/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/auth-node/.eslintrc.js +++ b/plugins/auth-node/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/azure-devops-backend/.eslintrc.js b/plugins/azure-devops-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/azure-devops-backend/.eslintrc.js +++ b/plugins/azure-devops-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/azure-devops-common/.eslintrc.js b/plugins/azure-devops-common/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/azure-devops-common/.eslintrc.js +++ b/plugins/azure-devops-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/azure-devops/.eslintrc.js b/plugins/azure-devops/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/azure-devops/.eslintrc.js +++ b/plugins/azure-devops/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/badges-backend/.eslintrc.js b/plugins/badges-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/badges-backend/.eslintrc.js +++ b/plugins/badges-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/badges/.eslintrc.js b/plugins/badges/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/badges/.eslintrc.js +++ b/plugins/badges/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/bazaar-backend/.eslintrc.js b/plugins/bazaar-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/bazaar-backend/.eslintrc.js +++ b/plugins/bazaar-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/bazaar/.eslintrc.js b/plugins/bazaar/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/bazaar/.eslintrc.js +++ b/plugins/bazaar/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/bitrise/.eslintrc.js b/plugins/bitrise/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/bitrise/.eslintrc.js +++ b/plugins/bitrise/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-backend-module-aws/.eslintrc.js b/plugins/catalog-backend-module-aws/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/catalog-backend-module-aws/.eslintrc.js +++ b/plugins/catalog-backend-module-aws/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-backend-module-ldap/.eslintrc.js b/plugins/catalog-backend-module-ldap/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/catalog-backend-module-ldap/.eslintrc.js +++ b/plugins/catalog-backend-module-ldap/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-backend-module-msgraph/.eslintrc.js b/plugins/catalog-backend-module-msgraph/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/catalog-backend-module-msgraph/.eslintrc.js +++ b/plugins/catalog-backend-module-msgraph/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-backend/.eslintrc.js b/plugins/catalog-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/catalog-backend/.eslintrc.js +++ b/plugins/catalog-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-common/.eslintrc.js b/plugins/catalog-common/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/catalog-common/.eslintrc.js +++ b/plugins/catalog-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-graph/.eslintrc.js b/plugins/catalog-graph/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/catalog-graph/.eslintrc.js +++ b/plugins/catalog-graph/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-graphql/.eslintrc.js b/plugins/catalog-graphql/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/catalog-graphql/.eslintrc.js +++ b/plugins/catalog-graphql/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-import/.eslintrc.js b/plugins/catalog-import/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/catalog-import/.eslintrc.js +++ b/plugins/catalog-import/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-react/.eslintrc.js b/plugins/catalog-react/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/catalog-react/.eslintrc.js +++ b/plugins/catalog-react/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog/.eslintrc.js b/plugins/catalog/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/catalog/.eslintrc.js +++ b/plugins/catalog/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/cicd-statistics/.eslintrc.js b/plugins/cicd-statistics/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/cicd-statistics/.eslintrc.js +++ b/plugins/cicd-statistics/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/circleci/.eslintrc.js b/plugins/circleci/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/circleci/.eslintrc.js +++ b/plugins/circleci/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/cloudbuild/.eslintrc.js b/plugins/cloudbuild/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/cloudbuild/.eslintrc.js +++ b/plugins/cloudbuild/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/code-climate/.eslintrc.js b/plugins/code-climate/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/code-climate/.eslintrc.js +++ b/plugins/code-climate/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/code-coverage-backend/.eslintrc.js b/plugins/code-coverage-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/code-coverage-backend/.eslintrc.js +++ b/plugins/code-coverage-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/code-coverage/.eslintrc.js b/plugins/code-coverage/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/code-coverage/.eslintrc.js +++ b/plugins/code-coverage/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/config-schema/.eslintrc.js b/plugins/config-schema/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/config-schema/.eslintrc.js +++ b/plugins/config-schema/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/cost-insights/.eslintrc.js b/plugins/cost-insights/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/cost-insights/.eslintrc.js +++ b/plugins/cost-insights/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/explore-react/.eslintrc.js b/plugins/explore-react/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/explore-react/.eslintrc.js +++ b/plugins/explore-react/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/explore/.eslintrc.js b/plugins/explore/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/explore/.eslintrc.js +++ b/plugins/explore/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/firehydrant/.eslintrc.js b/plugins/firehydrant/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/firehydrant/.eslintrc.js +++ b/plugins/firehydrant/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/fossa/.eslintrc.js b/plugins/fossa/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/fossa/.eslintrc.js +++ b/plugins/fossa/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/gcp-projects/.eslintrc.js b/plugins/gcp-projects/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/gcp-projects/.eslintrc.js +++ b/plugins/gcp-projects/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/git-release-manager/.eslintrc.js b/plugins/git-release-manager/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/git-release-manager/.eslintrc.js +++ b/plugins/git-release-manager/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/github-actions/.eslintrc.js b/plugins/github-actions/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/github-actions/.eslintrc.js +++ b/plugins/github-actions/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/github-deployments/.eslintrc.js b/plugins/github-deployments/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/github-deployments/.eslintrc.js +++ b/plugins/github-deployments/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/gitops-profiles/.eslintrc.js b/plugins/gitops-profiles/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/gitops-profiles/.eslintrc.js +++ b/plugins/gitops-profiles/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/gocd/.eslintrc.js b/plugins/gocd/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/gocd/.eslintrc.js +++ b/plugins/gocd/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/graphiql/.eslintrc.js b/plugins/graphiql/.eslintrc.js index 6a513fbafe..e358722664 100644 --- a/plugins/graphiql/.eslintrc.js +++ b/plugins/graphiql/.eslintrc.js @@ -1,7 +1,5 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { - // Prefer to use rendered.getBy*, which will throw an error 'jest/expect-expect': 0, }, -}; +}); diff --git a/plugins/graphql-backend/.eslintrc.js b/plugins/graphql-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/graphql-backend/.eslintrc.js +++ b/plugins/graphql-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/home/.eslintrc.js b/plugins/home/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/home/.eslintrc.js +++ b/plugins/home/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/ilert/.eslintrc.js b/plugins/ilert/.eslintrc.js index 4d51616e98..b4ec729b09 100644 --- a/plugins/ilert/.eslintrc.js +++ b/plugins/ilert/.eslintrc.js @@ -1,6 +1,5 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { quotes: ['error', 'single'], }, -}; +}); diff --git a/plugins/jenkins-backend/.eslintrc.js b/plugins/jenkins-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/jenkins-backend/.eslintrc.js +++ b/plugins/jenkins-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/jenkins-common/.eslintrc.js b/plugins/jenkins-common/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/jenkins-common/.eslintrc.js +++ b/plugins/jenkins-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/jenkins/.eslintrc.js b/plugins/jenkins/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/jenkins/.eslintrc.js +++ b/plugins/jenkins/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/kafka-backend/.eslintrc.js b/plugins/kafka-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/kafka-backend/.eslintrc.js +++ b/plugins/kafka-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/kafka/.eslintrc.js b/plugins/kafka/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/kafka/.eslintrc.js +++ b/plugins/kafka/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/kubernetes-backend/.eslintrc.js b/plugins/kubernetes-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/kubernetes-backend/.eslintrc.js +++ b/plugins/kubernetes-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/kubernetes-common/.eslintrc.js b/plugins/kubernetes-common/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/kubernetes-common/.eslintrc.js +++ b/plugins/kubernetes-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/kubernetes/.eslintrc.js b/plugins/kubernetes/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/kubernetes/.eslintrc.js +++ b/plugins/kubernetes/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/lighthouse/.eslintrc.js b/plugins/lighthouse/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/lighthouse/.eslintrc.js +++ b/plugins/lighthouse/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/newrelic-dashboard/.eslintrc.js b/plugins/newrelic-dashboard/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/newrelic-dashboard/.eslintrc.js +++ b/plugins/newrelic-dashboard/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/newrelic/.eslintrc.js b/plugins/newrelic/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/newrelic/.eslintrc.js +++ b/plugins/newrelic/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/org/.eslintrc.js b/plugins/org/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/org/.eslintrc.js +++ b/plugins/org/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/pagerduty/.eslintrc.js b/plugins/pagerduty/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/pagerduty/.eslintrc.js +++ b/plugins/pagerduty/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/permission-backend/.eslintrc.js b/plugins/permission-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/permission-backend/.eslintrc.js +++ b/plugins/permission-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/permission-common/.eslintrc.js b/plugins/permission-common/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/permission-common/.eslintrc.js +++ b/plugins/permission-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/permission-node/.eslintrc.js b/plugins/permission-node/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/permission-node/.eslintrc.js +++ b/plugins/permission-node/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/permission-react/.eslintrc.js b/plugins/permission-react/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/permission-react/.eslintrc.js +++ b/plugins/permission-react/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/proxy-backend/.eslintrc.js b/plugins/proxy-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/proxy-backend/.eslintrc.js +++ b/plugins/proxy-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/rollbar-backend/.eslintrc.js b/plugins/rollbar-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/rollbar-backend/.eslintrc.js +++ b/plugins/rollbar-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/rollbar/.eslintrc.js b/plugins/rollbar/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/rollbar/.eslintrc.js +++ b/plugins/rollbar/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/scaffolder-backend-module-cookiecutter/.eslintrc.js b/plugins/scaffolder-backend-module-cookiecutter/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/.eslintrc.js +++ b/plugins/scaffolder-backend-module-cookiecutter/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/scaffolder-backend-module-rails/.eslintrc.js b/plugins/scaffolder-backend-module-rails/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/scaffolder-backend-module-rails/.eslintrc.js +++ b/plugins/scaffolder-backend-module-rails/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/scaffolder-backend-module-yeoman/.eslintrc.js b/plugins/scaffolder-backend-module-yeoman/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/scaffolder-backend-module-yeoman/.eslintrc.js +++ b/plugins/scaffolder-backend-module-yeoman/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/scaffolder-backend/.eslintrc.js b/plugins/scaffolder-backend/.eslintrc.js index 2632b5e089..d32253d562 100644 --- a/plugins/scaffolder-backend/.eslintrc.js +++ b/plugins/scaffolder-backend/.eslintrc.js @@ -1,16 +1,16 @@ -const parent = require('@backstage/cli/config/eslint.backend'); - -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { ignorePatterns: ['sample-templates/'], rules: { - 'no-console': 0, // Permitted in console programs - 'new-cap': ['error', { capIsNew: false }], // Because Express constructs things e.g. like 'const r = express.Router()' - // Usage of path.resolve is extra sensitive in the scaffolder, so forbid it in non-test code + 'no-console': 0, + 'new-cap': [ + 'error', + { + capIsNew: false, + }, + ], 'no-restricted-imports': [ 'error', { - ...parent.rules['no-restricted-imports'][1], paths: [ { name: 'path', @@ -19,23 +19,41 @@ module.exports = { 'Do not use path.resolve, use `resolveSafeChildPath` from `@backstage/backend-common` instead as it prevents security issues', }, ], + patterns: ['**/../../**/*/src/**', '**/../../**/*/src'], }, ], - 'no-restricted-syntax': parent.rules['no-restricted-syntax'].concat([ + 'no-restricted-syntax': [ + 'error', + { + message: + 'Default import from winston is not allowed, import `* as winston` instead.', + selector: + 'ImportDeclaration[source.value="winston"] ImportDefaultSpecifier', + }, + { + message: + "`__dirname` doesn't refer to the same dir in production builds, try `resolvePackagePath()` from `@backstage/backend-common` instead.", + selector: 'Identifier[name="__dirname"]', + }, { message: 'Do not use path.resolve, use `resolveSafeChildPath` from `@backstage/backend-common` instead as it prevents security issues', selector: 'MemberExpression[object.name="path"][property.name="resolve"]', }, - ]), + ], }, overrides: [ { files: ['*.test.*', 'src/setupTests.*', 'dev/**'], rules: { - 'no-restricted-imports': parent.rules['no-restricted-imports'], + 'no-restricted-imports': [ + 2, + { + patterns: ['**/../../**/*/src/**', '**/../../**/*/src'], + }, + ], }, }, ], -}; +}); diff --git a/plugins/scaffolder-common/.eslintrc.js b/plugins/scaffolder-common/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/scaffolder-common/.eslintrc.js +++ b/plugins/scaffolder-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/scaffolder/.eslintrc.js b/plugins/scaffolder/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/scaffolder/.eslintrc.js +++ b/plugins/scaffolder/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/search-backend-module-elasticsearch/.eslintrc.js b/plugins/search-backend-module-elasticsearch/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/search-backend-module-elasticsearch/.eslintrc.js +++ b/plugins/search-backend-module-elasticsearch/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/search-backend-module-pg/.eslintrc.js b/plugins/search-backend-module-pg/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/search-backend-module-pg/.eslintrc.js +++ b/plugins/search-backend-module-pg/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/search-backend-node/.eslintrc.js b/plugins/search-backend-node/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/search-backend-node/.eslintrc.js +++ b/plugins/search-backend-node/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/search-backend/.eslintrc.js b/plugins/search-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/search-backend/.eslintrc.js +++ b/plugins/search-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/search-common/.eslintrc.js b/plugins/search-common/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/search-common/.eslintrc.js +++ b/plugins/search-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/search/.eslintrc.js b/plugins/search/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/search/.eslintrc.js +++ b/plugins/search/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/sentry/.eslintrc.js b/plugins/sentry/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/sentry/.eslintrc.js +++ b/plugins/sentry/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/shortcuts/.eslintrc.js b/plugins/shortcuts/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/shortcuts/.eslintrc.js +++ b/plugins/shortcuts/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/sonarqube/.eslintrc.js b/plugins/sonarqube/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/sonarqube/.eslintrc.js +++ b/plugins/sonarqube/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/splunk-on-call/.eslintrc.js b/plugins/splunk-on-call/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/splunk-on-call/.eslintrc.js +++ b/plugins/splunk-on-call/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/tech-insights-backend-module-jsonfc/.eslintrc.js b/plugins/tech-insights-backend-module-jsonfc/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/tech-insights-backend-module-jsonfc/.eslintrc.js +++ b/plugins/tech-insights-backend-module-jsonfc/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/tech-insights-backend/.eslintrc.js b/plugins/tech-insights-backend/.eslintrc.js index a126c438a4..f98046cfe0 100644 --- a/plugins/tech-insights-backend/.eslintrc.js +++ b/plugins/tech-insights-backend/.eslintrc.js @@ -1,5 +1,4 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { 'jest/expect-expect': [ 'error', @@ -8,4 +7,4 @@ module.exports = { }, ], }, -}; +}); diff --git a/plugins/tech-insights-common/.eslintrc.js b/plugins/tech-insights-common/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/tech-insights-common/.eslintrc.js +++ b/plugins/tech-insights-common/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/tech-insights-node/.eslintrc.js b/plugins/tech-insights-node/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/tech-insights-node/.eslintrc.js +++ b/plugins/tech-insights-node/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/tech-insights/.eslintrc.js b/plugins/tech-insights/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/tech-insights/.eslintrc.js +++ b/plugins/tech-insights/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/tech-radar/.eslintrc.js b/plugins/tech-radar/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/tech-radar/.eslintrc.js +++ b/plugins/tech-radar/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/techdocs-backend/.eslintrc.js b/plugins/techdocs-backend/.eslintrc.js index 595853b48b..d1e1c522da 100644 --- a/plugins/techdocs-backend/.eslintrc.js +++ b/plugins/techdocs-backend/.eslintrc.js @@ -1,4 +1,3 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { ignorePatterns: ['static/**'], -}; +}); diff --git a/plugins/techdocs-node/.eslintrc.js b/plugins/techdocs-node/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/techdocs-node/.eslintrc.js +++ b/plugins/techdocs-node/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/techdocs/.eslintrc.js b/plugins/techdocs/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/techdocs/.eslintrc.js +++ b/plugins/techdocs/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/todo-backend/.eslintrc.js b/plugins/todo-backend/.eslintrc.js index 16a033dbc6..e2a53a6ad2 100644 --- a/plugins/todo-backend/.eslintrc.js +++ b/plugins/todo-backend/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint.backend')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/todo/.eslintrc.js b/plugins/todo/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/todo/.eslintrc.js +++ b/plugins/todo/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/user-settings/.eslintrc.js b/plugins/user-settings/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/user-settings/.eslintrc.js +++ b/plugins/user-settings/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/xcmetrics/.eslintrc.js b/plugins/xcmetrics/.eslintrc.js index 13573efa9c..e2a53a6ad2 100644 --- a/plugins/xcmetrics/.eslintrc.js +++ b/plugins/xcmetrics/.eslintrc.js @@ -1,3 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); From 251688a75e673c7006ded323d4f6464ed8166d8a Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Mon, 7 Mar 2022 08:01:06 -0600 Subject: [PATCH 265/353] Updated CatalogKindHeader to respect query param changes Signed-off-by: Andre Wanlin --- .changeset/great-hounds-allow.md | 5 ++++ .../CatalogKindHeader.test.tsx | 30 +++++++++++++++++++ .../CatalogKindHeader/CatalogKindHeader.tsx | 18 ++++++++++- 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 .changeset/great-hounds-allow.md diff --git a/.changeset/great-hounds-allow.md b/.changeset/great-hounds-allow.md new file mode 100644 index 0000000000..54405fcf94 --- /dev/null +++ b/.changeset/great-hounds-allow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Updated `CatalogKindHeader` to respond to external changes to query parameters in the URL, such as two sidebar links that apply different catalog filters. diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx index 45d42d0ea6..7ce14817d6 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx @@ -135,4 +135,34 @@ describe('', () => { kind: new EntityKindFilter('template'), }); }); + + it('responds to external queryParameters changes', async () => { + const updateFilters = jest.fn(); + const rendered = await renderWithEffects( + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + kind: new EntityKindFilter('Components'), + }); + rendered.rerender( + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + kind: new EntityKindFilter('Template'), + }); + }); }); diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx index ed54f1fa84..5124f95086 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { capitalize, createStyles, @@ -61,6 +61,14 @@ export function CatalogKindHeader(props: CatalogKindHeaderProps) { }); const { updateFilters, queryParameters } = useEntityList(); + const queryParamKind = useMemo( + () => + ([queryParameters.kind].flat()[0] ?? initialFilter).toLocaleLowerCase( + 'en-US', + ), + [initialFilter, queryParameters], + ); + const [selectedKind, setSelectedKind] = useState( ([queryParameters.kind].flat()[0] ?? initialFilter).toLocaleLowerCase( 'en-US', @@ -73,6 +81,14 @@ export function CatalogKindHeader(props: CatalogKindHeaderProps) { }); }, [selectedKind, updateFilters]); + // Set selected Kind on query parameter updates; this happens at initial page load and from + // external updates to the page location. + useEffect(() => { + if (queryParamKind) { + setSelectedKind(queryParamKind); + } + }, [queryParamKind]); + // Before allKinds is loaded, or when a kind is entered manually in the URL, selectedKind may not // be present in allKinds. It should still be shown in the dropdown, but may not have the nice // enforced casing from the catalog-backend. This makes a key/value record for the Select options, From 7ef026339df61736ae346239ab947b37b7a69723 Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Fri, 25 Feb 2022 11:46:26 +0100 Subject: [PATCH 266/353] Add Periskop plugin Signed-off-by: Julio Zynger --- .changeset/strong-pandas-roll.md | 6 + .github/styles/vocab.txt | 2 + app-config.yaml | 7 + .../well-known-annotations.md | 14 ++ microsite/data/plugins/periskop.yaml | 9 + plugins/periskop-backend/.eslintrc.js | 3 + plugins/periskop-backend/README.md | 5 + plugins/periskop-backend/api-report.md | 22 ++ plugins/periskop-backend/package.json | 43 ++++ plugins/periskop-backend/src/api/index.ts | 72 +++++++ plugins/periskop-backend/src/index.ts | 17 ++ plugins/periskop-backend/src/run.ts | 33 +++ .../src/service/router.test.ts | 56 ++++++ .../periskop-backend/src/service/router.ts | 57 ++++++ .../src/service/standaloneServer.ts | 56 ++++++ plugins/periskop-backend/src/setupTests.ts | 17 ++ plugins/periskop-backend/src/types.ts | 48 +++++ plugins/periskop/.eslintrc.js | 3 + plugins/periskop/README.md | 34 ++++ plugins/periskop/api-report.md | 59 ++++++ plugins/periskop/config.d.ts | 39 ++++ plugins/periskop/dev/index.tsx | 20 ++ .../docs/periskop-plugin-screenshot.png | Bin 0 -> 205851 bytes plugins/periskop/package.json | 58 ++++++ plugins/periskop/src/api/index.ts | 89 ++++++++ .../PeriskopErrorsTable.tsx | 190 ++++++++++++++++++ .../components/PeriskopErrorsTable/index.ts | 21 ++ plugins/periskop/src/extensions.ts | 33 +++ plugins/periskop/src/index.ts | 23 +++ plugins/periskop/src/plugin.test.ts | 23 +++ plugins/periskop/src/plugin.ts | 51 +++++ plugins/periskop/src/routes.ts | 21 ++ plugins/periskop/src/setupTests.ts | 18 ++ plugins/periskop/src/types.ts | 48 +++++ 34 files changed, 1197 insertions(+) create mode 100644 .changeset/strong-pandas-roll.md create mode 100644 microsite/data/plugins/periskop.yaml create mode 100644 plugins/periskop-backend/.eslintrc.js create mode 100644 plugins/periskop-backend/README.md create mode 100644 plugins/periskop-backend/api-report.md create mode 100644 plugins/periskop-backend/package.json create mode 100644 plugins/periskop-backend/src/api/index.ts create mode 100644 plugins/periskop-backend/src/index.ts create mode 100644 plugins/periskop-backend/src/run.ts create mode 100644 plugins/periskop-backend/src/service/router.test.ts create mode 100644 plugins/periskop-backend/src/service/router.ts create mode 100644 plugins/periskop-backend/src/service/standaloneServer.ts create mode 100644 plugins/periskop-backend/src/setupTests.ts create mode 100644 plugins/periskop-backend/src/types.ts create mode 100644 plugins/periskop/.eslintrc.js create mode 100644 plugins/periskop/README.md create mode 100644 plugins/periskop/api-report.md create mode 100644 plugins/periskop/config.d.ts create mode 100644 plugins/periskop/dev/index.tsx create mode 100644 plugins/periskop/docs/periskop-plugin-screenshot.png create mode 100644 plugins/periskop/package.json create mode 100644 plugins/periskop/src/api/index.ts create mode 100644 plugins/periskop/src/components/PeriskopErrorsTable/PeriskopErrorsTable.tsx create mode 100644 plugins/periskop/src/components/PeriskopErrorsTable/index.ts create mode 100644 plugins/periskop/src/extensions.ts create mode 100644 plugins/periskop/src/index.ts create mode 100644 plugins/periskop/src/plugin.test.ts create mode 100644 plugins/periskop/src/plugin.ts create mode 100644 plugins/periskop/src/routes.ts create mode 100644 plugins/periskop/src/setupTests.ts create mode 100644 plugins/periskop/src/types.ts diff --git a/.changeset/strong-pandas-roll.md b/.changeset/strong-pandas-roll.md new file mode 100644 index 0000000000..6271ed6f1c --- /dev/null +++ b/.changeset/strong-pandas-roll.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-periskop': patch +'@backstage/plugin-periskop-backend': patch +--- + +Add periskop and periskop-backend plugin, for usage with exception aggregation tool https://periskop.io/ diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index aa94c3d953..2614dd6b50 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -211,6 +211,8 @@ Patrik Peloton performant Performant +periskop +Periskop plantuml Platformize Podman diff --git a/app-config.yaml b/app-config.yaml index ab68d4398f..c88415da2e 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -456,3 +456,10 @@ apacheAirflow: gocd: baseUrl: https://your.gocd.instance.com + +periskop: + locations: + - name: + host: + - name: + host: diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index 26a8710820..2f41377a36 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -222,6 +222,20 @@ definition. Specifying this annotation will enable GoCD related features in Backstage for that entity. +### periskop.io/name + +```yaml +# Example: +metadata: + annotations: + periskop.io/name: pump-station +``` + +The value of this annotation is the periskop project name for the given entity. + +Specifying this annotation will enable [Periskop](https://periskop.io/) related features in Backstage for +that entity if the periskop plugin is installed. + ### sentry.io/project-slug ```yaml diff --git a/microsite/data/plugins/periskop.yaml b/microsite/data/plugins/periskop.yaml new file mode 100644 index 0000000000..cce8d0dde5 --- /dev/null +++ b/microsite/data/plugins/periskop.yaml @@ -0,0 +1,9 @@ +--- +title: Periskop +author: Periskop +authorUrl: https://periskop.io/ +category: Monitoring +description: Periskop is a pull-based, language agnostic exception aggregator for microservice environments. +documentation: https://github.com/backstage/backstage/tree/master/plugins/periskop +iconUrl: https://raw.githubusercontent.com/periskop-dev/periskop/master/docs/assets/periskop-logo.png +npmPackageName: '@backstage/plugin-periskop' diff --git a/plugins/periskop-backend/.eslintrc.js b/plugins/periskop-backend/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/periskop-backend/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/periskop-backend/README.md b/plugins/periskop-backend/README.md new file mode 100644 index 0000000000..02552fe5b4 --- /dev/null +++ b/plugins/periskop-backend/README.md @@ -0,0 +1,5 @@ +# periskop-backend + +Simple plugin that proxies requests to the Periskop server APIs. + +To be used with the [Periskop plugin](../periskop/README.md). diff --git a/plugins/periskop-backend/api-report.md b/plugins/periskop-backend/api-report.md new file mode 100644 index 0000000000..7e737a21a8 --- /dev/null +++ b/plugins/periskop-backend/api-report.md @@ -0,0 +1,22 @@ +## API Report File for "@backstage/plugin-periskop-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Config } from '@backstage/config'; +import express from 'express'; +import { Logger as Logger_2 } from 'winston'; + +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise; + +// @public (undocumented) +export interface RouterOptions { + // (undocumented) + config: Config; + // (undocumented) + logger: Logger_2; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/periskop-backend/package.json b/plugins/periskop-backend/package.json new file mode 100644 index 0000000000..33da28be06 --- /dev/null +++ b/plugins/periskop-backend/package.json @@ -0,0 +1,43 @@ +{ + "name": "@backstage/plugin-periskop-backend", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "homepage": "https://periskop.io", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "^0.10.8", + "@backstage/config": "^0.1.14", + "@types/express": "*", + "cross-fetch": "^3.0.6", + "express": "^4.17.1", + "express-promise-router": "^4.1.0", + "node-fetch": "^2.6.0", + "winston": "^3.2.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.14.0", + "@types/supertest": "^2.0.8", + "msw": "^0.35.0", + "supertest": "^4.0.2" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/periskop-backend/src/api/index.ts b/plugins/periskop-backend/src/api/index.ts new file mode 100644 index 0000000000..f2b15f58fb --- /dev/null +++ b/plugins/periskop-backend/src/api/index.ts @@ -0,0 +1,72 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import { AggregatedError, NotFoundInLocation } from '../types'; +import fetch from 'node-fetch'; + +export type Options = { + config: Config; +}; + +type PeriskopLocation = { + name: string; + host: string; +}; + +export class PeriskopApi { + private readonly locations: PeriskopLocation[]; + + constructor(options: Options) { + this.locations = options.config + .getConfigArray('periskop.locations') + .flatMap(locConf => { + const name = locConf.getString('name'); + const host = locConf.getString('host'); + return { name: name, host: host }; + }); + } + + private getApiUrl(locationName: string): string | undefined { + return this.locations.find(loc => loc.name === locationName)?.host; + } + + async getErrors( + locationName: string, + serviceName: string, + ): Promise { + const apiUrl = this.getApiUrl(locationName); + if (!apiUrl) { + throw new Error( + `failed to fetch data, no periskop location with name ${locationName}`, + ); + } + const response = await fetch(`${apiUrl}/services/${serviceName}/errors/`); + + if (!response.ok) { + if (response.status === 404) { + return { + body: await response.text(), + }; + } + + throw new Error( + `failed to fetch data, status ${response.status}: ${response.statusText}`, + ); + } + return response.json(); + } +} diff --git a/plugins/periskop-backend/src/index.ts b/plugins/periskop-backend/src/index.ts new file mode 100644 index 0000000000..ca73cb27ba --- /dev/null +++ b/plugins/periskop-backend/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 './service/router'; diff --git a/plugins/periskop-backend/src/run.ts b/plugins/periskop-backend/src/run.ts new file mode 100644 index 0000000000..0a3ed2b7f0 --- /dev/null +++ b/plugins/periskop-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { getRootLogger } from '@backstage/backend-common'; +import yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/periskop-backend/src/service/router.test.ts b/plugins/periskop-backend/src/service/router.test.ts new file mode 100644 index 0000000000..efc326280a --- /dev/null +++ b/plugins/periskop-backend/src/service/router.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import express from 'express'; +import request from 'supertest'; + +import { createRouter } from './router'; + +describe('createRouter', () => { + let app: express.Express; + + beforeAll(async () => { + const router = await createRouter({ + logger: getVoidLogger(), + config: new ConfigReader({ + periskop: { + locations: [ + { + name: 'db', + host: 'http://periskop-db', + }, + ], + }, + }), + }); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /health', () => { + it('returns ok', async () => { + const response = await request(app).get('/health'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + }); +}); diff --git a/plugins/periskop-backend/src/service/router.ts b/plugins/periskop-backend/src/service/router.ts new file mode 100644 index 0000000000..4f8ae12c31 --- /dev/null +++ b/plugins/periskop-backend/src/service/router.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { errorHandler } from '@backstage/backend-common'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; +import { PeriskopApi } from '../api/index'; +import { Config } from '@backstage/config'; + +/** + * @public + */ +export interface RouterOptions { + logger: Logger; + config: Config; +} + +/** + * @public + */ +export async function createRouter( + options: RouterOptions, +): Promise { + const { logger, config } = options; + const periskopApi = new PeriskopApi({ config }); + + const router = Router(); + router.use(express.json()); + + router.get('/health', (_, response) => { + logger.info('PONG!'); + response.send({ status: 'ok' }); + }); + + router.get('/:locationName/:serviceName', async (request, response) => { + const { locationName, serviceName } = request.params; + logger.info(`Periskop got queried for ${serviceName} in ${locationName}`); + const errors = await periskopApi.getErrors(locationName, serviceName); + response.status(200).json(errors); + }); + router.use(errorHandler()); + return router; +} diff --git a/plugins/periskop-backend/src/service/standaloneServer.ts b/plugins/periskop-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..635b139ccf --- /dev/null +++ b/plugins/periskop-backend/src/service/standaloneServer.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { + createServiceBuilder, + loadBackendConfig, +} from '@backstage/backend-common'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { createRouter } from './router'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ service: 'periskop-backend' }); + const config = await loadBackendConfig({ logger, argv: process.argv }); + + logger.debug('Starting application server...'); + const router = await createRouter({ + logger, + config, + }); + + let service = createServiceBuilder(module) + .setPort(options.port) + .addRouter('/periskop', router); + if (options.enableCors) { + service = service.enableCors({ origin: 'http://localhost:3000' }); + } + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/plugins/periskop-backend/src/setupTests.ts b/plugins/periskop-backend/src/setupTests.ts new file mode 100644 index 0000000000..813cdeaae3 --- /dev/null +++ b/plugins/periskop-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 {}; diff --git a/plugins/periskop-backend/src/types.ts b/plugins/periskop-backend/src/types.ts new file mode 100644 index 0000000000..e6ee09f97c --- /dev/null +++ b/plugins/periskop-backend/src/types.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 AggregatedError { + aggregation_key: string; + total_count: number; + severity: string; + latest_errors: ErrorInstance[]; +} +export interface ErrorInstance { + error: Error; + uuid: string; + timestamp: number; + severity: string; + http_context: HttpContext; +} +export interface Error { + class: string; + message: string; + stacktrace?: string[]; + cause?: Error | null; +} +export interface HttpContext { + request_method: string; + request_url: string; + request_headers: RequestHeaders; + request_body: string; +} +export interface RequestHeaders { + [k: string]: string; +} + +export interface NotFoundInLocation { + body: string; +} diff --git a/plugins/periskop/.eslintrc.js b/plugins/periskop/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/periskop/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/periskop/README.md b/plugins/periskop/README.md new file mode 100644 index 0000000000..e937ee17ad --- /dev/null +++ b/plugins/periskop/README.md @@ -0,0 +1,34 @@ +# periskop + +[Periskop](https://periskop.io/) is a pull-based, language agnostic exception aggregator for microservice environments. + +![periskop-logo](https://i.imgur.com/z8BLePO.png) + +### `PeriskopErrorsTable` + +The Periskop Backstage Plugin exposes an entity tab component named `PeriskopErrorsTable`. Each of the entries in the table will direct you to the error details in your deployed Periskop instance location. + +![periskop-errors-card](./docs/periskop-plugin-screenshot.png) + +Now your plugin should be visible as a tab at the top of the entity pages. +However, it warns of a missing `periskop.io/name` annotation. + +Add the annotation to your component descriptor file as shown in the highlighted example below: + +```yaml +annotations: + periskop.io/name: '' +``` + +### Locations + +The periskop plugin can be configured to fetch aggregated errors from multiple deployment locations. This is especially useful if you have a multi-zone deployment, or a federated setup and would like to drill deeper into a single instance of the federation. Each of the configured locations will be included in the plugin's UI via a dropdown on the errors table. + +The plugin requires to configure _at least one_ Periskop API location in the [app-config.yaml](https://github.com/backstage/backstage/blob/master/app-config.yaml): + +```yaml +periskop: + locations: + - name: + host: +``` diff --git a/plugins/periskop/api-report.md b/plugins/periskop/api-report.md new file mode 100644 index 0000000000..8222f7a1c5 --- /dev/null +++ b/plugins/periskop/api-report.md @@ -0,0 +1,59 @@ +## API Report File for "@backstage/plugin-periskop" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { ApiRef } from '@backstage/core-plugin-api'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { ConfigApi } from '@backstage/core-plugin-api'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// @public +export const isPeriskopAvailable: (entity: Entity) => boolean; + +// @public +export const PERISKOP_NAME_ANNOTATION = 'periskop.io/name'; + +// @public +export class PeriskopApi { + // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts + constructor(options: Options); + // Warning: (ae-forgotten-export) The symbol "AggregatedError" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getErrorInstanceUrl( + locationName: string, + serviceName: string, + error: AggregatedError, + ): string; + // Warning: (ae-forgotten-export) The symbol "NotFoundInLocation" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getErrors( + locationName: string, + serviceName: string, + ): Promise; + // (undocumented) + getLocationNames(): string[]; +} + +// @public (undocumented) +export const periskopApiRef: ApiRef; + +// @public (undocumented) +export const PeriskopErrorsTable: () => JSX.Element; + +// @public (undocumented) +export const periskopPlugin: BackstagePlugin< + { + root: RouteRef; + }, + {} +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/periskop/config.d.ts b/plugins/periskop/config.d.ts new file mode 100644 index 0000000000..7aa259a6d0 --- /dev/null +++ b/plugins/periskop/config.d.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 Config { + /** + * Configuration options for the periskop plugin + */ + periskop: { + /** + * Integration configuration for the periskop servers + * @visibility frontend + */ + locations: Array<{ + /** + * The name of the given Periskop instance + * @visibility frontend + */ + name: string; + /** + * The hostname of the given Periskop instance + * @visibility frontend + */ + host: string; + }>; + }; +} diff --git a/plugins/periskop/dev/index.tsx b/plugins/periskop/dev/index.tsx new file mode 100644 index 0000000000..b752f7f8b0 --- /dev/null +++ b/plugins/periskop/dev/index.tsx @@ -0,0 +1,20 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { periskopPlugin } from '../src/plugin'; + +createDevApp().registerPlugin(periskopPlugin).render(); diff --git a/plugins/periskop/docs/periskop-plugin-screenshot.png b/plugins/periskop/docs/periskop-plugin-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..9971f41992b313557f211e7fa020fced43da13e6 GIT binary patch literal 205851 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

$Fj8Jki8zowF9BJPf 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

>|}? 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

;-Iv)&Sh&4RpwqvmG%Wd=;K)<_a;=% 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 TU%=mn0KTNe<;6-xKKcAVBeKRU literal 0 HcmV?d00001 diff --git a/plugins/periskop/package.json b/plugins/periskop/package.json new file mode 100644 index 0000000000..b832372090 --- /dev/null +++ b/plugins/periskop/package.json @@ -0,0 +1,58 @@ +{ + "name": "@backstage/plugin-periskop", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "homepage": "https://periskop.io", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/catalog-model": "^0.11.0", + "@backstage/core-components": "^0.8.9", + "@backstage/core-plugin-api": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/theme": "^0.2.15", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.57", + "moment": "^2.29.1", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0", + "react-dom": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.14.0", + "@backstage/core-app-api": "^0.5.3", + "@backstage/dev-utils": "^0.2.22", + "@backstage/test-utils": "^0.2.5", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^13.1.8", + "@types/jest": "*", + "@types/node": "*", + "cross-fetch": "^3.0.6", + "msw": "^0.35.0" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/periskop/src/api/index.ts b/plugins/periskop/src/api/index.ts new file mode 100644 index 0000000000..8683d636e4 --- /dev/null +++ b/plugins/periskop/src/api/index.ts @@ -0,0 +1,89 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 { ConfigApi, DiscoveryApi } from '@backstage/core-plugin-api'; +import { AggregatedError, NotFoundInLocation } from '../types'; + +type Options = { + discoveryApi: DiscoveryApi; + configApi: ConfigApi; +}; + +type PeriskopLocation = { + name: string; + host: string; +}; + +/** + * API abstraction to interact with Periskop's backends. + * + * @public + */ +export class PeriskopApi { + private readonly discoveryApi: DiscoveryApi; + private readonly locations: PeriskopLocation[]; + + constructor(options: Options) { + this.discoveryApi = options.discoveryApi; + this.locations = options.configApi + .getConfigArray('periskop.locations') + .flatMap(locConf => { + const name = locConf.getString('name'); + const host = locConf.getString('host'); + return { name: name, host: host }; + }); + } + + private getApiUrl(locationName: string): string | undefined { + return this.locations.find(loc => loc.name === locationName)?.host; + } + + getLocationNames(): string[] { + return this.locations.map(e => e.name); + } + + getErrorInstanceUrl( + locationName: string, + serviceName: string, + error: AggregatedError, + ): string { + return `${this.getApiUrl( + locationName, + )}/#/${serviceName}/errors/${encodeURIComponent(error.aggregation_key)}`; + } + + async getErrors( + locationName: string, + serviceName: string, + ): Promise { + const apiUrl = `${await this.discoveryApi.getBaseUrl( + 'periskop', + )}/${locationName}/${serviceName}`; + const response = await fetch(apiUrl); + + if (!response.ok) { + if (response.status === 404) { + return { + body: await response.text(), + }; + } + throw new Error( + `failed to fetch data, status ${response.status}: ${response.statusText}`, + ); + } + return response.json(); + } +} diff --git a/plugins/periskop/src/components/PeriskopErrorsTable/PeriskopErrorsTable.tsx b/plugins/periskop/src/components/PeriskopErrorsTable/PeriskopErrorsTable.tsx new file mode 100644 index 0000000000..494f83cef0 --- /dev/null +++ b/plugins/periskop/src/components/PeriskopErrorsTable/PeriskopErrorsTable.tsx @@ -0,0 +1,190 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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 moment from 'moment'; +import { Entity } from '@backstage/catalog-model'; +import { useApi } from '@backstage/core-plugin-api'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { + Content, + ContentHeader, + Table, + TableColumn, + Progress, + StatusWarning, + StatusError, + StatusPending, + Select, + EmptyState, + Link, +} from '@backstage/core-components'; +import Alert from '@material-ui/lab/Alert'; +import useAsync from 'react-use/lib/useAsync'; +import { periskopApiRef } from '../..'; +import { AggregatedError, NotFoundInLocation } from '../../types'; + +/** + * Constant storing Periskop project name. + * + * @public + */ +export const PERISKOP_NAME_ANNOTATION = 'periskop.io/name'; + +/** + * Returns true if Periskop annotation is present in the given entity. + * + * @public + */ +export const isPeriskopAvailable = (entity: Entity): boolean => + Boolean(entity.metadata.annotations?.[PERISKOP_NAME_ANNOTATION]); + +const renderKey = ( + error: AggregatedError, + linkTarget: string, +): React.ReactNode => { + return ( + + {error.aggregation_key} + + ); +}; + +const renderSeverity = (severity: string): React.ReactNode => { + if (severity.toLocaleLowerCase('en-US') === 'warning') { + return {severity}; + } else if (severity.toLocaleLowerCase('en-US') === 'error') { + return {severity}; + } + return {severity}; +}; + +const renderLastOccurrence = (error: AggregatedError): React.ReactNode => { + return moment(new Date(error.latest_errors[0].timestamp * 1000)).fromNow(); +}; + +function isNotFoundInLocation( + apiResult: AggregatedError[] | NotFoundInLocation | undefined, +): apiResult is NotFoundInLocation { + return (apiResult as NotFoundInLocation)?.body !== undefined; +} + +export const PeriskopErrorsTable = () => { + const { entity } = useEntity(); + const entityPeriskopName: string = + (entity.metadata.annotations?.[PERISKOP_NAME_ANNOTATION] as + | string + | undefined) ?? entity.metadata.name; + + const periskopApi = useApi(periskopApiRef); + const locations = periskopApi.getLocationNames(); + const [locationOption, setLocationOption] = React.useState( + locations[0], + ); + const { + value: aggregatedErrors, + loading, + error, + } = useAsync(async (): Promise => { + return periskopApi.getErrors(locationOption, entityPeriskopName); + }, [locationOption]); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + const columns: TableColumn[] = [ + { + title: 'Key', + field: 'aggregation_key', + highlight: true, + sorting: false, + render: aggregatedError => { + const errorUrl = periskopApi.getErrorInstanceUrl( + locationOption, + entityPeriskopName, + aggregatedError, + ); + return renderKey(aggregatedError, errorUrl); + }, + }, + { title: 'Occurrences', field: 'total_count', sorting: true }, + { + title: 'Last Occurrence', + render: aggregatedError => renderLastOccurrence(aggregatedError), + defaultSort: 'asc', + customSort: (a, b) => + b.latest_errors[0].timestamp - a.latest_errors[0].timestamp, + type: 'datetime', + }, + { + title: 'Severity', + field: 'severity', + render: aggregatedError => renderSeverity(aggregatedError.severity), + sorting: false, + }, + ]; + + const sortingSelect = ( + ({ + selected={instanceOption} + label="Instance" + items={instanceNames.map(e => ({ label: e, value: e, }))} - onChange={el => setLocationOption(el.toString())} + onChange={el => setInstanceOption(el.toString())} /> ); @@ -154,7 +154,7 @@ export const PeriskopErrorsTable = () => { ); - if (isNotFoundInLocation(aggregatedErrors)) { + if (isNotFoundInInstance(aggregatedErrors)) { return ( {contentHeader} diff --git a/plugins/periskop/src/types.ts b/plugins/periskop/src/types.ts index e6ee09f97c..084ba52620 100644 --- a/plugins/periskop/src/types.ts +++ b/plugins/periskop/src/types.ts @@ -43,6 +43,6 @@ export interface RequestHeaders { [k: string]: string; } -export interface NotFoundInLocation { +export interface NotFoundInInstance { body: string; } From 7bb98ee65536d00f44443ed2f00b334dccdae231 Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Tue, 1 Mar 2022 10:32:47 +0100 Subject: [PATCH 276/353] Rename host -> url Signed-off-by: Julio Zynger --- plugins/periskop-backend/src/api/index.ts | 9 ++++----- plugins/periskop-backend/src/service/router.test.ts | 2 +- plugins/periskop/README.md | 2 +- plugins/periskop/config.d.ts | 2 +- plugins/periskop/src/api/index.ts | 9 ++++----- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/plugins/periskop-backend/src/api/index.ts b/plugins/periskop-backend/src/api/index.ts index ff0f1e8b5d..6245dcacfd 100644 --- a/plugins/periskop-backend/src/api/index.ts +++ b/plugins/periskop-backend/src/api/index.ts @@ -24,7 +24,7 @@ export type Options = { type PeriskopInstance = { name: string; - host: string; + url: string; }; export class PeriskopApi { @@ -35,14 +35,13 @@ export class PeriskopApi { .getConfigArray('periskop.instances') .flatMap(locConf => { const name = locConf.getString('name'); - const host = locConf.getString('host'); - return { name: name, host: host }; + const url = locConf.getString('url'); + return { name: name, url: url }; }); } private getApiUrl(instanceName: string): string | undefined { - return this.instances.find(instance => instance.name === instanceName) - ?.host; + return this.instances.find(instance => instance.name === instanceName)?.url; } async getErrors( diff --git a/plugins/periskop-backend/src/service/router.test.ts b/plugins/periskop-backend/src/service/router.test.ts index f071ab901f..687255c6e0 100644 --- a/plugins/periskop-backend/src/service/router.test.ts +++ b/plugins/periskop-backend/src/service/router.test.ts @@ -32,7 +32,7 @@ describe('createRouter', () => { instances: [ { name: 'db', - host: 'http://periskop-db', + url: 'http://periskop-db', }, ], }, diff --git a/plugins/periskop/README.md b/plugins/periskop/README.md index ad71529b70..0c19d5f570 100644 --- a/plugins/periskop/README.md +++ b/plugins/periskop/README.md @@ -31,5 +31,5 @@ The plugin requires to configure _at least one_ Periskop API location in the [ap periskop: instances: - name: - host: + url: ``` diff --git a/plugins/periskop/config.d.ts b/plugins/periskop/config.d.ts index 31ab363a15..dfb914cd4f 100644 --- a/plugins/periskop/config.d.ts +++ b/plugins/periskop/config.d.ts @@ -33,7 +33,7 @@ export interface Config { * The hostname of the given Periskop instance * @visibility frontend */ - host: string; + url: string; }>; }; } diff --git a/plugins/periskop/src/api/index.ts b/plugins/periskop/src/api/index.ts index c35de9bcd3..1258529698 100644 --- a/plugins/periskop/src/api/index.ts +++ b/plugins/periskop/src/api/index.ts @@ -24,7 +24,7 @@ type Options = { type PeriskopInstance = { name: string; - host: string; + url: string; }; /** @@ -42,14 +42,13 @@ export class PeriskopApi { .getConfigArray('periskop.instances') .flatMap(locConf => { const name = locConf.getString('name'); - const host = locConf.getString('host'); - return { name: name, host: host }; + const url = locConf.getString('url'); + return { name: name, url: url }; }); } private getApiUrl(instanceName: string): string | undefined { - return this.instances.find(instance => instance.name === instanceName) - ?.host; + return this.instances.find(instance => instance.name === instanceName)?.url; } getInstanceNames(): string[] { From b6fdf5c5f351ed99fa67b531f30a45f393021c50 Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Tue, 1 Mar 2022 10:49:24 +0100 Subject: [PATCH 277/353] Fix warning and update api-reports Signed-off-by: Julio Zynger --- plugins/periskop/api-report.md | 92 +++++++++++++++++++++++++------ plugins/periskop/src/api/index.ts | 5 +- plugins/periskop/src/index.ts | 1 + plugins/periskop/src/types.ts | 10 ++++ 4 files changed, 89 insertions(+), 19 deletions(-) diff --git a/plugins/periskop/api-report.md b/plugins/periskop/api-report.md index 8222f7a1c5..e60f039d4e 100644 --- a/plugins/periskop/api-report.md +++ b/plugins/periskop/api-report.md @@ -10,37 +10,94 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ConfigApi } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; -import { RouteRef } from '@backstage/core-plugin-api'; + +// @public (undocumented) +export interface AggregatedError { + // (undocumented) + aggregation_key: string; + // (undocumented) + latest_errors: ErrorInstance[]; + // (undocumented) + severity: string; + // (undocumented) + total_count: number; +} + +// @public (undocumented) +interface Error_2 { + // (undocumented) + cause?: Error_2 | null; + // (undocumented) + class: string; + // (undocumented) + message: string; + // (undocumented) + stacktrace?: string[]; +} +export { Error_2 as Error }; + +// @public (undocumented) +export interface ErrorInstance { + // (undocumented) + error: Error_2; + // (undocumented) + http_context: HttpContext; + // (undocumented) + severity: string; + // (undocumented) + timestamp: number; + // (undocumented) + uuid: string; +} + +// @public (undocumented) +export interface HttpContext { + // (undocumented) + request_body: string; + // (undocumented) + request_headers: RequestHeaders; + // (undocumented) + request_method: string; + // (undocumented) + request_url: string; +} // @public export const isPeriskopAvailable: (entity: Entity) => boolean; +// @public (undocumented) +export interface NotFoundInInstance { + // (undocumented) + body: string; +} + // @public export const PERISKOP_NAME_ANNOTATION = 'periskop.io/name'; // @public export class PeriskopApi { - // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts - constructor(options: Options); - // Warning: (ae-forgotten-export) The symbol "AggregatedError" needs to be exported by the entry point index.d.ts - // + constructor(options: PeriskopApiOptions); // (undocumented) getErrorInstanceUrl( - locationName: string, + instanceName: string, serviceName: string, error: AggregatedError, ): string; - // Warning: (ae-forgotten-export) The symbol "NotFoundInLocation" needs to be exported by the entry point index.d.ts - // // (undocumented) getErrors( - locationName: string, + instanceName: string, serviceName: string, - ): Promise; + ): Promise; // (undocumented) - getLocationNames(): string[]; + getInstanceNames(): string[]; } +// @public (undocumented) +export type PeriskopApiOptions = { + discoveryApi: DiscoveryApi; + configApi: ConfigApi; +}; + // @public (undocumented) export const periskopApiRef: ApiRef; @@ -48,12 +105,13 @@ export const periskopApiRef: ApiRef; export const PeriskopErrorsTable: () => JSX.Element; // @public (undocumented) -export const periskopPlugin: BackstagePlugin< - { - root: RouteRef; - }, - {} ->; +export const periskopPlugin: BackstagePlugin<{}, {}>; + +// @public (undocumented) +export interface RequestHeaders { + // (undocumented) + [k: string]: string; +} // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/periskop/src/api/index.ts b/plugins/periskop/src/api/index.ts index 1258529698..c935733a1e 100644 --- a/plugins/periskop/src/api/index.ts +++ b/plugins/periskop/src/api/index.ts @@ -17,7 +17,8 @@ import { ConfigApi, DiscoveryApi } from '@backstage/core-plugin-api'; import { AggregatedError, NotFoundInInstance } from '../types'; -type Options = { +/** @public */ +export type PeriskopApiOptions = { discoveryApi: DiscoveryApi; configApi: ConfigApi; }; @@ -36,7 +37,7 @@ export class PeriskopApi { private readonly discoveryApi: DiscoveryApi; private readonly instances: PeriskopInstance[]; - constructor(options: Options) { + constructor(options: PeriskopApiOptions) { this.discoveryApi = options.discoveryApi; this.instances = options.configApi .getConfigArray('periskop.instances') diff --git a/plugins/periskop/src/index.ts b/plugins/periskop/src/index.ts index 4f66e60228..a22fc65d73 100644 --- a/plugins/periskop/src/index.ts +++ b/plugins/periskop/src/index.ts @@ -21,3 +21,4 @@ export { PERISKOP_NAME_ANNOTATION, } from './components/PeriskopErrorsTable'; export * from './api/index'; +export * from './types'; diff --git a/plugins/periskop/src/types.ts b/plugins/periskop/src/types.ts index 084ba52620..d3d0d6663f 100644 --- a/plugins/periskop/src/types.ts +++ b/plugins/periskop/src/types.ts @@ -14,12 +14,15 @@ * limitations under the License. */ +/** @public */ export interface AggregatedError { aggregation_key: string; total_count: number; severity: string; latest_errors: ErrorInstance[]; } + +/** @public */ export interface ErrorInstance { error: Error; uuid: string; @@ -27,22 +30,29 @@ export interface ErrorInstance { severity: string; http_context: HttpContext; } + +/** @public */ export interface Error { class: string; message: string; stacktrace?: string[]; cause?: Error | null; } + +/** @public */ export interface HttpContext { request_method: string; request_url: string; request_headers: RequestHeaders; request_body: string; } + +/** @public */ export interface RequestHeaders { [k: string]: string; } +/** @public */ export interface NotFoundInInstance { body: string; } From 5ed59fb8c14162a32160a1abd99aa0bd1884e1a0 Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Tue, 1 Mar 2022 11:04:40 +0100 Subject: [PATCH 278/353] Revamp README Signed-off-by: Julio Zynger --- plugins/periskop/README.md | 42 +++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/plugins/periskop/README.md b/plugins/periskop/README.md index 0c19d5f570..acc1e03074 100644 --- a/plugins/periskop/README.md +++ b/plugins/periskop/README.md @@ -4,22 +4,54 @@ ![periskop-logo](https://i.imgur.com/z8BLePO.png) -### `PeriskopErrorsTable` +## Periskop aggregated errors -The Periskop Backstage Plugin exposes an entity tab component named `PeriskopErrorsTable`. Each of the entries in the table will direct you to the error details in your deployed Periskop instance location. +The Periskop Backstage Plugin exposes a component named `PeriskopErrorsTable`. +Each of the entries in the table will direct you to the error details in your deployed Periskop instance location. ![periskop-errors-card](./docs/periskop-plugin-screenshot.png) -Now your plugin should be visible as a tab at the top of the entity pages. -However, it warns of a missing `periskop.io/name` annotation. +## Setup -Add the annotation to your component descriptor file as shown in the highlighted example below: +1. Configure the [periskop backend plugin](https://github.com/backstage/backstage/tree/master/plugins/periskop-backend/) + +2. If you have standalone app (you didn't clone this repo), then do + +```bash +# From your Backstage root directory +cd packages/app +yarn add @backstage/plugin-periskop +``` + +3. Add to the app `EntityPage` component: + +```tsx +// In packages/app/src/components/catalog/EntityPage.tsx +import { PeriskopErrorsTable } from '@backstage/plugin-periskop'; + +const componentPage = ( + + {/* other tabs... */} + + + + + + + +``` + +4. [Setup the `app-config.yaml`](#instances) and instance name and urls + +5. Annotate entities with the periskop service name ```yaml annotations: periskop.io/name: '' ``` +6. Run app with `yarn start` and navigate to `/periskop` or a catalog entity 'Periskop' tab + ### Instances The periskop plugin can be configured to fetch aggregated errors from multiple deployment instances. From a9c4cc91671b01b07a8c1950753925faf8fc7c64 Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Tue, 1 Mar 2022 14:13:57 +0100 Subject: [PATCH 279/353] Replace moment with luxon Signed-off-by: Julio Zynger --- plugins/periskop/package.json | 3 ++- .../components/PeriskopErrorsTable/PeriskopErrorsTable.tsx | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/periskop/package.json b/plugins/periskop/package.json index c6fc02a308..661efc23c3 100644 --- a/plugins/periskop/package.json +++ b/plugins/periskop/package.json @@ -30,7 +30,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "moment": "^2.29.1", + "luxon": "^2.0.2", "react-use": "^17.2.4" }, "peerDependencies": { @@ -46,6 +46,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", + "@types/luxon": "^2.0.4", "@types/node": "^14.14.32", "cross-fetch": "^3.1.5", "msw": "^0.35.0" diff --git a/plugins/periskop/src/components/PeriskopErrorsTable/PeriskopErrorsTable.tsx b/plugins/periskop/src/components/PeriskopErrorsTable/PeriskopErrorsTable.tsx index 54f97b0700..ae299c6128 100644 --- a/plugins/periskop/src/components/PeriskopErrorsTable/PeriskopErrorsTable.tsx +++ b/plugins/periskop/src/components/PeriskopErrorsTable/PeriskopErrorsTable.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import moment from 'moment'; +import { DateTime } from 'luxon'; import { Entity } from '@backstage/catalog-model'; import { useApi } from '@backstage/core-plugin-api'; import { useEntity } from '@backstage/plugin-catalog-react'; @@ -69,7 +69,9 @@ const renderSeverity = (severity: string): React.ReactNode => { }; const renderLastOccurrence = (error: AggregatedError): React.ReactNode => { - return moment(new Date(error.latest_errors[0].timestamp * 1000)).fromNow(); + return DateTime.fromMillis( + error.latest_errors[0].timestamp * 1000, + ).toRelative(); }; function isNotFoundInInstance( From 2d7aa19e099e08f4fd02b29c9b840f666a54a45a Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Tue, 1 Mar 2022 14:15:36 +0100 Subject: [PATCH 280/353] Remove redundant cast Signed-off-by: Julio Zynger --- .../components/PeriskopErrorsTable/PeriskopErrorsTable.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/periskop/src/components/PeriskopErrorsTable/PeriskopErrorsTable.tsx b/plugins/periskop/src/components/PeriskopErrorsTable/PeriskopErrorsTable.tsx index ae299c6128..2292ed0013 100644 --- a/plugins/periskop/src/components/PeriskopErrorsTable/PeriskopErrorsTable.tsx +++ b/plugins/periskop/src/components/PeriskopErrorsTable/PeriskopErrorsTable.tsx @@ -83,9 +83,8 @@ function isNotFoundInInstance( export const PeriskopErrorsTable = () => { const { entity } = useEntity(); const entityPeriskopName: string = - (entity.metadata.annotations?.[PERISKOP_NAME_ANNOTATION] as - | string - | undefined) ?? entity.metadata.name; + entity.metadata.annotations?.[PERISKOP_NAME_ANNOTATION] ?? + entity.metadata.name; const periskopApi = useApi(periskopApiRef); const instanceNames = periskopApi.getInstanceNames(); From 9cbfe00c5c988198aafcbab95e4c1531d241e1a2 Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Tue, 1 Mar 2022 14:17:28 +0100 Subject: [PATCH 281/353] Use ResponseErrorPanel Signed-off-by: Julio Zynger --- .../components/PeriskopErrorsTable/PeriskopErrorsTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/periskop/src/components/PeriskopErrorsTable/PeriskopErrorsTable.tsx b/plugins/periskop/src/components/PeriskopErrorsTable/PeriskopErrorsTable.tsx index 2292ed0013..e7044a7037 100644 --- a/plugins/periskop/src/components/PeriskopErrorsTable/PeriskopErrorsTable.tsx +++ b/plugins/periskop/src/components/PeriskopErrorsTable/PeriskopErrorsTable.tsx @@ -25,6 +25,7 @@ import { Table, TableColumn, Progress, + ResponseErrorPanel, StatusWarning, StatusError, StatusPending, @@ -32,7 +33,6 @@ import { EmptyState, Link, } from '@backstage/core-components'; -import Alert from '@material-ui/lab/Alert'; import useAsync from 'react-use/lib/useAsync'; import { periskopApiRef } from '../..'; import { AggregatedError, NotFoundInInstance } from '../../types'; @@ -102,7 +102,7 @@ export const PeriskopErrorsTable = () => { if (loading) { return ; } else if (error) { - return {error.message}; + return ; } const columns: TableColumn[] = [ From 77caacc10d88bf6e8e0bce88ee62044ea9e996fa Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Tue, 1 Mar 2022 14:18:40 +0100 Subject: [PATCH 282/353] Rewrite section in README Signed-off-by: Julio Zynger --- plugins/periskop/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/periskop/README.md b/plugins/periskop/README.md index acc1e03074..64bd26ae86 100644 --- a/plugins/periskop/README.md +++ b/plugins/periskop/README.md @@ -41,9 +41,9 @@ const componentPage = ( ``` -4. [Setup the `app-config.yaml`](#instances) and instance name and urls +1. [Setup the `app-config.yaml`](#instances) `periskop` block -5. Annotate entities with the periskop service name +2. Annotate entities with the periskop service name ```yaml annotations: From 4d7c6e2a110da10b4416fc17aafef8384df4ece9 Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Tue, 1 Mar 2022 14:26:54 +0100 Subject: [PATCH 283/353] Count properly Signed-off-by: Julio Zynger --- plugins/periskop/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/periskop/README.md b/plugins/periskop/README.md index 64bd26ae86..ecf66c4e24 100644 --- a/plugins/periskop/README.md +++ b/plugins/periskop/README.md @@ -41,9 +41,9 @@ const componentPage = ( ``` -1. [Setup the `app-config.yaml`](#instances) `periskop` block +4. [Setup the `app-config.yaml`](#instances) `periskop` block -2. Annotate entities with the periskop service name +5. Annotate entities with the periskop service name ```yaml annotations: From bdfdace83c8f987e2a583b4bf5011b13bcbe68f0 Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Fri, 4 Mar 2022 09:46:18 +0100 Subject: [PATCH 284/353] Rename PeriskopErrorsTable -> EntityPeriskopErrorsCard Per https://backstage.io/docs/plugins/composability#naming-patterns Signed-off-by: Julio Zynger --- plugins/periskop/README.md | 6 +++--- plugins/periskop/api-report.md | 2 +- ...riskopErrorsTable.tsx => EntityPeriskopErrorsCard.tsx} | 2 +- .../periskop/src/components/PeriskopErrorsTable/index.ts | 4 ++-- plugins/periskop/src/extensions.ts | 8 ++++---- plugins/periskop/src/index.ts | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) rename plugins/periskop/src/components/PeriskopErrorsTable/{PeriskopErrorsTable.tsx => EntityPeriskopErrorsCard.tsx} (99%) diff --git a/plugins/periskop/README.md b/plugins/periskop/README.md index ecf66c4e24..ecc7349bef 100644 --- a/plugins/periskop/README.md +++ b/plugins/periskop/README.md @@ -6,7 +6,7 @@ ## Periskop aggregated errors -The Periskop Backstage Plugin exposes a component named `PeriskopErrorsTable`. +The Periskop Backstage Plugin exposes a component named `EntityPeriskopErrorsCard`. Each of the entries in the table will direct you to the error details in your deployed Periskop instance location. ![periskop-errors-card](./docs/periskop-plugin-screenshot.png) @@ -27,7 +27,7 @@ yarn add @backstage/plugin-periskop ```tsx // In packages/app/src/components/catalog/EntityPage.tsx -import { PeriskopErrorsTable } from '@backstage/plugin-periskop'; +import { EntityPeriskopErrorsCard } from '@backstage/plugin-periskop'; const componentPage = ( @@ -35,7 +35,7 @@ const componentPage = ( - + diff --git a/plugins/periskop/api-report.md b/plugins/periskop/api-report.md index e60f039d4e..5412b70dfb 100644 --- a/plugins/periskop/api-report.md +++ b/plugins/periskop/api-report.md @@ -102,7 +102,7 @@ export type PeriskopApiOptions = { export const periskopApiRef: ApiRef; // @public (undocumented) -export const PeriskopErrorsTable: () => JSX.Element; +export const EntityPeriskopErrorsCard: () => JSX.Element; // @public (undocumented) export const periskopPlugin: BackstagePlugin<{}, {}>; diff --git a/plugins/periskop/src/components/PeriskopErrorsTable/PeriskopErrorsTable.tsx b/plugins/periskop/src/components/PeriskopErrorsTable/EntityPeriskopErrorsCard.tsx similarity index 99% rename from plugins/periskop/src/components/PeriskopErrorsTable/PeriskopErrorsTable.tsx rename to plugins/periskop/src/components/PeriskopErrorsTable/EntityPeriskopErrorsCard.tsx index e7044a7037..c4c8623f8e 100644 --- a/plugins/periskop/src/components/PeriskopErrorsTable/PeriskopErrorsTable.tsx +++ b/plugins/periskop/src/components/PeriskopErrorsTable/EntityPeriskopErrorsCard.tsx @@ -80,7 +80,7 @@ function isNotFoundInInstance( return (apiResult as NotFoundInInstance)?.body !== undefined; } -export const PeriskopErrorsTable = () => { +export const EntityPeriskopErrorsCard = () => { const { entity } = useEntity(); const entityPeriskopName: string = entity.metadata.annotations?.[PERISKOP_NAME_ANNOTATION] ?? diff --git a/plugins/periskop/src/components/PeriskopErrorsTable/index.ts b/plugins/periskop/src/components/PeriskopErrorsTable/index.ts index 61936dc476..0d0ab65d41 100644 --- a/plugins/periskop/src/components/PeriskopErrorsTable/index.ts +++ b/plugins/periskop/src/components/PeriskopErrorsTable/index.ts @@ -15,7 +15,7 @@ */ export { - PeriskopErrorsTable, + EntityPeriskopErrorsCard, isPeriskopAvailable, PERISKOP_NAME_ANNOTATION, -} from './PeriskopErrorsTable'; +} from './EntityPeriskopErrorsCard'; diff --git a/plugins/periskop/src/extensions.ts b/plugins/periskop/src/extensions.ts index 663be0ec35..990a41f24e 100644 --- a/plugins/periskop/src/extensions.ts +++ b/plugins/periskop/src/extensions.ts @@ -20,13 +20,13 @@ import { createComponentExtension } from '@backstage/core-plugin-api'; /** * @public */ -export const PeriskopErrorsTable = periskopPlugin.provide( +export const EntityPeriskopErrorsCard = periskopPlugin.provide( createComponentExtension({ - name: 'PeriskopErrorsTable', + name: 'EntityPeriskopErrorsCard', component: { lazy: () => - import('./components/PeriskopErrorsTable').then( - m => m.PeriskopErrorsTable, + import('./components/EntityPeriskopErrorsCard').then( + m => m.EntityPeriskopErrorsCard, ), }, }), diff --git a/plugins/periskop/src/index.ts b/plugins/periskop/src/index.ts index a22fc65d73..facb97bcc8 100644 --- a/plugins/periskop/src/index.ts +++ b/plugins/periskop/src/index.ts @@ -15,10 +15,10 @@ */ export { periskopPlugin, periskopApiRef } from './plugin'; -export { PeriskopErrorsTable } from './extensions'; +export { EntityPeriskopErrorsCard } from './extensions'; export { isPeriskopAvailable, PERISKOP_NAME_ANNOTATION, -} from './components/PeriskopErrorsTable'; +} from './components/EntityPeriskopErrorsCard'; export * from './api/index'; export * from './types'; From dc4103fe0991604811e2a8011ab53eb65c510a9b Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Fri, 4 Mar 2022 09:47:24 +0100 Subject: [PATCH 285/353] Update documentation - don't mention standalone app difference Signed-off-by: Julio Zynger --- plugins/periskop/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/periskop/README.md b/plugins/periskop/README.md index ecc7349bef..e475fd3477 100644 --- a/plugins/periskop/README.md +++ b/plugins/periskop/README.md @@ -15,7 +15,7 @@ Each of the entries in the table will direct you to the error details in your de 1. Configure the [periskop backend plugin](https://github.com/backstage/backstage/tree/master/plugins/periskop-backend/) -2. If you have standalone app (you didn't clone this repo), then do +2. Install the plugin by running: ```bash # From your Backstage root directory From 71ced2f7a5e58c2708d14c4be852e9764b4e5e35 Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Fri, 4 Mar 2022 09:51:18 +0100 Subject: [PATCH 286/353] Rename dir Signed-off-by: Julio Zynger --- .../EntityPeriskopErrorsCard.tsx | 2 +- .../{PeriskopErrorsTable => EntityPeriskopErrorsCard}/index.ts | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename plugins/periskop/src/components/{PeriskopErrorsTable => EntityPeriskopErrorsCard}/EntityPeriskopErrorsCard.tsx (98%) rename plugins/periskop/src/components/{PeriskopErrorsTable => EntityPeriskopErrorsCard}/index.ts (100%) diff --git a/plugins/periskop/src/components/PeriskopErrorsTable/EntityPeriskopErrorsCard.tsx b/plugins/periskop/src/components/EntityPeriskopErrorsCard/EntityPeriskopErrorsCard.tsx similarity index 98% rename from plugins/periskop/src/components/PeriskopErrorsTable/EntityPeriskopErrorsCard.tsx rename to plugins/periskop/src/components/EntityPeriskopErrorsCard/EntityPeriskopErrorsCard.tsx index c4c8623f8e..cd4aa6da0f 100644 --- a/plugins/periskop/src/components/PeriskopErrorsTable/EntityPeriskopErrorsCard.tsx +++ b/plugins/periskop/src/components/EntityPeriskopErrorsCard/EntityPeriskopErrorsCard.tsx @@ -42,7 +42,7 @@ import { AggregatedError, NotFoundInInstance } from '../../types'; * * @public */ -export const PERISKOP_NAME_ANNOTATION = 'periskop.io/name'; +export const PERISKOP_NAME_ANNOTATION = 'periskop.io/service-name'; /** * Returns true if Periskop annotation is present in the given entity. diff --git a/plugins/periskop/src/components/PeriskopErrorsTable/index.ts b/plugins/periskop/src/components/EntityPeriskopErrorsCard/index.ts similarity index 100% rename from plugins/periskop/src/components/PeriskopErrorsTable/index.ts rename to plugins/periskop/src/components/EntityPeriskopErrorsCard/index.ts From 7235b16054f1d786a75b09bd8259855156fe7952 Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Fri, 4 Mar 2022 09:51:29 +0100 Subject: [PATCH 287/353] Rename annotation Signed-off-by: Julio Zynger --- docs/features/software-catalog/well-known-annotations.md | 4 ++-- plugins/periskop/README.md | 2 +- plugins/periskop/api-report.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index 2f41377a36..4fa71329dd 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -222,13 +222,13 @@ definition. Specifying this annotation will enable GoCD related features in Backstage for that entity. -### periskop.io/name +### periskop.io/service-name ```yaml # Example: metadata: annotations: - periskop.io/name: pump-station + periskop.io/service-name: pump-station ``` The value of this annotation is the periskop project name for the given entity. diff --git a/plugins/periskop/README.md b/plugins/periskop/README.md index e475fd3477..35e86f083d 100644 --- a/plugins/periskop/README.md +++ b/plugins/periskop/README.md @@ -47,7 +47,7 @@ const componentPage = ( ```yaml annotations: - periskop.io/name: '' + periskop.io/service-name: '' ``` 6. Run app with `yarn start` and navigate to `/periskop` or a catalog entity 'Periskop' tab diff --git a/plugins/periskop/api-report.md b/plugins/periskop/api-report.md index 5412b70dfb..bd3e00d012 100644 --- a/plugins/periskop/api-report.md +++ b/plugins/periskop/api-report.md @@ -72,7 +72,7 @@ export interface NotFoundInInstance { } // @public -export const PERISKOP_NAME_ANNOTATION = 'periskop.io/name'; +export const PERISKOP_NAME_ANNOTATION = 'periskop.io/service-name'; // @public export class PeriskopApi { From 9e047159673f81331e253deaa495941124d3b108 Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Fri, 4 Mar 2022 10:09:44 +0100 Subject: [PATCH 288/353] Extract interface Signed-off-by: Julio Zynger --- plugins/periskop/src/api/index.ts | 32 ++++++++++++++++++++++++++++++- plugins/periskop/src/plugin.ts | 4 ++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/plugins/periskop/src/api/index.ts b/plugins/periskop/src/api/index.ts index c935733a1e..0afbe9abd9 100644 --- a/plugins/periskop/src/api/index.ts +++ b/plugins/periskop/src/api/index.ts @@ -33,7 +33,37 @@ type PeriskopInstance = { * * @public */ -export class PeriskopApi { +export interface PeriskopApi { + /** + * Returns the list of registered Periskop instance names. + */ + getInstanceNames(): string[]; + + /** + * For the given instance and service, returns the URL pointing to the specific error instance occurrence. + * Note: This method might point to an external route. + */ + getErrorInstanceUrl( + instanceName: string, + serviceName: string, + error: AggregatedError, + ): string; + + /** + * Fetches all errors for the given service from the specified Periskop instance, given its name. + */ + getErrors( + instanceName: string, + serviceName: string, + ): Promise; +} + +/** + * API implementation to interact with Periskop's backends. + * + * @public + */ +export class PeriskopClient implements PeriskopApi { private readonly discoveryApi: DiscoveryApi; private readonly instances: PeriskopInstance[]; diff --git a/plugins/periskop/src/plugin.ts b/plugins/periskop/src/plugin.ts index 6a99980263..0fb0c2bede 100644 --- a/plugins/periskop/src/plugin.ts +++ b/plugins/periskop/src/plugin.ts @@ -21,7 +21,7 @@ import { createPlugin, createApiRef, } from '@backstage/core-plugin-api'; -import { PeriskopApi } from './api'; +import { PeriskopApi, PeriskopClient } from './api'; /** * @public @@ -40,7 +40,7 @@ export const periskopPlugin = createPlugin({ api: periskopApiRef, deps: { configApi: configApiRef, discoveryApi: discoveryApiRef }, factory: ({ configApi, discoveryApi }) => - new PeriskopApi({ configApi, discoveryApi }), + new PeriskopClient({ configApi, discoveryApi }), }), ], }); From aee6d9d60c21e1afb71a2792549732b74bfa1b65 Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Mon, 7 Mar 2022 12:59:53 +0200 Subject: [PATCH 289/353] Use @backstage/errors Signed-off-by: Julio Zynger --- plugins/periskop/package.json | 1 + plugins/periskop/src/api/index.ts | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/periskop/package.json b/plugins/periskop/package.json index 661efc23c3..5c925cde08 100644 --- a/plugins/periskop/package.json +++ b/plugins/periskop/package.json @@ -25,6 +25,7 @@ "@backstage/catalog-model": "^0.11.0", "@backstage/core-components": "^0.8.10", "@backstage/core-plugin-api": "^0.7.0", + "@backstage/errors": "^0.2.2", "@backstage/plugin-catalog-react": "^0.7.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", diff --git a/plugins/periskop/src/api/index.ts b/plugins/periskop/src/api/index.ts index 0afbe9abd9..4171d55ae2 100644 --- a/plugins/periskop/src/api/index.ts +++ b/plugins/periskop/src/api/index.ts @@ -15,6 +15,7 @@ */ import { ConfigApi, DiscoveryApi } from '@backstage/core-plugin-api'; +import { ResponseError } from '@backstage/errors'; import { AggregatedError, NotFoundInInstance } from '../types'; /** @public */ @@ -111,9 +112,7 @@ export class PeriskopClient implements PeriskopApi { body: await response.text(), }; } - throw new Error( - `failed to fetch data, status ${response.status}: ${response.statusText}`, - ); + throw await ResponseError.fromResponse(response); } return response.json(); } From 0dcea38a79ecf1510b02a71ed2e0dee4ee9d318d Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Mon, 7 Mar 2022 13:00:46 +0200 Subject: [PATCH 290/353] Bump @backstage/backend-common Signed-off-by: Julio Zynger --- plugins/periskop-backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/periskop-backend/package.json b/plugins/periskop-backend/package.json index d036313df5..034945c7cd 100644 --- a/plugins/periskop-backend/package.json +++ b/plugins/periskop-backend/package.json @@ -21,7 +21,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.8", + "@backstage/backend-common": "^0.12.0", "@backstage/config": "^0.1.14", "@types/express": "*", "cross-fetch": "^3.0.6", From ad865be1701b84b5c09f3702d5b7138f39739416 Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Mon, 7 Mar 2022 13:03:24 +0200 Subject: [PATCH 291/353] Apply package roles Signed-off-by: Julio Zynger --- plugins/periskop-backend/package.json | 17 ++++++++++------- plugins/periskop/package.json | 17 ++++++++++------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/plugins/periskop-backend/package.json b/plugins/periskop-backend/package.json index 034945c7cd..1fddb1b9d6 100644 --- a/plugins/periskop-backend/package.json +++ b/plugins/periskop-backend/package.json @@ -6,19 +6,22 @@ "license": "Apache-2.0", "private": false, "homepage": "https://periskop.io", + "backstage": { + "role": "backend-plugin" + }, "publishConfig": { "access": "public", "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, "scripts": { - "start": "backstage-cli backend:dev", - "build": "backstage-cli backend:build", - "lint": "backstage-cli lint", - "test": "backstage-cli test", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { "@backstage/backend-common": "^0.12.0", diff --git a/plugins/periskop/package.json b/plugins/periskop/package.json index 5c925cde08..1bf83576e3 100644 --- a/plugins/periskop/package.json +++ b/plugins/periskop/package.json @@ -6,20 +6,23 @@ "license": "Apache-2.0", "private": false, "homepage": "https://periskop.io", + "backstage": { + "role": "frontend-plugin" + }, "publishConfig": { "access": "public", "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", - "lint": "backstage-cli lint", - "test": "backstage-cli test", + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", "diff": "backstage-cli plugin:diff", - "prepack": "backstage-cli prepack", - "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" }, "dependencies": { "@backstage/catalog-model": "^0.11.0", From 45ea463a0d2ab675bb6ae8ae8d483d723b55437a Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Mon, 7 Mar 2022 14:05:51 +0200 Subject: [PATCH 292/353] Update api-report Signed-off-by: Julio Zynger --- plugins/periskop/api-report.md | 40 +++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/plugins/periskop/api-report.md b/plugins/periskop/api-report.md index bd3e00d012..2cf84faccf 100644 --- a/plugins/periskop/api-report.md +++ b/plugins/periskop/api-report.md @@ -23,6 +23,9 @@ export interface AggregatedError { total_count: number; } +// @public (undocumented) +export const EntityPeriskopErrorsCard: () => JSX.Element; + // @public (undocumented) interface Error_2 { // (undocumented) @@ -75,7 +78,30 @@ export interface NotFoundInInstance { export const PERISKOP_NAME_ANNOTATION = 'periskop.io/service-name'; // @public -export class PeriskopApi { +export interface PeriskopApi { + getErrorInstanceUrl( + instanceName: string, + serviceName: string, + error: AggregatedError, + ): string; + getErrors( + instanceName: string, + serviceName: string, + ): Promise; + getInstanceNames(): string[]; +} + +// @public (undocumented) +export type PeriskopApiOptions = { + discoveryApi: DiscoveryApi; + configApi: ConfigApi; +}; + +// @public (undocumented) +export const periskopApiRef: ApiRef; + +// @public +export class PeriskopClient implements PeriskopApi { constructor(options: PeriskopApiOptions); // (undocumented) getErrorInstanceUrl( @@ -92,18 +118,6 @@ export class PeriskopApi { getInstanceNames(): string[]; } -// @public (undocumented) -export type PeriskopApiOptions = { - discoveryApi: DiscoveryApi; - configApi: ConfigApi; -}; - -// @public (undocumented) -export const periskopApiRef: ApiRef; - -// @public (undocumented) -export const EntityPeriskopErrorsCard: () => JSX.Element; - // @public (undocumented) export const periskopPlugin: BackstagePlugin<{}, {}>; From 14a790901767f898bcabcdb07c14653c78f8cfac Mon Sep 17 00:00:00 2001 From: Julio Zynger Date: Mon, 7 Mar 2022 14:48:07 +0200 Subject: [PATCH 293/353] Bump dependencies Signed-off-by: Julio Zynger --- plugins/periskop-backend/package.json | 4 ++-- plugins/periskop/package.json | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/periskop-backend/package.json b/plugins/periskop-backend/package.json index 1fddb1b9d6..6c7951283a 100644 --- a/plugins/periskop-backend/package.json +++ b/plugins/periskop-backend/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@backstage/backend-common": "^0.12.0", - "@backstage/config": "^0.1.14", + "@backstage/config": "^0.1.15", "@types/express": "*", "cross-fetch": "^3.0.6", "express": "^4.17.1", @@ -35,7 +35,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.0", + "@backstage/cli": "^0.15.0", "@types/supertest": "^2.0.8", "msw": "^0.35.0", "supertest": "^4.0.2" diff --git a/plugins/periskop/package.json b/plugins/periskop/package.json index 1bf83576e3..8045b94133 100644 --- a/plugins/periskop/package.json +++ b/plugins/periskop/package.json @@ -25,11 +25,11 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@backstage/catalog-model": "^0.11.0", - "@backstage/core-components": "^0.8.10", - "@backstage/core-plugin-api": "^0.7.0", + "@backstage/catalog-model": "^0.12.0", + "@backstage/core-components": "^0.9.0", + "@backstage/core-plugin-api": "^0.8.0", "@backstage/errors": "^0.2.2", - "@backstage/plugin-catalog-react": "^0.7.0", + "@backstage/plugin-catalog-react": "^0.8.0", "@backstage/theme": "^0.2.15", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -42,10 +42,10 @@ "react-dom": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.14.1", - "@backstage/core-app-api": "^0.5.4", - "@backstage/dev-utils": "^0.2.23", - "@backstage/test-utils": "^0.2.6", + "@backstage/cli": "^0.15.0", + "@backstage/core-app-api": "^0.6.0", + "@backstage/dev-utils": "^0.2.24", + "@backstage/test-utils": "^0.3.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", From c72eb5bbb8a6e62dfa91fff7c9826af3deb7f59a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 7 Mar 2022 15:10:20 +0100 Subject: [PATCH 294/353] fixup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- plugins/periskop-backend/api-report.md | 4 ++-- plugins/periskop-backend/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/periskop-backend/api-report.md b/plugins/periskop-backend/api-report.md index 7e737a21a8..caaed109f9 100644 --- a/plugins/periskop-backend/api-report.md +++ b/plugins/periskop-backend/api-report.md @@ -5,7 +5,7 @@ ```ts import { Config } from '@backstage/config'; import express from 'express'; -import { Logger as Logger_2 } from 'winston'; +import { Logger } from 'winston'; // @public (undocumented) export function createRouter(options: RouterOptions): Promise; @@ -15,7 +15,7 @@ export interface RouterOptions { // (undocumented) config: Config; // (undocumented) - logger: Logger_2; + logger: Logger; } // (No @packageDocumentation comment for this package) diff --git a/plugins/periskop-backend/package.json b/plugins/periskop-backend/package.json index 6c7951283a..0efb9d7168 100644 --- a/plugins/periskop-backend/package.json +++ b/plugins/periskop-backend/package.json @@ -38,7 +38,7 @@ "@backstage/cli": "^0.15.0", "@types/supertest": "^2.0.8", "msw": "^0.35.0", - "supertest": "^4.0.2" + "supertest": "^6.1.6" }, "files": [ "dist" From b713a987c8ceaf729257b3f90cee2538918f545e Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Mon, 7 Mar 2022 08:13:54 -0600 Subject: [PATCH 295/353] Fixed failing test Signed-off-by: Andre Wanlin --- .../CatalogKindHeader.test.tsx | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx index 7ce14817d6..7783278eda 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx @@ -139,30 +139,34 @@ describe('', () => { it('responds to external queryParameters changes', async () => { const updateFilters = jest.fn(); const rendered = await renderWithEffects( - - - , + + + + + , ); expect(updateFilters).toHaveBeenLastCalledWith({ - kind: new EntityKindFilter('Components'), + kind: new EntityKindFilter('components'), }); rendered.rerender( - - - , + + + + + , ); expect(updateFilters).toHaveBeenLastCalledWith({ - kind: new EntityKindFilter('Template'), + kind: new EntityKindFilter('templates'), }); }); }); From 72fa341eb5a270dfa6221b238fcd7e3705ad0f03 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 7 Mar 2022 14:53:26 +0100 Subject: [PATCH 296/353] packages,plugins: tweak migrated lint configurations Signed-off-by: Patrik Oldsberg --- packages/app/.eslintrc.js | 11 +-- packages/core-app-api/.eslintrc.js | 1 - packages/core-components/.eslintrc.js | 83 ++----------------- packages/core-plugin-api/.eslintrc.js | 7 +- packages/techdocs-cli/.eslintrc.js | 11 +-- .../techdocs-cli/src/commands/serve/serve.ts | 1 + plugins/scaffolder-backend/.eslintrc.js | 65 +++------------ 7 files changed, 22 insertions(+), 157 deletions(-) diff --git a/packages/app/.eslintrc.js b/packages/app/.eslintrc.js index 7441f46811..e2a53a6ad2 100644 --- a/packages/app/.eslintrc.js +++ b/packages/app/.eslintrc.js @@ -1,10 +1 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { - overrides: [ - { - files: ['**/*.ts?(x)'], - rules: { - 'react/prop-types': 1, - }, - }, - ], -}); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/core-app-api/.eslintrc.js b/packages/core-app-api/.eslintrc.js index 3c32e018c8..e358722664 100644 --- a/packages/core-app-api/.eslintrc.js +++ b/packages/core-app-api/.eslintrc.js @@ -1,6 +1,5 @@ module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { - 'react/prop-types': 0, 'jest/expect-expect': 0, }, }); diff --git a/packages/core-components/.eslintrc.js b/packages/core-components/.eslintrc.js index 65bfa69f59..2ebb50e64f 100644 --- a/packages/core-components/.eslintrc.js +++ b/packages/core-components/.eslintrc.js @@ -1,82 +1,11 @@ module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { - 'react/prop-types': 0, 'jest/expect-expect': 0, - 'no-restricted-imports': [ - 2, - { - paths: [ - { - name: '@material-ui/core', - message: "Please import '@material-ui/core/...' instead.", - }, - { - name: '@material-ui/icons', - message: "Please import '@material-ui/icons/' instead.", - }, - { - name: '@material-ui/icons/', - message: "Please import '@material-ui/icons/' instead.", - }, - '_http_agent', - '_http_client', - '_http_common', - '_http_incoming', - '_http_outgoing', - '_http_server', - '_stream_duplex', - '_stream_passthrough', - '_stream_readable', - '_stream_transform', - '_stream_wrap', - '_stream_writable', - '_tls_common', - '_tls_wrap', - 'assert', - 'async_hooks', - 'buffer', - 'child_process', - 'cluster', - 'console', - 'constants', - 'crypto', - 'dgram', - 'diagnostics_channel', - 'dns', - 'domain', - 'events', - 'fs', - 'fs/promises', - 'http', - 'http2', - 'https', - 'inspector', - 'module', - 'net', - 'os', - 'path', - 'perf_hooks', - 'process', - 'punycode', - 'querystring', - 'readline', - 'repl', - 'stream', - 'string_decoder', - 'sys', - 'timers', - 'tls', - 'trace_events', - 'tty', - 'url', - 'util', - 'v8', - 'vm', - 'worker_threads', - 'zlib', - ], - patterns: ['**/../../**/*/src/**', '**/../../**/*/src'], - }, - ], }, + restrictedImports: [ + { + name: '@material-ui/core', + message: "Please import '@material-ui/core/...' instead.", + }, + ], }); diff --git a/packages/core-plugin-api/.eslintrc.js b/packages/core-plugin-api/.eslintrc.js index 3c32e018c8..e2a53a6ad2 100644 --- a/packages/core-plugin-api/.eslintrc.js +++ b/packages/core-plugin-api/.eslintrc.js @@ -1,6 +1 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { - rules: { - 'react/prop-types': 0, - 'jest/expect-expect': 0, - }, -}); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/techdocs-cli/.eslintrc.js b/packages/techdocs-cli/.eslintrc.js index 8b5adc6794..e2a53a6ad2 100644 --- a/packages/techdocs-cli/.eslintrc.js +++ b/packages/techdocs-cli/.eslintrc.js @@ -1,10 +1 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { - overrides: [ - { - files: ['**/*.ts?(x)'], - rules: { - 'no-restricted-imports': 0, - }, - }, - ], -}); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/techdocs-cli/src/commands/serve/serve.ts b/packages/techdocs-cli/src/commands/serve/serve.ts index a6a671c2df..d5b8191cf9 100644 --- a/packages/techdocs-cli/src/commands/serve/serve.ts +++ b/packages/techdocs-cli/src/commands/serve/serve.ts @@ -37,6 +37,7 @@ function findPreviewBundlePath(): string { // This can be tested by running `yarn pack` and extracting the resulting tarball into a directory. // Within the extracted directory, run `npm install --only=prod`. // Once that's done you can test the CLI in any directory using `node /package `. + // eslint-disable-next-line no-restricted-syntax return findPaths(__dirname).resolveOwn('dist/embedded-app'); } } diff --git a/plugins/scaffolder-backend/.eslintrc.js b/plugins/scaffolder-backend/.eslintrc.js index d32253d562..953af54f90 100644 --- a/plugins/scaffolder-backend/.eslintrc.js +++ b/plugins/scaffolder-backend/.eslintrc.js @@ -1,59 +1,18 @@ module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { ignorePatterns: ['sample-templates/'], - rules: { - 'no-console': 0, - 'new-cap': [ - 'error', - { - capIsNew: false, - }, - ], - 'no-restricted-imports': [ - 'error', - { - paths: [ - { - name: 'path', - importNames: ['resolve'], - message: - 'Do not use path.resolve, use `resolveSafeChildPath` from `@backstage/backend-common` instead as it prevents security issues', - }, - ], - patterns: ['**/../../**/*/src/**', '**/../../**/*/src'], - }, - ], - 'no-restricted-syntax': [ - 'error', - { - message: - 'Default import from winston is not allowed, import `* as winston` instead.', - selector: - 'ImportDeclaration[source.value="winston"] ImportDefaultSpecifier', - }, - { - message: - "`__dirname` doesn't refer to the same dir in production builds, try `resolvePackagePath()` from `@backstage/backend-common` instead.", - selector: 'Identifier[name="__dirname"]', - }, - { - message: - 'Do not use path.resolve, use `resolveSafeChildPath` from `@backstage/backend-common` instead as it prevents security issues', - selector: - 'MemberExpression[object.name="path"][property.name="resolve"]', - }, - ], - }, - overrides: [ + restrictedSrcImports: [ { - files: ['*.test.*', 'src/setupTests.*', 'dev/**'], - rules: { - 'no-restricted-imports': [ - 2, - { - patterns: ['**/../../**/*/src/**', '**/../../**/*/src'], - }, - ], - }, + name: 'path', + importNames: ['resolve'], + message: + 'Do not use path.resolve, use `resolveSafeChildPath` from `@backstage/backend-common` instead as it prevents security issues', + }, + ], + restrictedSrcSyntax: [ + { + message: + 'Do not use path.resolve, use `resolveSafeChildPath` from `@backstage/backend-common` instead as it prevents security issues', + selector: 'MemberExpression[object.name="path"][property.name="resolve"]', }, ], }); From 44cc7c3b955af83106d74765beaf75c585a6bb2d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 7 Mar 2022 14:57:59 +0100 Subject: [PATCH 297/353] changesets: added changeset for CLI lint config refactor Signed-off-by: Patrik Oldsberg --- .changeset/gentle-dancers-greet.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/gentle-dancers-greet.md diff --git a/.changeset/gentle-dancers-greet.md b/.changeset/gentle-dancers-greet.md new file mode 100644 index 0000000000..bf160a7099 --- /dev/null +++ b/.changeset/gentle-dancers-greet.md @@ -0,0 +1,7 @@ +--- +'@backstage/cli': patch +--- + +Added a new ESLint configuration setup for packages, which utilizes package roles to generate the correct configuration. The new configuration is available at `@backstage/cli/config/eslint-factory`. + +Introduced a new `backstage-cli migrate package-lint-configs` command, which migrates old lint configurations to use `@backstage/cli/config/eslint-factory`. From 74663277fb371b01e989e08bc33a352b93ac2930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 7 Mar 2022 15:32:13 +0100 Subject: [PATCH 298/353] rename to createScheduledTaskRunner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/new-books-protect.md | 2 +- docs/integrations/ldap/org.md | 2 +- packages/backend-tasks/api-report.md | 4 ++-- .../src/tasks/PluginTaskSchedulerImpl.test.ts | 4 ++-- .../src/tasks/PluginTaskSchedulerImpl.ts | 4 ++-- packages/backend-tasks/src/tasks/index.ts | 2 +- packages/backend-tasks/src/tasks/types.ts | 7 ++++--- plugins/catalog-backend-module-ldap/api-report.md | 4 ++-- .../src/processors/LdapOrgEntityProvider.ts | 15 ++++++++++----- 9 files changed, 25 insertions(+), 19 deletions(-) diff --git a/.changeset/new-books-protect.md b/.changeset/new-books-protect.md index 2a561b5e5c..568710ccd0 100644 --- a/.changeset/new-books-protect.md +++ b/.changeset/new-books-protect.md @@ -21,7 +21,7 @@ All things said, a typical setup might now look as follows: + id: 'our-ldap-master', + target: 'ldaps://ds.example.net', + logger: env.logger, -+ schedule: env.scheduler.createTaskSchedule({ ++ schedule: env.scheduler.createScheduledTaskRunner({ + frequency: Duration.fromObject({ minutes: 60 }), + timeout: Duration.fromObject({ minutes: 15 }), + }), diff --git a/docs/integrations/ldap/org.md b/docs/integrations/ldap/org.md index b0fa2ca76d..f7f4e25b1a 100644 --- a/docs/integrations/ldap/org.md +++ b/docs/integrations/ldap/org.md @@ -49,7 +49,7 @@ schedule it: + id: 'our-ldap-master', + target: 'ldaps://ds.example.net', + logger: env.logger, -+ schedule: env.scheduler.createTaskSchedule({ ++ schedule: env.scheduler.createScheduledTaskRunner({ + frequency: Duration.fromObject({ minutes: 60 }), + timeout: Duration.fromObject({ minutes: 15 }), + }), diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md index db4dee0c65..98199a55b0 100644 --- a/packages/backend-tasks/api-report.md +++ b/packages/backend-tasks/api-report.md @@ -11,7 +11,7 @@ import { Logger } from 'winston'; // @public export interface PluginTaskScheduler { - createTaskSchedule(schedule: TaskScheduleDefinition): TaskSchedule; + createScheduledTaskRunner(schedule: TaskScheduleDefinition): TaskRunner; scheduleTask( task: TaskScheduleDefinition & TaskInvocationDefinition, ): Promise; @@ -30,7 +30,7 @@ export interface TaskInvocationDefinition { } // @public -export interface TaskSchedule { +export interface TaskRunner { run(task: TaskInvocationDefinition): Promise; } diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts index 0a811a5b88..993041bed4 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts @@ -62,7 +62,7 @@ describe('PluginTaskManagerImpl', () => { // This is just to test the wrapper code; most of the actual tests are in // TaskWorker.test.ts - describe('createTaskSchedule', () => { + describe('createScheduledTaskRunner', () => { it.each(databases.eachSupportedId())( 'can run the happy path, %p', async databaseId => { @@ -70,7 +70,7 @@ describe('PluginTaskManagerImpl', () => { const fn = jest.fn(); await manager - .createTaskSchedule({ + .createScheduledTaskRunner({ timeout: Duration.fromMillis(5000), frequency: Duration.fromMillis(5000), }) diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts index b410e4d3b5..8d50298ffd 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts @@ -20,7 +20,7 @@ import { TaskWorker } from './TaskWorker'; import { PluginTaskScheduler, TaskInvocationDefinition, - TaskSchedule, + TaskRunner, TaskScheduleDefinition, } from './types'; import { validateId } from './util'; @@ -55,7 +55,7 @@ export class PluginTaskSchedulerImpl implements PluginTaskScheduler { ); } - createTaskSchedule(schedule: TaskScheduleDefinition): TaskSchedule { + createScheduledTaskRunner(schedule: TaskScheduleDefinition): TaskRunner { return { run: async task => { await this.scheduleTask({ ...task, ...schedule }); diff --git a/packages/backend-tasks/src/tasks/index.ts b/packages/backend-tasks/src/tasks/index.ts index d925f089e8..f6695a8d73 100644 --- a/packages/backend-tasks/src/tasks/index.ts +++ b/packages/backend-tasks/src/tasks/index.ts @@ -19,6 +19,6 @@ export type { PluginTaskScheduler, TaskFunction, TaskInvocationDefinition, - TaskSchedule, + TaskRunner, TaskScheduleDefinition, } from './types'; diff --git a/packages/backend-tasks/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts index 3e46acf55e..35b5598700 100644 --- a/packages/backend-tasks/src/tasks/types.ts +++ b/packages/backend-tasks/src/tasks/types.ts @@ -103,7 +103,7 @@ export interface TaskInvocationDefinition { * * @public */ -export interface TaskSchedule { +export interface TaskRunner { /** * Takes the schedule and executes an actual task using it. * @@ -136,7 +136,8 @@ export interface PluginTaskScheduler { ): Promise; /** - * Creates a task schedule, ready to be invoked at a later time. + * Creates a scheduled but dormant recurring task, ready to be launched at a + * later time. * * @remarks * @@ -146,7 +147,7 @@ export interface PluginTaskScheduler { * * @param schedule - The task schedule */ - createTaskSchedule(schedule: TaskScheduleDefinition): TaskSchedule; + createScheduledTaskRunner(schedule: TaskScheduleDefinition): TaskRunner; } function isValidOptionalDurationString(d: string | undefined): boolean { diff --git a/plugins/catalog-backend-module-ldap/api-report.md b/plugins/catalog-backend-module-ldap/api-report.md index 25f479f73c..edeef4cb82 100644 --- a/plugins/catalog-backend-module-ldap/api-report.md +++ b/plugins/catalog-backend-module-ldap/api-report.md @@ -15,7 +15,7 @@ import { LocationSpec } from '@backstage/plugin-catalog-backend'; import { Logger } from 'winston'; import { SearchEntry } from 'ldapjs'; import { SearchOptions } from 'ldapjs'; -import { TaskSchedule } from '@backstage/backend-tasks'; +import { TaskRunner } from '@backstage/backend-tasks'; import { UserEntity } from '@backstage/catalog-model'; // @public @@ -119,7 +119,7 @@ export interface LdapOrgEntityProviderOptions { groupTransformer?: GroupTransformer; id: string; logger: Logger; - schedule: 'manual' | TaskSchedule; + schedule: 'manual' | TaskRunner; target: string; userTransformer?: UserTransformer; } diff --git a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts index 9c483c7b94..f7d07a15af 100644 --- a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { TaskSchedule } from '@backstage/backend-tasks'; +import { TaskRunner } from '@backstage/backend-tasks'; import { ANNOTATION_LOCATION, ANNOTATION_ORIGIN_LOCATION, @@ -69,11 +69,16 @@ export interface LdapOrgEntityProviderOptions { /** * The refresh schedule to use. * - * If you pass in 'manual', you are responsible for calling the `read` - * method manually at some interval. If not, it will be automatically - * called regularly with the given schedule using the scheduler. + * @remarks + * + * If you pass in 'manual', you are responsible for calling the `read` method + * manually at some interval. + * + * But more commonly you will pass in the result of + * {@link @backstage/backend-tasks#PluginTaskScheduler.createScheduledTaskRunner} + * to enable automatic scheduling of tasks. */ - schedule: 'manual' | TaskSchedule; + schedule: 'manual' | TaskRunner; /** * The function that transforms a user entry in LDAP to an entity. From bd87e805c00b0f5bd362d0244697552bc9efb0e3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 5 Mar 2022 18:21:27 +0100 Subject: [PATCH 299/353] techdocs-cli: tweak e2e test setup Signed-off-by: Patrik Oldsberg --- packages/techdocs-cli/e2e-test.config.js | 22 +++++++++++++++++++ .../techdocs-cli.test.ts} | 2 +- packages/techdocs-cli/package.json | 6 ++--- 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 packages/techdocs-cli/e2e-test.config.js rename packages/techdocs-cli/{src/e2e.test.ts => e2e-tests/techdocs-cli.test.ts} (98%) diff --git a/packages/techdocs-cli/e2e-test.config.js b/packages/techdocs-cli/e2e-test.config.js new file mode 100644 index 0000000000..2fa92462ab --- /dev/null +++ b/packages/techdocs-cli/e2e-test.config.js @@ -0,0 +1,22 @@ +/* + * Copyright 2022 The Backstage Authors + * + * 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. + */ + +const path = require('path'); + +module.exports = require('@backstage/cli/config/jest').then(baseConfig => ({ + ...baseConfig, + rootDir: path.resolve(__dirname, 'e2e-tests'), +})); diff --git a/packages/techdocs-cli/src/e2e.test.ts b/packages/techdocs-cli/e2e-tests/techdocs-cli.test.ts similarity index 98% rename from packages/techdocs-cli/src/e2e.test.ts rename to packages/techdocs-cli/e2e-tests/techdocs-cli.test.ts index 7cdd867d0f..d9d736cb6d 100644 --- a/packages/techdocs-cli/src/e2e.test.ts +++ b/packages/techdocs-cli/e2e-tests/techdocs-cli.test.ts @@ -58,7 +58,7 @@ const timeout = 25000; jest.setTimeout(timeout * 2); describe('end-to-end', () => { - const cwd = path.resolve(__dirname, 'example-docs'); + const cwd = path.resolve(__dirname, '../src/example-docs'); afterEach(async () => { // On Windows the pid of a spawned process may be wrong diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index e6cfab0916..a6c7ada37a 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -27,9 +27,9 @@ "build": "backstage-cli package build", "clean": "backstage-cli package clean", "lint": "backstage-cli package lint", - "test": "backstage-cli package test --testPathIgnorePatterns=src/e2e.test.ts", - "test:e2e": "backstage-cli test src/e2e.test.ts", - "test:e2e:ci": "backstage-cli test --watchAll=false --ci src/e2e.test.ts", + "test": "backstage-cli package test", + "test:e2e": "backstage-cli test --config e2e-test.config.js", + "test:e2e:ci": "backstage-cli test --config e2e-test.config.js --watchAll=false --ci", "test:cypress": "cypress open", "prepack": "./scripts/prepack.sh" }, From 95667624c16c6b8e9f4b585c1eb74fcad1dc4ea3 Mon Sep 17 00:00:00 2001 From: Alisa Wong Date: Mon, 7 Mar 2022 10:25:50 -0500 Subject: [PATCH 300/353] create changeset Signed-off-by: Alisa Wong --- .changeset/shiny-eels-mix.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shiny-eels-mix.md diff --git a/.changeset/shiny-eels-mix.md b/.changeset/shiny-eels-mix.md new file mode 100644 index 0000000000..2d886e9a7f --- /dev/null +++ b/.changeset/shiny-eels-mix.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Add names to sidebar sub menu styles for customization From 0f93aeaf8f980cf6cde4a3370962748ea19b8181 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Mon, 7 Mar 2022 10:26:13 -0600 Subject: [PATCH 301/353] Refactored based on feedback Signed-off-by: Andre Wanlin --- .../CatalogKindHeader.test.tsx | 4 ++-- .../CatalogKindHeader/CatalogKindHeader.tsx | 17 +++++++---------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx index 7783278eda..a7dc18eae2 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx @@ -158,7 +158,7 @@ describe('', () => { @@ -166,7 +166,7 @@ describe('', () => { , ); expect(updateFilters).toHaveBeenLastCalledWith({ - kind: new EntityKindFilter('templates'), + kind: new EntityKindFilter('template'), }); }); }); diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx index 5124f95086..27184bafef 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx @@ -59,20 +59,17 @@ export function CatalogKindHeader(props: CatalogKindHeaderProps) { .getEntityFacets({ facets: ['kind'] }) .then(response => response.facets.kind?.map(f => f.value).sort() || []); }); - const { updateFilters, queryParameters } = useEntityList(); + const { + updateFilters, + queryParameters: { kind: kindParameter }, + } = useEntityList(); const queryParamKind = useMemo( - () => - ([queryParameters.kind].flat()[0] ?? initialFilter).toLocaleLowerCase( - 'en-US', - ), - [initialFilter, queryParameters], + () => [kindParameter].flat()[0], + [kindParameter], ); - const [selectedKind, setSelectedKind] = useState( - ([queryParameters.kind].flat()[0] ?? initialFilter).toLocaleLowerCase( - 'en-US', - ), + queryParamKind ?? initialFilter, ); useEffect(() => { From 9a06d183851b450f8874728da903eb29124152e2 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Thu, 3 Mar 2022 10:54:07 -0700 Subject: [PATCH 302/353] Add allowedKinds option to CatalogKindHeader Signed-off-by: Tim Hansen --- .changeset/gorgeous-trains-clap.md | 5 +++ plugins/catalog/api-report.md | 2 +- .../CatalogKindHeader.test.tsx | 43 +++++++++++++++++++ .../CatalogKindHeader/CatalogKindHeader.tsx | 31 +++++++++---- 4 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 .changeset/gorgeous-trains-clap.md diff --git a/.changeset/gorgeous-trains-clap.md b/.changeset/gorgeous-trains-clap.md new file mode 100644 index 0000000000..d0abf6148a --- /dev/null +++ b/.changeset/gorgeous-trains-clap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Added an `allowedKinds` option to `CatalogKindHeader` to limit entity kinds available in the dropdown. diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 3223d6aa59..43cde23c57 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -74,7 +74,7 @@ export function CatalogKindHeader(props: CatalogKindHeaderProps): JSX.Element; // @public export interface CatalogKindHeaderProps { - // (undocumented) + allowedKinds?: string[]; initialFilter?: string; } diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx index a7dc18eae2..15d41f3ab3 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx @@ -169,4 +169,47 @@ describe('', () => { kind: new EntityKindFilter('template'), }); }); + + it('limits kinds when allowedKinds is set', async () => { + const rendered = await renderWithEffects( + + + + + , + ); + + const input = rendered.getByText('Components'); + fireEvent.mouseDown(input); + + expect( + rendered.getByRole('option', { name: 'Components' }), + ).toBeInTheDocument(); + expect( + rendered.getByRole('option', { name: 'Systems' }), + ).toBeInTheDocument(); + expect( + rendered.queryByRole('option', { name: 'Templates' }), + ).not.toBeInTheDocument(); + }); + + it('renders kind from the query parameter even when not in allowedKinds', async () => { + const rendered = await renderWithEffects( + + + + + , + ); + + expect(rendered.getByText('Frobs')).toBeInTheDocument(); + const input = rendered.getByText('Frobs'); + fireEvent.mouseDown(input); + + expect( + rendered.getByRole('option', { name: 'Systems' }), + ).toBeInTheDocument(); + }); }); diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx index 27184bafef..5b87919f45 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx @@ -46,12 +46,21 @@ const useStyles = makeStyles((theme: Theme) => * @public */ export interface CatalogKindHeaderProps { + /** + * Entity kinds to show in the dropdown; by default all kinds are fetched from the catalog and + * displayed. + */ + allowedKinds?: string[]; + /** + * The initial kind to select; defaults to 'component'. A kind filter entered directly in the + * query parameter will override this value. + */ initialFilter?: string; } /** @public */ export function CatalogKindHeader(props: CatalogKindHeaderProps) { - const { initialFilter = 'component' } = props; + const { initialFilter = 'component', allowedKinds } = props; const classes = useStyles(); const catalogApi = useApi(catalogApiRef); const { value: allKinds } = useAsync(async () => { @@ -91,13 +100,19 @@ export function CatalogKindHeader(props: CatalogKindHeaderProps) { // enforced casing from the catalog-backend. This makes a key/value record for the Select options, // including selectedKind if it's unknown - but allows the selectedKind to get clobbered by the // more proper catalog kind if it exists. - const options = [capitalize(selectedKind)] - .concat(allKinds ?? []) - .sort() - .reduce((acc, kind) => { - acc[kind.toLocaleLowerCase('en-US')] = kind; - return acc; - }, {} as Record); + const availableKinds = [capitalize(selectedKind)].concat( + allKinds?.filter(k => + allowedKinds + ? allowedKinds.some( + a => a.toLocaleLowerCase('en-US') === k.toLocaleLowerCase('en-US'), + ) + : true, + ) ?? [], + ); + const options = availableKinds.sort().reduce((acc, kind) => { + acc[kind.toLocaleLowerCase('en-US')] = kind; + return acc; + }, {} as Record); return (

drzRn1r+`CPe)vnwbYdZaIoFpG1@)Giq$;yQ9xeG1t?}vP`hS9?V~L{P zLrqHOP9eW|o+S91#jbR8C$+a%60o3Fpqr+W7UYlp_{eP+^EvG#?% zrQ|`#35it&cCm&3qu8EBv47a-obZ|copm$HE^%8j;s5J*_Fn>yPdHqYL8MnX0A&s- zO2i~Y;3^j#MOa`gfjJc+c9lml#vv<=n2K?^%4ZX zUL!QexHKEnzkvivi;M_M|5I$K{jOgK@RsUyrO(&5T$e9!Fw-pJ_BSupS9qOn>0hTW zy#94v=}%Zjb<2f(d*fae_KwGhq!rXhAyk+iQfkJoxHzoi@xrpL%tCl%aXjW-Y|HO5 zD>=ob>7%%+C_f&3+EUqBF`@b@zj8a>4A4@IM?J#5+}>4JcctH>vE%yGE;NHHDgCBt zP`Lu-5}mTRD|XvF8GY89&b@l1^Utw;m!Rics=MXW*t)S>;pIqc@GB;zaZmZR&jRhr zuNu#$Gy8Mz-N@XNrQ7zqu}WEZ>H6P?+YYoCY^!714Gh}5PFx|G@0;%p0i`MJ=hzj& zuO3$cig(@Q{ud!kqnm`jR6WlC)uGW|rC1sN6x;FF0pCWqC|aB9xc#f6ES0v$9khCw z`<)@2bX)YEA_E^7oMXdyc9@S-2jzrZUsxdW*hBVupy7$8%i%szRc=N-C z`Vk`tcYKP{-pfCCJ(hr)G(0cUvB%RfsCDg+&67Qm8SinAao5b-C`*a;`w1U|+IVc# zs+m*@^_VG9g>w&`z9^!L5awT3u*!d^@a8A9*fN-yD;>C*eyi{=xU;l$|=~UlR zzHA}wOE(n^@XnR-KiFdKCe(9kY~LMo@oR0}{pEi@tU=_OKB2eAJJ390$>560TK=#v zvEV&n%XgK1{^LOMi`aCEziXjz=HVxH9|{mr2yc|$7z;{dzAiVT z_B3qX@i4#q_qwsNd2w6WcWvMQrlV5)EbQfYM@Q3Xyuo;!w|E%9i-1a^9_e@yd-H-Kt7<>8mCf0CwC${5Zf8y_DD+=}d`1QxX z=du2OX97^y#R0!4!I#C@^PcWVEf+kv`?FN{9&?|G0d0F2R;#BcMXD#&od z(Q68Q5DNN&t53fV#({%pZXpXeVc=bWF0teNtrz#5-;*5pFD2L)7Yr5wW84MYdxrVJ z0|HW^sA8x;5E?TW7H#FP*=R|?5#~S@mI?(Y-^m)MK$2xVXzx&gc~n6KA`xbw2)I{Z zZt8c}Fh~|$Tjeey`x?M05-bVz@a71ShX;!`MK+{H{-lcP5Q*}!3huFr8iYr-S@{n^ zy?{+2HD20t*Ax4U0-!?-Sb}34p#h8;or@1b_oyIm2BQCH#(d>~oB<#g z9FTo4XPqi+;|-@(cyvp#Qw1nGuRh!k3AP7?r+I}d7x}s4LZ2wHOeO(DLk_yC?pBNd z7!(@m6&Gg}2TR441OqH{V6pJH4NKc2j`&b7D7$sQIeWMrF@T*Dzq!*`(3W>9g+?E9Bx5!vy-2|{;g3^ajJ8*`mGXvszfDLak952LTF>FV4M_w} z`t1Ocb)AwV!PqTA4jy;5ki(SG!KfC9f2)wgHU*Xno)47MO%euB$iWPiPip{RC8VXo z4pSi1u9Ex!4xBVPZ`*M1)Ev%q|K_x)aNF!fzgp_Fh&1d5Yg_xAn1-}ed8ee$@DSaN z@1A(yJhXp=y$H2WbKI59SoXpOf&pYh4yO&+OM{6)-q!tzQ zb`du1jXmw1q>JWgIF$7+4BMU`@I@8n06NTCA966MO0;wW{A$4Fwazr|%)GZqcj(O0 zBnFs%%53s*UiOaNqz34s&KP}_DN89O+nZJV0#!8;#Y9+sY(A0ip%0S`NpbFhb{ZQo!vQKwq-$L&G@B-E*7 z;aLRt$=iFd_;4({s_fCG0uva(9#&v#l-p|RP*15N02yb7Zf9Y=MUqh( zrX3g{?}V+!h2_Qtut@{B!84pnuobl(n3f7#hZDuB3Kcp_d#rOGEJ~$dm{W@YF-9!; zhmvV(tNQY?<;a>jDnf6XOdi;e} z#A&Z-u$pzk=tK+j_~QsTGkIE)qr)=-IMae?;@*r@=`mvcTdK49RK#OhDyZuq^(pa1 zc*eRyjYw%RqZq1|kFxM1ZA}7aX8urJAXldDMTQ9&+d(W)iZ(4UJc;nWx>DO#50?1r zsDk-VvNso2LlKrkXI@7dR;NhtB&a%DrrJ$WZovwIr3Cdor2fXwuOGoy$bkopBrbUuo^dwoAHf`QFctVKj4oPv@U5$s z3hfok;M_?r@;^MEeG_TPM=BzU)7y${L~u*u?}}Yy)9dBYa$bbnzIv9;{vH6TN|uKK zsCn$RrT3isjq3$fSdqQu1pAb*Qbl(?eIiSt=NeBZqCCLp$No# z`JRQ+wzp*;CA8Z@UL?ACmN^ZVzFKb4fmPWtrd=K-28w2dWB}?RiCbxopUPZT@f}}A zV4HRVURaeKAzk!olQT20nukkeqp+5S3Y9)&Tj#hQrTyERWo29b`Fk#5bG8P1Y6y3& zbW{WVkVF6bIIi?wzm@iry~IC0g`ZUWvPxnU@mfF9Re!Mi!A3R2mz3gl;IeyzCH$-~ z@E#R+z+D(129W1(*b29G7X$d>#c?;LoLUSk4G%XY7wogni~#6;Dt6@jF`Qmgg~0BxE@odD_WO|-HC%T#KOo$iW1VBWJKtqz zTM~dM<A5BxeM!|A7OayN@{9b z5hlaq+;;N(g;uQK@|?5sg2U-Q)!IsSwGBA7pwPAews%3$%m&XQ5TIoHe?MN7G}`Su znl2)tt1*94og(~_|7h0z)f`A0cGwni6q8GK_6`(KKz`pDFKCkr4y$eeg&d47W#M>g z(J#w_+-y@KKd~=2jrq~z&ruL0*mVL{IG0tFm#h3jTz!|m3r24Ttpp`DCh#sEG&xPb zkNripdI?-5!2!IsANe~U9rJy4ehf*8Gdp_vH6XJ`-oEEZfA-+1jWI3m4IOHJ(|2KG z1jyXA5cb`d4eh+O?m zvt}f+7CXZaUZU;S?OShDw%@JlFneC-eKR%f8S?3P>FA!pJmJ6 zK|FYWrb2(a9ljNIwgnyE5*|N@bU28qIEd%l<`Z#%Q5=XCtPBUE4h66}lI(XR8IE!! zj^z1{3RRBYi2ZQn|G(Hf3!u2VE#EiZSdajLh6D|60fIY(;1CiB!9xfhAS59KcXxMp zcbdlC-QC^Y>DNg<$#>`8nLAT&UcH&R)TyFq&faIAt-b$yt=|%idn9gFw@}o#R$Q=F z`Lst(C(XUxvZ`(yGPix$wq3us{gP&N86r;B-aKyY`^ zVRtNHr?D?cX>Rw$-EJZC_R=G@{5jF9;Ox0x|E+|*ow~ifxxItCy`x9_Corv6YyZMw z|0-esrf&ZZrr4VAW2o=3A#NZl2=v4rpw%B>%pcr?9$=Fm;tCz&X&(|e9ug)VVzF?e z%!?q;ACh6AZ<8E7JXtZ3-!9$TT?{zF88KkSGIYo=U|~6C6*_+6c+8o2{M7M?zW;b( z@0cO*?ML||ac$^H`01fU;)yhtc#PnQ4D>`%TSWwVY_4@mr*QgO=u{2sM8o`adG1&% zP@j+FR8RYi+wDX@@ywW1z_9+z40<;D?&vMn*_-*(FGA<4(9>`IM|KM5Hub00^P2Kl z=Wf~;qr+z&i5EVkPvqvkeW4dYP(CNe^DyYSmHDN!sM zts6&r;#E!{wO9RB0rZL;Hz%R+GG6;S&ha`?=sLRoI@$cXYX4eL=%PsIro~*8U+bnL z@#b*w!ZrV<4|;QScnwy#9`3(Ra=eAiUyl~v4$a?=2r;#d-XEd8SzWnU!dhH$yo*h^ zTQ6@DPFbM)q|hrN=ouE!+{MM%)x+Jx4Ms)q@(%FyH4kzP^9u}# z2#t>OkB>@7PD%|k>T=2&tLiIjn%bHhI$K++ zduzKohWgt_y88ylh9_rwW+&zr7naLIyp@eNwl;UR_jV8Vj}A|c&rUDSuP$${?`{E5 zR18TN3&915MK9Z7k=*QoO)3pDSx9O1A!IS`>G+u19zZ7K^8<wL&bx$nY$Dt7;_RRixAzS!|^kuaDsUpF+%Elbo!|4jKqVe?Q^uxI?ij_unD^C7p%@6*S$&*o|4OruSyN@P-jYll84w4t-0n#<39PfXIp3Nu^z84et@(Pq zFzEYQ;qK;J#KCfRqC$P$?e)d+_I!VRJroK+rd{|0im>4FQ?QkwU39}!FGXmnA7!st%Ic1GVT$eMdZH6D{U0{A-%V^ew>DCPBem)1xR<*E7t9 zCh0OQY(Vr`mLV~#+4?D<%$$^ntIgchrY+;VvOZ&m+>DHz?VRcgpWM9qG0;wat-kT^ zPz28Hozek`KcEQuxw{oJR@=LkEl2bPg|%zE1yw5v=%!WMISh=|`w(I?$VrNcdG+a% zt$EGmQT2We^d19i4PYA`y4U!pjqP{EwgnGHMfeUyIBc6omOW@Va+xu2yaQ<*by8(A zmb8;Q?pSuS2G*2x($_PV_A<_sz-(-X`AOP^QJ7A?E4C+tk~H}zL$X}EC&P-*nVvcc zr1F=Ja$mJCvzn+`v`fY*fFn_RF+kJ=b@|_QUzC^?LsKoZb2E z`MeVf^Th%IM3Zyct3Li>(TA(=V%exU-)bgI!K`W}isez&nuqxwcs=Mo^KT}$S(ydD zo7ig4T<(M$-0|#2e$uMmtD3K?-meu4sNQXYYS|qm(Fob?z@P{)!S*=|^!5i8;dH`) z^zf+ReqqhwJZ1mw;ani8!{tJ3p~KaV@4VpEo@@T?&B?eq^!8%C5PElWz7K_pscYXX zJQtqBFS13JC4+w;DTu@wOLPAUrr2VnV0%ioP=F4-BxRR;%~YGIVJ5ab zpLOw)!D4hO!9E-7p+Kk=%*OWg;<>cMP{@}aDypYE zo%}W-f!L;Lw9lowzC#hd+t|LotdVyK35}XbW7K@psW5gFUh4OliN#1pYe~s04sT7bR5+oo56-qE~9!j6kR`(&XE``W6W6^Gg$URv881?j$OdW?UVwlm225la)Tb4Xp=Hb)^l2wMD`}>ephU5&PtOo^9>)0zk%pilm!Un z8&jI7jb)6LN4f9DNyto3O`TPMd-KgkeWoTwwnJhf3r{)PN%iBI|nMHQbto;yd z3uwI=W|r$Xt0tZmSQS=l&RcPTC*1=+x68~O#hlk{$gx1Ph_&`D&ujPmS*+{3wXW1& z*9E>RbU5$+DcEwV*+XG6Ej$)s{W%k>OK$hV89mq=E}+PbMs^WR{h}$F_rUdO&m!Vi zaN`+Lv9o}z?y4?Kv6V0Oc@0x+Uv4zt^Z4lXPD2;uYu(7%cj)(Yed*ElMZ565k|1@) zWt@5I=BIPT!Cz|h9xqRHN^126Wy-G7e^+efO@k9>R+v9scE8YPi>f5ir(NgjHk2=o z8Sh!+G!N^skS~es=3nO~o$S-iI2NDnSr@#%?033%qP)tlFS>m>V9i;Yd|z|@nSshc zP(WE4johX*G;q+pt~B*&?4S?-1NoyESCUn>%5Qzzea8vcH|xUBO?n=iS;pUgxc)rzvN!y0 zH;M0dQ!x<6LgGEid0@XOQOP@0-uF3F{&rgjt9qinK(fH#c8gEJcBsGZOIe|2p{7ow zva{u1o_uuKyZ!JWY20r7iNe)T?A=M$*QgDi{;U4NxziwX`+3*+tEuU`GjP8^sg}@n z-?i3Ra-hJD!2EUM=G{eyLfuhhAmtoN;zd97_8?vRW)p!ls;9s1tg`=h&!ppW;eo?x zJL&DVZ~e`K)4Hqi{=2j4`pk7Kp_5ew`;*Yby9USlyYqhN9Xly>?19ty14l$|M-km1|BQurtYovmlD|*#j3cZWnx7Ckmj&qbL`8 zFw}*S){M&9nfk&759o@+?Rq-y!X4$x3wF(>b|t)UeWL2}6zC>K==Px9RWiy=8tf*v z<|<+6DsJtw2t!hdg`f6>uJ&9se9q+a`DVY)yu=$%ZvM^lcC4Q2`}Fb&(GXm z;o)9ks@@US-hflD09tQnLhpnL&sd;+6t_>Bs!xWhbWD^_T!&ZAg?GHQYbK#@3Ab-q zltezzr}DxF(&3W}^wxjr+d$~o#O?Pm%(rI4r`^l9me8lmTCe58Z-CH$h+C*_1L|9C z=-+AW-;?DJ;`E=p@LwPdaDC}N9p%5~<-egCusPumv##B{2sog9y22gUEg9GW4mhR# zb_fo3!#k;u~`bnM-5@-2_Z!dW!e;=e-q02EtGqcoUtmDTq;C> zD3rqc0}oG__?s|^NizP+5RuJL>CP}V9*^fl;YvK=uO`EUH^XGUg{$#|z9Di^z6{qP zig*?srf?am;T>*R72Zb^VLBOMwi&258U8Uk!XP_B-&@H%I?|>pa+f>O$_N_%g(%8_ zE(*0R(&Jl{mz0ldRn*tXD5uV-mr+q6Jkep^UO{gn0ym?gs-i77t->#(lZayYt)t_k zqcb|AKTE|}QN`p<#uQ|`cIs$ypz#8w)`))B?U7=5mJ6W8`F zu9GG%^G$3|bWHPRoTO*m5KsJwlyw(sT)$N8fOmX1kHP3=`~p$J$FK3d+3{p-mMc~9n?#B0Qi(X{2^Z0cSJ@V4lZlhr38$BdVwH)AYDvglrgxL^a5+hU zsU%c25DF+6mp3`*Ea{joiLfh)pvwzyD*4e>vPnfUdRIJ^O)@AZnFAG?LJvt{lzvaK zl|*Zt@|Zr617g5rlgb^FigT924@!NCmP+Fj%gdW4u9mhnnkvGZN=ct8dzHHOE=>`X zuB4_enUf}LoUS~Tw!)RJF_o@0_2$i1njj=iHzrLjhF?1-!x)nB5ugO!eGgYwHC9%$%zsg&9xRt+rRB#bfc;&-!l2dS` zR=5etN2n@9R4+nC=eVUWLiH^`_svGRF2W`*o*gPW0~KMQ7vb@#<5m}wOc#%Z6qETD zQ*0NeZ5317me4ZLk;{}2To*D{m#hSru<@00Ob1iu7IE1Yv&5D%tCtF1mkRsRu;!L> zSC>ktm)#>Mlddk4MW+;-E)^#(SL7>KpDI(aEmyTAS5PljpDxirFM2~;NmV>?R4gAP&9)@j782$QiBN~+ zO+zknAl}%P|lgwA=ACN*MGynpCu?7JEI02vo z0Ps)a0<*h!u~Os8rhH51YOb+7mLwzAyfe1CD)!LZk7UQs{MWDg&hM}=&^%jG4!vaQ zBW1PVzT*ym%?SgR6VHD>r=E;s)Tekq%Z2p4Tn(^t9sHNeCHb>lL;w^3BRnjp0ytCv z072XuGIPh;C6!ytz1>?4)u9d0GfFVbDKOoxvtaE1{9)d@VBc;Z+K^-+$p{MUNVDuT z36V}Wkocu&v==uHcIn_el-8G$i^P6LhE%Q zQb$<6XCngu*O-31i!VnQ-aDIcWsDg2nZIYY@kw7Ug`pH&iOVb!0Ur3{(pNXkXAV8g z*gx6;2>`3U6LZmoJ!`6aGdaT{9f1SBlfLBt;*_&+M+qpRTlUUhayQsVtbSwm#mP?XE1G zuXjQ~em9MlewO%()IqNzj2b^9xr;=ogi0Tu{#a^v_02bUAaHs;EGNvUjsf|?g(Yj?yIRf zULQ!6N$lJ9*V|&tS8spx^4e;Txz-V-P^si%d*!8Z;gW#;ogO{w4Oh%7zkXYt3umPJwspYLFl3SFnxL(vm zvM&qos}w()u?cK%Kvpu0OCY)69Dw<8!x<64kL^wc5@!>a@O=dE)(ypQS4~0TQ2M5b z?<)QT5f#oN`@NWKfVU#qZ^Ib?U`&E_9BlaT9~}qqzi2l6@98*y4Cns~9Y?+va=JZH zYBJwfTYbL2(Cka9P*-z#yzwVo0jB(tUdCSGjFQ;gZ$)tMBm^h|ko7Q9+K9~=aTXjg zf)|}ZQ~--aH=s%~DL|PuYTzC~XQ2^WjKpif3q)P!hN7p#>IN@^S0si{Nuin|A{Imj zhkCX792r%n1ttU|9ICcrBg3rO z*ab=h?h73!DtDmPMY<4?)x}#lTv}G3-#J~?n$U}#hGkqCJl*y9AIdKs-6c)(DdNw` z2?+*BWwe9!F%Ebb=oGf%{;QAu(@Vzxt3LL3P8X!9-~fb-$sS}fcwQkO<(d~rfcx?r zNmAy3gEkLhEmd4kGRuv(lH|r&Z#~fiCSJHbM5B1?>#CZy^o+s7xuO6~iM8+ibk2 z;UR?+O^6}l0y4L8a`rP8J!y*))4|q@Ft&b^DJBnJL`L*Nu?iu|QIsP+>1A8=hPbh{ z@T!Z9q2Tofko!nwU?42mJG!j4G^Ks>0X) zYt6LyXV%RB=TqO`>6X8d!2i8NuPFi~WM#LBA!2hp*v^44s$+;)IgPu+2Vkw3;!BgA zECEp7054+$5WPk4xD(u#f@zVl)1X0I1Vk_Z+7MXJSKbAW(nl_Wb~WVDD^#Trx<^C} z5l9%tEdg3Kh}0nrHe_%BEXLrqNF8LSC0wc?5f>Dh9oF^8CsWaBWX9G`&S;K9-c;mz z;?DXh4@Kxw$>cKB^F;OG~9uxm2#{C@n&fGjbSSHHB7f7!?jF7>A)axA-Lqi!AGcR&=65JDZLOy z5YgihB^9aFa2fgN^l)|E$+QS;h4HG%=w6+CDlOyZpdr#!0LBTH{1vRCK zH-4itGQ7i#jqp8^*i4yV;uQ@obim$3ukM4;x;WP5>|8Y@@K9%>r||C4l;5inE{b%y zI)G-uxzi;)C_z5DZm_wbsTJ$*_vzku@IDwjf5}fb{53HUdi2F(h@ZUQm!*F(IaKL( ze`~pvpKhi-u>U;O&0I*JD;C~;_d3?|MEXT7pbx>;XB!cK2^Iuw6yDdPB}G^jsZ$ab zak;O7at*v_z^HiVkgA%5GVLt%B1puR66UJN=+%gqgP^iY{0vSr>mFiC5iUg0^M@DxxVuOzwUfA2iN&9}PHIPoQ*he)jg#i6+E3<)ehr%4pXrB>viTc4@g=!wsOLcZB zLzKdcPF^yi(sZj09Ywgryl4JUC8Lp`6j{IWp4C2D`Yl0dR6gR`6Wh+7x167%GhanD z=^EBNmh{6I%Twhn{swauDvezj(dVwB>$40wj+^H&;2q@Y`vNJAKX_%pKl#&DXur%r z@Q|n9{;D()Flr!tN%zB52+Cn7io-kL&Rdp@t!yY>)#=vqaNi>eKo-XxT~uQBWXgdtH%kRT#CPL z3x3Z1J^$Jk{BQaGrK*MR>9j%4NkO2ri0s2!^}@@CxkRRyv{)`talk9O2k}JE zt@s;WNv^UQR76wLy~RzDpraqlUWyrOA@cW89+{*`=-Kf0k9ULLQoz@K^GddK$%xu$49N8C5z?rQl;nc2?a><2*_!dY`n? zAvA@ctd$&-=M^cJCeID-spl479@zKAMah-riCVs7{aC-f#GnoxMeSflGo@ zkCusWP>ZQRWzjM-;mexVx;=rG|M*t-rne>9Y3J}MO5o68#|-%GVP&$ZHyc-u#O0C5 zr!kC%Hum*A_L|50`~%pJFcv?Fh-DSGbcsRP#qq>wi^K+AP|acuh`ri%1`+CDmycXB ziZo#(;gz@zXe{4%M*)21F^Cj9(cl!&MnC#8NJVyg#&3)mZ-u7^u>UG(OgpfOE3;E{ zI)BcX?1Vxnnbc73H80kNYkUxIzOwb}z~zPsjW6Ie|INTv_WQ4eQ2Ost&<9A~eg|^? zClvI5N;BG_dHSaW3|-!`p{!)CFC==YHUAxrEqhei&VM8}{?Lrh%k*&4 zRu#YQ3|6|51_HgTkH+#>z$G1MHfys?D-DP7inr%|@5`cb5QR?nACx&eBR=SHaJq%2O z&BiD)jSav!L`J5dr+xQP4e-&K*hD z6%D=(pG|T>1QAI*DJ`N4?ky6EE>*DDJ@q)}HOnV`s1J#KFAQnSzF1bSTTXs4Y{-S*V97$k|h}mi!r)R3-gg4uDR9!dsH5L7Z!bD*@tXQ7h0u8RTy(K?(%k zdSTK^ooV6n^4X8W)TKLDBeguqd z@9=5rM79$ur_@XeYWB7m3hKAec8Zz^rWuOcLBysdJ8<5 zuft*Rw?acV9CsPZ*@$aJBtC4w@55F0gwn3LN;ZUX|{RsGNDRHp3}X9{W> zE9OzEaF7^ZoXCNNEYXuAo8V*1#EBmss||fCm|(-;S3T z#v-hN)J<95igqb@n*`73@CGGHM ziQey0wB8#|!mKqACxu0Fx%-q%;BF|vKtJfOTNbw)mrU_oS_!2tJB{`+CilxOr2((9 zbXNat*5|6?i;uum8z7~9CIWFzzOJewN+|? zfpV^PyU19b$CskT33$zHdes&O*FwcncQWYF_~(C2 z9~{lmA^_xTGC$Q}m^D1HD$dE@V+*Ms?^si*?q2wy-^ho#5b^*tjxYYX|Jtw9<8AhD zMj$_qM8sinD@_q#!7u-HB=S$3*ZvRDp8q3KSCk0_M?z~Ms4%#NWTeKDY0wQ3D5qfK=? zgHIaYwX7KFwHX+$usSU-R^qkMh_5-FNe7=70RY=nxf<$HvIhbrCF2viocLNaMb|v> zg0=$kg4=3b2sMhxgt2v5u^(aKmQd1UhOkjHune(vB-UU!^{~+>S+;XwEwQvF=WSaM zD9_HYK={8Zxr5;E=bbc=1)7!&%L#>iqI>yL>6HAnE!*i>tTq3~!5GFF3vx9lxU%*) zSnwROhU~LXS^$!H>=UtuGl%V8`}vRNSkE6y^;Qnt02dN~%&3c=JB+}#&)Q4u0F!r` ze)z6E!Y6zms{PN&EV=qnhmPlJPI_!YO?fxHLYb;)fy^ACJr6Db>ifht6pJoEmS4=cBcTim16IiJ7#Q6U z&yk!6#tQFqy2bYOVS5nrx|-m8LTDNY7r>!zvPDmuBbU@~mI3$UzEg~+t{7;^dlqa| zao=?w@vvUa!q4j9T^!EC#ZsyP1t}$5z~P&y|@<`Li?87`MhEt)dlM4c0l=< zgrHd*w<+x7J46z$j}l@eL+>;ZJ>a>%3cR=rwsiqcU>-`aiT0SlP_CN2pCut8rk1$H zD+{>j&h;U;4UuI@n%xVYozMCELIVtwm}nM7#7AUN0M@z`oYts1)TKDn8m(lFw3iySZXdha%iNnC6fUy;St>SHDHz!1AKr37VAX47HE)@ zx0(C__PZpib&cc~?*UKDL}h8If;wa#$D~BMOGZma8ll@2q@?;p>GMA0_3|j_Cow>b zksr14QW;7mQ`hy@nuQ%tI9<<4B%(;kAQ{M*;RUfo5?Eev+W54d^YQ(%4>`wPIWk#|>7?3QO}>kj;~Y1xkLR5JSpDJawAM>Z zCy*q21rop!$|Skqge3}IaKA5Tz2w1O2VU}{bg*6yQn<5TjFj24UI}~G_iYVEYr0&E za5S^ojG%-Q#0-@7?zdf;e#SVKcva|F2$ zTQj?#@7L@Oi3tfDwhI;7pXwM6Lf+1yFWEycT>Z4YF5Qx}1pzC*{dbr9nJ}N?-Fof% ztG#(4$Gd~=eMcxDY#xe8D13z<9FFNGaKfs#fGeSd)az-qDC@ zQ{+l^lLQo#Y{H2tawC7W0Dqh!d>S2S$HGB|u}jkoIx_cQPFuwMASp)D$Kt_?JvpjN=Gysy*2K(lp$J;K==qP@2_lwwhuuO_Gn%ve;IAME5~!R4cQ`px;up z?p~eYGmZ!xzjxx4*Bu>g>`jOMT4|||W>wp{=ZXUzHMH+Tk32X@;60eaNU!z5k|M^= z!LpwdsVJp74+NBg!tqEcS$I0-Y;d8WK^Wk!xQdI@_pC0HY^-Bww?z3I`RAC4{;&tM7WkuaXZ{c03O z&HO&QTN!!H$PfEH4NkJC<)foG4_X@D#mK(rS+Elvy-a?dw^HV4)W$A`?{OhS$Sn61 ze2Nh^o+9PQxD*^F!9Cj`x!(ml!FdINvKAU<%z6KX3{by3=Y0Vwvuou&gmSC5L4lycMO@3P%lfi9`^a;91r9zzL7PwkAHQpj8Y z08X8NtixkESx@PiCio;YSK$J3H)1L8g8p)!8B#^xYo)cNFO>!O%`)$=BURuR!ozmy z$q%CY$SwwKM{MW!RAXD6=I+%Z*t2M0IU|_P_0>}07g7xZ0Hc`wR$pXk2@XxPo!bH$ zP*e)lwHbg<2uqT-hnbbb2j~x8=E-qFJ~&Bn7Nm1jKdiHYzw(Oo--{*vRu}@Th9s&16jt^9MxHzW0%f zhbmYF2@o2}UkD=N>PEi1t5e1Ha7*09JLG~xaH@aD>_%Qb-J3=CrkaS83Ri^R zY+e}cJBa~NWTPRxS`P66^2Z$!j88aoy}GjP2rp(I}y4CA=j0w&$}%azq2(pKj&;3^iZSUYrXv*rX`J1NU)`65Ba3-U`Dn^*k^Jb%(VXc(~}il?T41 z(QQ%1Zt28v-hzJnYBTM6VcqiemaA&NVp-eM_p7#)>>D4JNe@zfz}ze{x%F)|ro_82 z<<1pOj@uY=X2kW_St|sPWvKy+c!Pa`uGl@T;W$pc82i0FnaCJy(TSaq?r-MeD||;f ziyigLHmzoS(2#gsKeFG9O3#hsM2@p}Ael2N#t=?d}S}LJ}psA?$5gp8- zW2IknDtw<4(PiONUAR8VIIKYh|s{r{4b(=Y~OHghx;5+*4zD!7H`Ek-5n@J03c z36SHx@!rYH*o$u-NuPYO@DI4&prm_bm-{FoDdidJBd7ahJ@rlh@*ETY83wmU=dyr$ z67Q|JFB*YdDV5`EY9Cv%*D!}e1sp36d<{|Mm$sw8j8}q#S>91Pntf~bUZ?U+qyA}O zHhS$qwJfNg)RS)|sm~TyRW=Fb6!2v}q^lUYE8B1f9;pU81K*%LmK_rhRCgD#LR8Uu z7I<_n8j>y-mJYNlMPA9mUpDj~1qMukHF+ehu&Vs=P<_TWf+kgiiKHBMD}ypr6|-o4 z3xIxwUN17gs6DO<$x#lWn+#$77Lv>ThGX&_R-_AAG(HodLZ7522b5BG`xCyBpzC9b7grZ^K5VUSKfEHv?y$DB?F!{6@i^--5mA zqT&vq!FERYW~#n3?9t=z1R|;g!o8y-rJ`e_qvNuplP;sX-NM2al_HE6VdTgRq->E@pC_12bXvfR^BA6eM{JgPS~pY;W~Jka6puJ#FKdP zCh_cB;&<1GYg#JkHx04NEbH|e|UpiL4mCJ7CagfW$L?@b_`h?wjkNOpKvcGOmO%v5&DRdy;UC(R}&BPJ&al9S$* zleP8Tfw1H%rxcW1W|LcCoTVz7Sz`mNH3rtJ0UM=(%`qBvHhB#`c`#IROIKc7Ox^$_ zZ*VGa80Ir*lRx5h5K zZKxF<85f?|6kf#?-ara&r(k@W0yyGAgxCVD5U)&WAh5c~v!Tdip~xLjj5b|_cU^=} zT>OBqn4r3tu)7#ET}*Zja~;g2T*DiF|E?w6y`_`=3 z)~&_Xb^F$B=hh8x*Y3sE9-`Ops@Lxm*X`KWubR{!b=RNd)?ZH71NiGNWE!sd8t&8^ zQ0*EfyB}9wH`MY~SMW80i5np@jWxcF#U_o|d5yRljgRaa$@rUyWSfXdnjY3vz6hye z=I3t~^lKKXX%;cfmJDfnY}Z0#+C=KtA`{ml z%h;q~+9J>2DyPw^DBJqlv{fmum1L%s*0fRns2-lbO;@80&9A{ww#^`~4a2nULrt3* zNyA6e21|@~tD81!jrMP{?RGUVE3$U`xVBF_?an=IPO=?dj2&O?I+|o2tH-ql-E@TP zv`Uk7hLN;J`n87Mw8m+4#_n`R_q4{}wAAypWMH&pF}7sSH0RoN<=k}TVRRRebeHjW zSIl&Q^T5wYx+3_yVtTq0{W|LyyOQ{OYBhQ~7<)Qzy4y&4dTM%9G`gxZx`*w$OEh{X z?0TnUd#6o%XKtXqb0mF}7=2%D!dGT`ml^vuYWmh^`nGoZHc9$-Zu(9b`zyMr`eb{$ zYWn+o`mbjC0k{3|_C0s918^S(5aRoRwF78!nX5Mg*uD3pV@pKT2c=8~<$TMC1O{bg z1|QrG68jH6dN@QTFho5&Nc*9Ts&wfjMW9XO!vac*^n1>21%xKPDd|0^DTsX5IxEq+e$2^by zXdd_8Jl@^HWDhEV)*?>9;{AlhM|F!3UTpY+1xkkn^niu$(K)o1P|cR28kS(8IT-e! z^HlQ7)XdA5^w=%c%k$mKf^*Blcgv!WR>TEYBz}hHNLZ1rTalkzQM_AGdbIjVa8*TX zRn=it?MHYHpzXBgqctt&H9f&KeOPD?hc)AbHPgB^v$?gmX4t!2>toXEE5_@4KI=AG z>vj(7_6h5bb?Xj-8&2{YF0jxX4jVp?HvG&s{1Y|;>ox)eH-qFiL$o%d95!PfZN`~x z#wTnh)@`1FW|OtH5*)UY0=80qM&~fw%D=Z=Sg=*1wGE5TQ5>*c8nA(UxcKniQp4SL zQ{Q6C-gdIiP6sSTN5M{~;0_N4Hn-U_li=|bg94ts-z=O8^0F2J5M+gorrm=xSwtUCZI?4{kHq9q{T4}Cj%XE*=pBz3gtAXG4_OlrpFj^V0}nY0 z57B0K+4m3l&5s2f4~4alMG~ud>#`6WPY?r7q!UkM>rdq8Ph{)bz3obM?ty(6PhawoaqFf=_<6Kf7}paIk5;l=G#C1EOgSr#Lkp>#8`h$ z*?(>$w38Kd?y`UGI)Cl~g-#6!o?EdT`#B!_ke&y!obQ4bg0(OGp=Tlc7m@u7NQbkI z&`Wo$D>tDlr+W72z{`lj%bdWg*!m0l9=2rC>ok@tPnPTZX5&P?teM*9LGxF&^;b3Z z*99z3v;(Hv6euecuDcR%dW5dm{n(qJ*Lhes&7?QkLbr#;?9$UW<*?8k^S28uGotrz zR~&E0_ODm_52m2E-Tikn^LL9v^DLovBP_S;Fz{XAZA~E??DrkkDfH)$jxXD6KU3}}-f)CM z&kLbHQ;up^TK`dqA4$db{*o~D5&Itr2h8t&Pjx#=+S~4U^Pjtf$UfVTeZm|E2VVc} zHWsLSwdh~Uto*&wSuOj&XpaB&g8un8haAqm zTlznfY2SXay=hlq_#@M^gWqaozyEA5=+AN>_+$N=ip~r(W~$?_kNNW{u+N!&vHp0U z1fOdfU(7#~3k`S&-u>C6iAz(zKlF&@_f*ikbGu)XBrYBNQkYTFlOf@TzqF7UR?O8e zA2_bfbT69(?oSuNMh)~Ij2wr}Nk^=EAAae&+OyVXn17Ue|Mz0;CxQoldAtbL-`2%1 zPlhVg)t@Eh{B3LhwNIXRFaKJ(boc%?!{5q)HG{kRTR-31&zbnO41xFltWfP+5f;qX zb!SF@eai1;fc5|REPsBiKRc4p6+iPmvtQ;31b^&%x723Zf4=H0KH z9sj{tg^gb5P`kq9Z>5LcwX(4NT1o^awtsI#IG%N(!=S=!|Nd;Zp78sG1-&C@{%igp z{${p=&UM3XScLvLA8P!P*zB)~nb>}9Mri#Cvk=?AGwi?rYj1%3_lXxaULO5AeF_aw zziazThT1pOzurLFOZP`w?Hh_;|0OiA4IMMT{l}O6jnD0GQ_13VApHi=)j;r`;03>#E{B190v_fhr#5t_y?P5&RFX^e`=e?!xR zjK)U%K-1h`>yF|6^b4A%tBdL<#lvQsdN@-tRXSd}JZCglwbXdFt2}qyTY}jAR=NUa z{EQHv@}1(L{>2zN|AVbkNN@F>t@8DCGuYz*nQ*%DJ6q);oO~JgpKKMaRDUKI#p4_8 z+h>Vo7{#N~_Si=8zSz=QThT(Oujg+(_wAx61eMWb-_#1}T^1coX!y6Uq%nZZu z%A@!F#PCx3;P1h#!XCj)bUlR$jM7BNIhz)s!9T6S2n;n~R$*25{8aR_A8VX{_-+-p zRH7H9*ECGYZ(PH%_ChQu$r}Z(p~@@n{TPpbkSBIFHjzu56fSZ+p3ZvGJXL2$OgoU* z3sIlk7&XEjz^lq^EKUwgbkjG+NeyyXG30eHR$KQOZ^$+zH)DeZTx)$B)0vshIH{TG zG9n4|1N(dd=I1uh=lIvO!P-ZRw?sT2PSvKP*p)U{4G~W-_0{p)kwW)3Dtyt#>in<{e_YY9Mpl^_7Gh zuWQ*$aiP_bRK3^!4Vdaj(Zmb|xXe*Gm*(#16p@=rFiPTOgG6`9vsEypvQpj3C$Qf# zere>NCkr&UBA$F?!@)yb+svM8!u zjx@p9S-x)#%>ov_wL`-4oI;k8;*!FUbO$Xp#1q0pA=c3Q^&KgetVf{_t8EQ65~7f_ zM!pb*O7ufo)#pu*lmIc9w}c(V_x+-tb501dE?5zX_AXVa&Ld8>xMgnm7NEdb%M0u$Ojnd zMYCWB9DKk5zCwiGMC90REPA85l5k7E!TAhB1fJe>3K2c|*@;mT72M1yO#=7-VehS; z+Ti>DO=y7zEd`20fl`WF1*a4!E~SOy?oOe_odChzT?)b7fCp4;-C z2H{!Y5|JzWzj)IwAv28s!dMx;gI3X|DIdfcA52O{L<5xg=!=MS#u;jbRY-~I;rIrb z0Z=-4xqs-PxYaUxU;B2+hzeFR`qF%!5Db!_#1Q!khC*?5E6W<$K0je11XjC}_Lur>hE8z+P22d--H1f><#K+_Xj^4*h zm_9y>T!=KGn+}8q==-W;(NHjAw|BpBXAh2|6K7INsTbIe*C zj5gO0@YZ+S6BKk9rj9k0_-B=F@jJdhzrVNvU&QqR0w)FuB(w-b$N2a{`B&pZL@n4o zrZB^IJBl=L-(U!mp%6$FqOhwN@`Y#s{4ot2K+VYmcp;dbAAL1~sx6pnf~pdIMR93g zqs=@4Po@;qhF$s;zda_K*}~zXvgIVz_w82|Z$guR;G%OeX--JqcgY9GEpT0*3rgJ| z*5!UBrgYJtU3_Aiwzq7}P9CEr!}!70o6d>>>Lz63ePy@UHOLqtD)zjI=cVGfzJO?B zaKJoI3(BLU$v881RRK>M0PmUGjFGGwV@Mf+Uvw-3kNH8uie2XZJ-#WT4zS9nTh2 zLcyLo4!&OESkU`7oR;rSO!57uPjviedTz)D`_VI4AY)lVkK87Aipy&$;i}}Ks?`sT zF8myxRjFHYTl{OCeU5`?w%mds+`XHjdenCUsPfyQ6t}~RQtz@!w39x*yB&!MdiRfp zS2Ni+OUbt= zv}-B5cTa}5DJy1CqCXKZU~P89C|5*eSt!2OB&Aoo{Jy-7`dsfT`GU$4t(Ikw)w8Ex z%Vs_oC%rq=Jys`$5iL`Yulh*bPIU-)7F4|AvXyF@d)aefMH}#2v(y>o>1a2F1p zoK@mS0!p&RxT`K$f2YlFaQlsNfc9f_;6$4DSWiGE$~Vn)wFh7BZ& zJoNd>9*4f>?_V{JFXvN@j~9fgi5{U{sQ!APEbgLtBYXOhxXo;j@O?dr8p+RtK7z_9 z-xnlUL-w)+;CRJR*~yMdruOg$&lKVzyn7khQ!@HbH^s87R4+e33QtiM(!l#QF z{Agz@ABvKN24KC~r=pW;SG_IQ7|YMryH&I2Hg2|o%hA>N(DHNuU^2ON zkeQTB{>98^Mup{dR?A#LyrSp{{_%;94IOF?wYSfR zcDWD1ci{-!&loll;H6s({SRb6^HMnCFCgEaZr5mGDjO7@S4n;+9o7~Qe>+3(-1|(W zbQQHGO2~5*U#pW_f8_&km>-|eqgwSFLFX6e72W_KkE)TETgAsdQDvE@Ow`9tU%S3s zA?L8hUxR+J_P#^2KgCEe{S?)P;F2x>5TETFjv}V7>a#2%e>J7Y^OHF-P9R?VJ0{pm z<|)U!%4I%&xp!!}B7U%(VlJ!-h935ps1o~yJIp%h{0~16sj0gtl z*e1K*m-?g9AKR*(15_M7pwzJ%(6c9Ty(@VSNPQ~@KDYXc>Ot+zeD`;rivcm!plm^g4- z`zcR;e1mWD{evHBF`5_14#Ar7?n52a+Fla#>o&0hZTK5W$2fcqQt#jB$ll=^FCDMF z3QBki`bWR9pby&&DtbiBW26}M)_SCv=$YIfow)6?ztH)Se0Oj=W>jq$qxcyWV0f^q49E5J4Fi9+nzOxE7Ik}=Dksv+-qz2Du{-`t>zp04)7H@*@yiO#<4)ZcV{vv z)r^9CZuiE4QJKUHT(NSb@w<|7X@3I7&^W56Rla_Up5*5CuCym6L)keYCyf*9^L-<` z>={bKwA9Y}>P&At$6nGLAu>Kn%1q zy0@#KfPgkOCkH?K5&WF(_}*-8BN|5M7tG;rF=Ohujn#p#uR=rg#7WCRs3I6K8;XG< zZ2ECZ>%6HM*Swl)p(Ce$WI&zDKv54yRJ{TvySwi;g{Yu1qdUvs>ck7-ubTmCGpfq*D)%N zQzaAiS29iBd&;wrv|-sbY3p`#?(od_d}o{lYYdN=!D#-0s!zq*J6JjRBqDB1Mql1UxguwYlwhIBdf{!BnP0msmU=ah; zMm)6qYtDe0V?m~!SB_7(H=h#kD>+xJ8_>IUz1%$SS4I|CV&$YDuzJ~~a$FlTNp0Tg zacX@4CcZ|#ByGM+UcO9szVmH<-ieVnKNzS17N7isyrqBk`H8%u>z@1xCohQRFK}GV zN8ZwJP*_5P3sPlX_-@KpPZng8zo4nn&T%a?@&SV(g^!W85eBx!1=X%b(Z&tq9H6{H4BNt6=9->k&0K8s7NaD_snf-qUQZ0cT)ZC4_SLwg zs4z2wp3bST+<+M?azmPIH>z8sl3|VrwatxCnftDzBqy1iHKoL}P}AX^Rf~+&cQNIB zr}u^Yg;aZ(cy3u76onyntWMKfvJ^j0ldG_)7BBZ1_rZmu-4}GrqizVjj)OYt&Df1M-&yd2{hMM3uF1NWN$vuKat~wZm zL5R8bYa#NMK8Mus5`Vm_kH*IK6KIH^!|8HuNN#LMEg}e*Ysj?3j-!C&_T$iKLcl4I zLV@Q|jgSOvp`1PB2^}j5dBNTT^t(1fG;uQ;8%rB8USbus2sEAYIai1_!9oCSDNS$+ z?8#Zkz#gQ6qPfM=utv6dI;DA57Q4Bzd0L=(k)kEW4zjA*vgy{c7~Hbk2u!PMIdp42 zq-Z@YYFQ9yt-@}(3~5EhnHAB^39L#rRm(!e$*K9KC&! zwjGtG-BzTXIJDg&yPeRTkZix50;hwD74S-~gVw6!wO0Gv;tnRQ4p#SeI-E{M)=myM zqW%4T2X9jcpS$Ig)V8;)82!P>ZX+712vi>*MbaHA5ek)Sf=X*aPSqipNX!Q ziSE+W<8t5QP6_iCg!yX0fbOsWq*}cR7Ca9Ny@!QU_C^Z!Mr-xP;`Dkpp_>hN$_2Cm z9ozld(Di5lu5x`jf_;+vJ*eJ&Mev>?IJ6MnSAjgIv-a1>^%sQpm!j?_K!!mcA@FxT-a{ zZq3()lO_UMUhjC@?Np<*ynf0Vm+Jr*dxV+E^y`TlI zmD$_`#!1t-k$V?k+EhU37>TY)okS z4co#n?tB;3D!J!MY{{6W-i%4$jAr)CpXM$Y6=Kp?WDOs8vFrf(RG?3<4K={7AFXuw zLU4#rXPy6XhKqgD^kB$AZUksC^0j+T<$kVJ2*%Q~S}F%%EwzS zlLPotZ%gg(nhs+a94|^x_Q;1Vf7V;8)i!s~Hv646pWZyOD~N&GhHl=r#lpT(_%Q8} zwn`tqDane)81pm)XO)6#(!6|NLTAiJ8?h4kaKI!qKNX5$db}^Wk71B`u;4iz?_taS z_=eMFw!w2h<#7RZ7#S}Fh_<1N3Jx`-qiIF%Ev4;Ax)11k3_MVyEU`g-MW7ArfCg$b zjmW*b^kd&h6~@>-H~8{<1t=b3F;2K|6z@dL9ncVZT!gpmsBq$AbM#zyt$`i-{Aics z)d{Ni@yEwL1od(0BbuMcFnE7bzGW|RbJ;X+&W~n8lX|R)YEqpV?S>k~uw2`#xYrz+ zB#k^WiyVYKOz5*=NQBO7+5+6DyQXzSl#T?%yfJ1=dlgFm23s{1*LI814ZJaSn^p`; z2W=uTnwnSa;TWVseZIDP$L@#=)W{=m;mgo6^bOA|mlkx7^h2@YUPs&;v&aol*#R{D z+SF!rLP$F1-;Y2Q-(Y|>e0gtyg(tGL=cR;%f!?xq@2#jxw zH^9hCf4w!c<-2cQ1MZJ#m5Tc|OLNu>OHu>N=H_60JY&M}DJv9!H7cr^roN_uwGC<@ zs=4%MKLZVY!w^4I5z|D`M5)-s)DRg>X;f=XLsZ46d{j}5tSlM7zc0i6d{J#Q)AIAe zrDJ0?{i^+J3?j4i5t*n~F?xPx(%pU<1~qwRs9KulHD>1KnhmJ^pWFZVZYG-5qVkIt zqHgs=2lYemZ$ViWMwtKSW#Tj>K}oe`#;pl{ay2s>*26WfS=DA-7ib#Z^eI1FX1Uo2 zC{jc_equg3j4Na>i{4MhV3;J;5CQb^zulos__z}4k1cZXvh2^4Jr@-a2qvaofF0%* ze29NdJNu+~;I4l${)bjHY@Z^ClblD(!UDI8K+Z5>jFSxltVGJN|23Pw%kxhEuXdTB z2SQoRfx_q!8l07MLr927w9JzGLQV9(Eirw-pze`He7LM$@3&G4n%hYEq?U3}uYK;g z0oXk9XG83eQi@)p#@sm%yGZ=ppu-^p5s&6H*FFu%zB`l*-$yNjPdAk9mw6)Q2|9@w^49c0JZkzvf(`3kvr|XTt zc9++P^<-LWG!so0gX$;Pxmur<=Ge<-x&j1zaR}SUQa1l|5j)o8cG=|N1P1Zdn~!K( z8X;o~Hlffrk)IM#UO*iuj=$mJOMzF-Tz?jP-*zn1(qH|svHc-F)zz3VJudG@$aXlY zIOF!e;k19&PtOXWD_M83T^lr3=P%VOn)F0d?1Lbq`buImi2*^9N@FaxQa{iaU+DcZ z#QmTJ{}JL`7OL{YXja(}M?3iCON^{1LImmTHt_L*qPt(K5+q_U?jI=e>`zMUHP5Rt z1Gidw?NDs>XSLPW-drya!kd&UU;cPKVg&icVH{7eF2=k**q`5=J| zrzIZ)8OKu_&ZlVcTj)&!eQB|^r8-Flk9F<=I@nHtca)RIfjmTF?@Lz*=xIzu=`9 z6M1n@6WRoa?{T={ddO}Xefu<;52(~Q`=_7ruN;zmvH=UnNI%4+`zH6Z?RFH`u~430 zStBN-pI^|OrEbgR^G`LDyySkDdhDJrxF$X1G8^MBH?L11QUxP4W)ZI*+N=6AJbDsS&ZJWYj=1V}~NhS^MjtB}9esu78ZqIsZfW~x_ zBtxZq*smQr{o8LN6Yujvn+g<6Eyk2hPXAQTZ?L=Zk4tREh*2IWNovkWQTvxB1g35% zEfHmR3tq^heku4dq7F9*=qS>}Eux9r8FIDwo@3UvP5uTZ0`NR5Auuk|!iG(9Ccgwe zd7rO#)i>30`!WQbTj{y68WfBX7adSy#QZ`)wu^c)Iy4SpWbk%Qtxu-5GO@W>SbckX zFnI#(nlz&;newHa98?XSpfgJ!`xO7{tY!*WVr3{hH#1vXTi;w_UhituL8hBa9jp2m4qTDhtA|qOPT9qiFnO@K)$R7N8b;Oh(mH%MZ8TJOoa!R%y$`?S3>t`5Et zs#n~jjaQY}BWIFI)d2r*IIZz!z%Kk>Vmo&;=sZ}Je)ZpQT6N~Le~B$fp*ovnU|T}q zcI3a|G|k)5#KG!3o`D?&x7)Eyyc#62{ZBZprciZY_ebOHM0t2kv8nu?#@y}Xzu~k! zox9tqHoV&MK>2+Gio0o;LM^gy_n&TBZFRoU(+2BWt_F@g@9vgRp4YcMQ$QU1{wJJ<{7(-MxL?7KsE58) zI0@FgU;R%wE!^#Xjplg+Oj6-AI^}+yQL*9QaN7L_XG8nh(2bLy*b3!HZJ2|AfZ7QQzo!D({fe6wQ%Indbe~T?A-pWb)?py|8(`an-y5q&`@TJ~&c7cxFEMyguY=J`~PAREa*6q`uV1YZI?8 zgPJcRqc@Y3H}kYF>$UImZC@^cA1R~X2PwZ-W`2AiKYFmA5X|rGw%=y}kcHPz!r4!< z-VgU$(r5~(a1H#brezxFq&)4S!t4Jd(O<3J-^|Hh8|<$O^Vi`G(31)qA2j2BkZPXM(~v6T|oG!w*S=kHO(5)8S{=;it|K z=b(to#EAR)h)1;ul!6Gtq(Sjl|k|Gmf_OZALvmyFw_jQfU+M@-jpvdjXqlrp}|CmNXm{z%gH zcR0yjxU*i*$-Q69W-dx*;q7`8V`Y&tXOU=RQ5I%V^Jl&4%c6y3(YaYWFsH) z)9hx`W95*M=WuA`ym!rcmz?vuFo&@(`(s}YGjomrf36U7?w8vfaq?U#*IeG@T;bVV zIjmeo=06gJxnC{*C_{49c5_v3bBBUbb;;B8AgQC$d4{togX%9Vrx6)ew;49Gd3LvX z_T>4$`12h#@||7tU6b?OA^D!O`9EDV(fGl>8ej}pFs21Kq29wdI4`U)HQY5XlE1*5 zyuiSS7|{om1Ax6|3yW_HOUaAM`HPUm_V4;B zxu_me6vy*x6^u{a=iLs;>L~Q?bp1Pe>VgzQXNzHd#Y4Nreat1p{3Ro2_b~(QfH3bJ;O6okm`&QkMsTlr_zk-QSiy zl9!_hlt0ldM{_GbkPhgoDc6OSt8ACgPL&hfl@n4RDu~=Fh*K&A`Myy$#!<6iQ`}YH zQ&iH-RlH%Td?{P`+Om=*q>{9#lBvIvYp;@=rHVt~MZQKMxUfpFzaV3_K;*7URJKr9 zwmLJps+aG#r%}0FNV&pX_1C-VZ^*@yK+TW-qAyoPm{EUJ`TuAY{?VcMqdQlltx>CI zS!?80YmriG)n8*HP;1LlYv1_Cfui=8Tb*M{of$=)b7P&|UES}!I-kZ`_oBLw$-i$k zYJ%NrLL2MDk+UZxu%&2lbgg=iT@~+EnLJmSB2bl<(vV64NoRp%3PAFZz*e&=!Lq9G zuAxL0QsxG!NP#2@1kZ0r3TReKxm7pxHwx_`8XT<5{oo7!mUE4kD~qyi*_N%MmL1EMgT|J_{+3gU z)^qIE%e$6qfz~VJ?n$%tp|SNKxhz5|+&aJ?nQ8;TA(%}NoP7xPej65LJFa^h0Zuz1 zYvXgdb`rV9m&NU+A&n!lZN`%!N~UcuS(>R+J4ol-S(@6b?mEckJBS54nc?m1l${^t zI=QtvIpH0A@J`M9B z!n@Q%F%|c_3~;(N<+`=4x^-C*y2i!brtofaxp$U0J=Ux}wn&UC*liQq^Q*YWF}26( zzQ>ai=7t0FV}*HW!9eb?V9K5l!JaU!o>0o(a2$VA#tvgiFetb;D7iPdus7WbOr-A_ zZ)Msy*OaH#2ZlEl+&3{7yg)zgGs5Y&!0E4+>#0Ewpm2IT?E4|9{gv}bV%uL&32%k> zceBFVDEnKQ;7~ZcZyr8;4<8kTkEO!L-3KO92YQg(sHTCzP~`Y&V0nIE9X_ymKQOB` zxJ@}Y0Uum*A6!ixJVvgdD2EPNhxW9F&IN~#QitwChfZ)R46k7Btbh43z+)Iodl++I z7+W5OhC70BfEXrX8^N_6d2S6O3>$e_GD31NLY4-jz#XMx8+~OxN=r3DKQO}DJnB3D zn?bP8skZn1LvO14*oTE)ezuh7w0+z{rbU!Z#mIe^*0>nec(K`w%i^Jjrg4P((AR-+ zl;)vFtqBF|31yGJxK?XnTw7>DhiXz!Xj186{G0Zq8r!5<^Mr-Rq>c5YUD~A4!=$~( zkRo`LQG3+6dFplA6x+cRJMOgaz|>p$X{Lp#fRZUt^R)lKbl}2t*)`K|++kBhQe7nbdciMbc^E`B69yTyPbTHqCgr`CaBOVLL_0vT2!uZ0% z^uWT>!NM%t;Do7xCQZ@OvXL5@4mWxethRc=*a{5%dmDRiDYqRC;x$S4;LiZva zxVY_exDB$|4x-+17T$?x-$~TjNqXE$DBTHYP(TN?Wk0s&4z}hUw&n}(7U=AP7k3K} zcZ(l)Pb_>WY4<8T_lnZ@%1ifZ2lwh*_SzQr+S$F^q5dg4+v$TlJ$O55$N^ON{vh?i zu+cJ6FJX+j5)Y&|5 z*|b&PI~m-(J={D^|GR%W6P8-&Z$|rcjG=oB7%B`&KgRJo##=hZe}%}%L@uv%PKdlt zTGCHCOHYW0PDoo%Xr8yyjGWLSPUyW(88S|(TTkB%op$K#TU{S>zdCvU>FlFo!z_EE zqtlt7?wRn?S;XAoBlYpI&e0d!Bk`pp3EhzEmUEO(=UV3Mrb5#hKJ2i_qWiLB5AG)+yy0mpn=BW3Ot@n1jvQD`&)vb42 zx^hQc`9@s%HD382T>;CkKtr{hL(Us6hpR1zk)LjM!VjZ$Z=zlu#(LePmfa*O29kch z%;vbv{dD^~`AkFkLiyE&)X=RY;#OwqR)n;#^wnKC;{03LEhGa`Sg3N_Vtd>A>RzMu zI6~L`qSw90_Wm;HzQ63A;OPFe_kQS=d#~cdfbGLrgxiF!N6-7cTEs&=$K9&eT~Wqe zjV*z&_4=C^Ed+y){AG{FOZYpA%cdA=%5sa>UW=Q}hz(o-sx2ze&mZUu3J3}e3ylbg z3XTbnj*E>GjFo z_QU?;G0JG8B&Y6YPXqymGVgGv7!dpQM}Qkxg{a|OZd z@l{wLi{hYr!`bF=hHQj_2jpURy3BHD(4+C{aJe<)xuR#&&3exN-aCs)cVn|%s5?K} z8$t|a*m&JPq63z_?=aPp-Tx)Ff8W>!l1-~9qZ3@~`jhG)y}pzh)A|9gY_^Gk^zmk^ zL5z7|17OXD3>T5rt?$_d_cwJf5nR)(%F^_EmWgL_ATU+S!%5qB>SZZn8LSWyB_BLy{g`iENtcD z0zSVIXV9&n119Pjgyc741f~$=X8-Cn{Z^&BFP2^zpvzx}2(!S@l7wc&+ zeU6oVA8XmZef!JK{R;S-vEs@Bwzd1y=6`inUSgK)Vr$>diyJBBL8GXg2gu;a-YGKr zL#SOdqWW6?V)BPW7}A97#CADl7$|=^W0K$eYtqth!EwemhpKK?zDm1p&Z#r4Zr*K7 zsAf?vg1(?U9q(~_CE%@a!D=M?A<|Uk_40fj5vyv`kPId$odPF099k3>J1$ymX9V)! z7X0yYZBVMLrM?_4Z)Lxms~8h@TdZ00bUSQ1I&3^>+oN_r>O`>LpFm&Rx}U;1j@(a% zSzdXZjeX)kHc$+0J(i1{jvlU--_;*=^2B6R-EicTo!#!%y{fu<*Qr}|ztL*zb$>OK z;q`ERRo05Qzso@Blx%xKe(deFLe25m&4Y0}V1t*Mf_C?@9q{egml#fi#slKPMKVk8 zr}|I7T(><><^;Z_)W?zq^Vov%d^kh1a4f%+lg%Cnd~v14bAokJQ}39UVjC;~rJ%Ir zNL`g+HbD#w`i8&aAHQ&pR2P#*1tM6jDVw+o*2VT$Fnpg)+9}n|MLiT~CYM7#1?&C@ zISI21&7s_q>fxU~33rC)Pzy;ygtG@DGVgL|o=QuN1sz5f2&7Sy^ui<-jd?=F=GW-S z8l*JrqZ5kh8K`=D6hjN|JdrI{G|Uw{l8CwQn?C z<$X3roS7I0{}}RdV;<)pMkx)XVAwu5{U|p7tDnrcGx1BXgb-LUp--8+`MiL;xj@-L zW~@N;L!O$o()YSP75b#}B1T*mwI1fl%IJ$?t=&S6De}Cc-S zMLHz0%DK0{!5--Qx)IE3mirgwpf8cyCFCj%+{C3${`-bV@gh{?vNCa?*o3ElX3+Jr zDigQFOj34sH2Ja`EMH=w+CMu9xvVJh zcAQ!sYDm0~sPx>QN-f>Y@^~C}akyWw=sr$PwO$$PdZIV!KDnA|+m*MI=qK&2;Zk#Y zKA7z{zzD^ZR0|=_;{<+~@4`tK>7?wo1F;Aiyfaa2XEfQlma1Kis7W;8@T`Ctz*or| zr@J}3xx%<=*C>~+dxY`qBaDmJ=$^X3B#7LWBdsN1z$!(!%1`h>zph%xZElk~Hx1urhfMZo0HUHsH1R zF8vOn)g+QKGhEkMnO)GUgCaoQj%xEh_Z8cY%=76{^zL(?4-ZSyZpmX&)W_M9xcvgU z*W+&_Y6`y(?5Z?ECdxf)iVfK$zRlgnJWsKY@ey2Orcj&uif3Qu#Ac=YGzA9rJT3S0 z*bn%S($jr;QRyReU`BXnQa4yj-7aYP&QGnQI(#+t!~KCBHl%aYX0@Io)rP5b4tk(q za9%BM%kP_v#HUVq7tQ9LPPdC0mrmd-`D0I#`=uv!_RS=N-8MaUD|ivS(6{U-!Qby! zUzyZ*uU%W2XwI$mK05bEvY$o=-mlXr^7SgU*vF;JZ?JmtRq`k}yt=%WNX}fl=@VYwQ zV!K}^QPk;tOMMa^A-L9+C(@$nX&=4$1()Mh^?<|US?pTvQjw(VL7cD~_*L^?>N-}` za#8HnysxczH$6ptS^li~&}ryy@lHpY`2GRsjiSy9#VgKefyXPsvir@9Af}Mt#pgMS z58Xd>E61E#F0JG452h%ts_L4r+r5xiGOznhMX#Hw2+zw5#KSI0>y;O_*VS9ZtekVJ z!04Ic)#Ka(;{M9(38^<)y_Xb^7evJyee*3a&onUJ^!c>+3tlrKfEj+G389$}0mz3E zWJc}m^Qzv5R?P=*+K0y3mk#7hpXl?3*LUONX8^CJ(UwipsQpJq2VPPKet?6Zv!B4W zpHQOT=W9Pv08oh7uih9a3iGzf^ChqMWk~e>2JmN}_Ej+R|9%bB0QoCB`?FsAD^2^W z!~8V?0V-wzBruPKuU-dJ0an)mC(i;ec>--n10DZiwi`Tt);}1qV2)miPCj5S3sY}@ zXOOoUD5&0BYFpC}Y-iFDXvp)I*2X#q#lvhwVg!7roMWbfHoqX;v#x`Jw}SF`gYyBw zg=)dz>EPkGAae{x7DuM4M8j$+t~y4}2Ee;U=a8D~kb1L_4nQb`H?&A3l!So z96DGZI+Pd+PYfMa3mZk!RkN@e=df9+u=(q-InwY&#_(p|@J+SwZE)zabGYF4yBbC= zL}f@9AOfE-g7y)IY=)9D$M)@gyh$%_RcEJOZE|fi)9>sve2i8~M~F68k3N z#Z4s9P9)(>Bmrg=X>SyTOB4ZL*bZa#F0aFk`916T_Dj^KLsZm=Po<4GM9N6%PVQ2O(l58e$b@VoxSK!lX1Kz)nW1(P}r* z>J4!kGjZFsL04BXf=uxO>hZ#S@n+`nMlSIdWNyqmZ)K!__I$u!WC_33fzEshj!0tb zlHkf@E!3Id#h2)*p6F|y=;4wWj3lf$lp7}|Aiz`)+MFFHdWlSk6i2X{IQoG~zU0t(AohjiaC5bGx zjW6|iPjan+MMqL9v>~-`CKWDiPzeg#mx>-|N_%ad_J%KQT0L#SIc*^+ZLuMIMLK+0 zJ$;@jeX}8L_a<#iI{kntGIBeda3_5)2$6oomp0kvstfQvq_V) z=^#llPsM+XC3(CvAq@Dysgc9sn)5z5$2mSn5RxOzpYw@4M3gy4j66reB1Z@-S4JaO ziab~RHdnGRM;7akJpUi1ze>=vBwu^a3{4)!X{k5C0?OMwHibOd9;e$3#2zWj*Y z{3tANm_|WNFgUUgoUjW{E-d(Uizvv1*v0kmWc4YfTJRWh=l_9d!s`k@5qdLaE3+GU zR0n%Zsj5f$7saTk)>x2!`u)6cw=n*t8c?Me>RRmQU+h1@sN?g!BUrtBSEaeHsFuH^ zL8b(f?13b<_`jpI-W3kHa)4bvpS&RMFZ@e*cU?>Ol1ujszrE$oTXE%6Q7;`nCfrUg zJ18teGTg_$vL0`yW&SeX%4A$RY_qs>lbUj*r3&Y+9B+=b>jlT*Da(j-`TKRNF!MMZ z?)y2hf{MI|%4KQ@EuQl)tE)sxq5E7>Wk*aWKb)T=l(ZOTkicTvj)8_R^} z%7n11KWV9!uWl1)>d4-4v}m{1rY1(I)>~0W4fo9eRF7hy)O z@VQo3NMEH|Q%{?O(1<$%Gwh@JSNoACN!Fn#CAgGVM32;(m-g^NPE)6W6~^b(xQ3N zYGKmmVbYFj%3f&dm-dvS$JGDYSM`5QR|QU+Eo`1GUYJF?>&mI-Duw2%wdZO*=IYbt zAkA}43v(?Gb8S@f9YXU^?fGtxd05(fU-LYCVSeyoewb=uR0zqK7bZOxrqdQ?n-}I6 z78V~CmZ=t3g%;Pf7dJf?w~@fMd2xSX@$g~sm#iZbe)Fu04?1wEImFf zp-?aX-$86Em3#SZ7Jl4y3572pq3!y>nz+J}gy*_U%evg+I(gcJ8Y#nzHAH zTKa|t-q`2)PMwxc-un%H!Oc&sP<^~j?eI;L^i89|P7C%;W4tY-w8~Ur%Zhr-Hhs&n zWz(H~>v#B;+u@dz@RqmEHqdk1t7Rj&Wjl0nJN$7wl6ogvcqdk8C*E@>F?}bw<#H!= zaVPz8CzE(WU3nb^6h53(~E0^!Rv$^6L1>r(-nT zV+^ljK*ljv>oLyKF&+X*Y)=S2oe=4s5PO}FW}J|>o=`5GP$N!gUY*i?I;Gb=W$-#> z%s6FkJ!M@wWk;NHzB=RnboO5N?4#EiZ^jva>zUxu^#4(?@vjP?=>HvBeVszO^2~NW z03ov$to(ne0+6oE{f`Qu@=t%lKPrH#yn&Sep#qTk|8fWI|JUmOKd99|9~LLEt~No# zJnbGaU_XG7Bgb!`ag?vt7c{DhVHcAcPTC))bluW^Pa8#8f1EZA3A>!NsMG#9XWJ+b zm;mw){4$UxiLRq}dp=r6qrF$iH7VM2&G+amMa;(1`p)95iFw z3LbYpi?}~&Kw`?XGVDr$?O+e~2b!@@aY7e<8xK6BdSJX3q*fou0lGoSeBOP&Tcz7_ zx7C@^a=({>Xn8nU`_%e)k?+-txOuw#h&J7Nc@p6X5X^jnnJ9vBTIT+Ap8Bez8vT!a zIi~`)F6nOF=S!Di&lQ$TggB|_RUAI>DULptq*=$ybUPq>C@rS(bO+gNc>u5Y3hwVj zv1fy2Ks~7i^ye|aQjk8#b0%d0%G1pGz9MKkWc4Bs%+Dw@6s(e)O;`i!Vk=Y#)_pI2 z*$rxD_ZgFDd3Y8-Ij*eOm6?|gi>UO`1?WEc+k{y8$ zL=&MQl*m){3jU0?L0Gm0({C9`G`~kK{dc-3?>UnC|8~%V z;1=9t$)ivc@{nFR0RwydD(Hrm9uqF6%-!MNKDvCSPUTOQ$u&kzh z<`YZ_y!5f8sk>z&63i}9$Z0f)$e2i*Y*^|WSF8plnm8fzh!pX+DDPY&t`FeBW-a#A z1YQFvD9yJoXwfffnv5lPvY|kWv(%vN4cRj3F`mBalpvHRbTdw4R^8v-h0;vEhBf%> zkjEtn;WCIW?MTtT|Bao^vL?M$KiVr^gKv#!^Cuus8kN(UAh8Bn(9WnSOYd`~sf1w* ze8&7-8(xb48AeTlfM3e5OMr#mlj?J6bEJ%YbEY&vnMxalb?r3 zOFg9nqIlETjTHnQWKulKQv1?D_@W?%nGqcurBCOiD&*w?DXd3|(f)l&Wi^@wHm&-| zkMknyX~Q2O*B~s%c!Fpuap@jL4NOOs`jjpm^dTBh@ss8FqL&Ax;&nX}%ye!x<)xYz z{6VoDwd@daBhCV5A!p&05?}ldTRvIP895yw*#XS`EK9o=g9lTq+}0tYUVw^76xEmX zsX0}<1Q#!tyCT4bjz~Z#iaNU1A7+Vg*s;(MbyvavLZtm;EOYs@*6&*Vb7T8f7LrCj z)!KmNjtywJK?b=lXj5V1H3_R6D*#8`NjP+sAZP*xv6`>+xsV#Fgu& zX0Y;T9gyb@tv9mjTMYVzlR~RDQ`p{IX!_m0%cpeX-aiPcj7x09bqy1+zKz+hbo&ig zIEcfnHZD4H+0cMXB8lxUh+u2w_}x(QlqOi?%7iXGk-?aTnsDpql_C z>iA1L8Fm?vzmTXs+392MvsjFo`(^G2q@cA8PEcMf#9mic!BLRQq%Dd!;rjBuX-`Zf z4xhR7;Po<#5enxO{$AV#v4xF3ieA=q%DV##rF z+b{F=zA2hZ#Cgm>um&-T6-}BE&mdG3R{b_TP;4ley;((h}B z$Lr>Oc+h#h;&{Gj3?2zWJMeZHD56V&-y$QQ`}q&J6!<;F%vs!OWwV*#BRRKZfVkRi z7f9+iX!*jKEmv1$ej zaTR)o)G7_Uv^LvURR8(XzS3`{=&eNAb;rexuICNCZrIB&%)bC_PVUs*o^4mZ*1Nv; zqS=tgsxM!D=^_#gd?Vic!_(}uC*^0O@<@xwd>^d}R{?){qCkoMI*DxIN?e&c=cVVnM>Ll1m%;VZB$QANZ{=lqgk^5yrJzOtmm zvQB&n=Th?%opc~eeMF%?O5m5Syf49Zf$E=qr^SFDeh2md{Q}H@!Mxx3Kp<>qw-7bZiisSs-iwbh zD4N$#HP$WqDaajQt)OOgys+t%@Vi$lFQ_2N`$acUHombSRGg(ATnLU4;ga6%H? zLV~-y26qVV1k0ZE{l2~TT5IohZqB(m&y%}cW?sx^WcM~`$y)Kzs_k^hqFcIw5%a?@*%V)AxuRf z^bR4XSwk;~hBC^BvN(jY&4;i8oRTh-+a#1L?RQ*na2J$z%IG;?gRFqbOCdlkEqd~Z zHBeYIoF8yY>B4b8!v#dcMJVBttl^?2;jdxg_Z%YRU=a#M5lSXJ&iKnVT@fEeBULpc z)f^%~73VNs?ZJ+wae(^4px`1XWPl0{qCi7fVPT@M2u)a&0}Kj-!HZzX z0a(CEhd5E%cwX8>icW$9JjoxP0)yj<;2E0mtTcG`06doh&v8KH`6CKoh~gqdsV1U4 z4N)GqH5{YNa-j2TOd86Sw51oTpe z*eUEq7w#d*Arqce?T z0LSe)#2q%oT_D9>BFE8-#j}9o+4$nwP2)NC;Y z$KNnb5E4s#z?UGXMNNDJPJB*^7i~-wXG@eeO%x_4zNSxl!0$ z{6!PxRu%+gc=!*noDLpxu|F<%XluQ!7Az z1;3jLtoIB40C9app)K}UEOh~b`T;n|&&+jFrZ;<*uXdKdBQ*=Ghz~Nu2M7GetUlnB zHsNCxvruMPfL5BIO$bmVq?!@J*$HWb1a*b%8dz!;0v*X$pd+6D1wkyt=IaxQUjvAx zSfaLgafxDafmv~RKyk5Sam`>cP}6T%DDGe=X%a8#1%mqfL}hkjC!(Zsu!O`8aLSSi z_Tm|A@pM4xq+`iEL+Jt{8;9X`bu8R$Dm+GwK*O0>=8i2pY%1H?FFVCierB=kEMxgO zWI3%89X5c#gd;$k2p5J3S9B^aEtWGgRv`E*Sd}U`%`4a;6*qAeG5nQxl`1(TD!Fi# zH=HW@&68gv5QR^wQsk*s&vdFp{>kbWS4j<3y*Q|P#ZfIIQTHk9tz^Ne$*#IqWfDbhw4;o@QfJS~}f=*+S)4wn!W~dPX z$&$W|_$JZxRi`P>si^??t%{onLruj8O{E;o(fe$w8gg(c-$cAFcBe^$Ux2O9yo;kXB!gw#NeX zn@(*T$TmRmTx@PzUTj-sY+o;KJ2TvN_OOkHvz<<|{oHUn?NU2EQwQT=`&G`4Ymyz; zV>(z$I<^ElIGsDV(>rdqbnF(l-*)c2yVSwU)X6W{DX83eDzNk6VJDA8CGTRTpi`Ha zMVCZirQ~6y_)^y#J>3(=G%10$H;``5m@0*mR;BbRWzP0miJp3$W2;mxrbnZsM{Br8 z=deewrRfKx)=;PSyK}FFd9O)euPv_EqNUe*vG-SCEsuB$&Z#d0*$0TFpy58J!#)?z zemBW}58Zw*=YAg`s}Go^F%pSl30^PF5MP;kBP0i+bO)f$1MrvuWXVA6@BlKsIiaOF z@vu3Wvn5Fx$EYLn3|E&fD3b2n%DL3Cpfot7GkD2jkj&WrbdglTG+372U8)SPifJuZ z9xB&uYp@uagB+&-;xvbxoQB#3;hoOI-IBv?>BAi{!~G@0qr<~vOT$x=BQt^{a~8wX zF(a)lBR#_-Wap8!rI8`Y(QT&D-NTVR-BC)*=sxGj&f(|+=h%_%*ol!*8lkcEz_HV< zV*u`V^Gp^!?7AY<{Q|hrM~(~N#zoSTBpu;;96h(r_uM(^zQ;8PWDWRYCj?3-ghnPF z98Ek@p$?eoPFh(^S_e-4K~6p&nUpx16z7_^!n`K34BJ{5&~Eh>Ws~NI42iiY2FJgk$yQF+t>XE^@XMIaiOI<+6YaTFB5_ z$c$Ym9g%(KkRzYiUR}~%&jX^l&Y8t56sF zRS*L~L!(m5DKVsJmt}O|a%#!)%u%lN;l|4W!b*b_>S_#sd zJLx*-ettF3WfdH>3Q_4pgiM{!oVvih=GHuQHFNrU+pw|J+V|2mFP-UY!E0>6Yh12t z#!~CI-mTx%U%x|L(<@!)7+t?Vx+ZjCUAS$X7q$LOe?!=HL+stgL)4o1=*A1`#=Q$0 ze8L;jZ5uMu8?Q$<1@t#XS2kob>t4Q^4UJn~YF*YS+nOKQ(puTlle>CNKG-`D;AAB@fcCByymUlPQ9&-ANGK_X!y~F|YU}D78k?G1sI6`79i3g>J-vPX1EhA- z6T@DkBbm6M>8aU*nfbX$^3vk++UjC)<;KG93T1tNXODVtVvj-YXIzB~?b&+*_|>>d zU&fna5V>CoH9jnN4RQF6n-3lHJpwo0eEcpKQuw}?-Q>|-#mKTRQ4s^bryHV0tjIP+ zUH9578ja~&C|#)?P?_LXC2*cmSHionYeR-CyCTc47T ze5h|53EP-?9rMyv&Acejuq*aMfSU4N-a>b#dYvT`v4Cnmm}j-n+9IC4(q7_D<&s4H zSsN%n!LQ$1T(mLUo(RNSO9)$dflM>ul<0@HE7o++xv0YrDXU`(QPP=QC6ui%g-xh; zWu=GvyZLqu%$6||)P$2fU);`|H~!*I%Z2{p!6-EM#gh?a*y?fl)$W-0bwj^#A2!?i zabGSU+6li~QF0Uh_fq^Oz6>k2O z)R6+W1zk!-f!J;X-zbpNQc~E+nB*KV28$`+TfpYlDlDYKXHx&WN$-zcsPDoAV~hNL zJ*-8+5TKJ5j=At#;-`YlEQ{u1%`Ec%4xyHj_=Itry;{N<|1pVNdS+kyXE_bSxs^)B z=Zg=X`mqXR%XpIHdE%}cIiYK>SuOsmyTQ(u6L%|GX}!Ta0kT$iFLiOf@phThS~Eu> zV{XfnmF?Bz9n{>asB?$t_ON?e^X+oXOy4_Y`Gw zPc6&+CA<-QIQ#vl!zW#(`h>i$N~#KAmnz0Mt=6+=N@x|y3LkDZqb(#!{rZ{C+e20> z&O2kS!li|GKUf`6W-dr%@6l$WR`-|dMvo2_`z}Zy?hFc-9qrC~9#Kyeb5hSi%T6t~ zxg9FrIC-J|#o1dw-OrofICGz+oF16kYvGRtc2HlA2~t-)TffE9yr!*W3TpSN6aPx{ z2^z~lSoLmCh&#hASH((t;2{$6^}HFrik-abD`7*!=qp!!i*3zMPATDXbba-`Qw)Am zj0sl(vy@Bg5?CE+a2@McBV^hUp!1JdDtLbv6Eh1^Sh4 zWBbcji}7?VCQ2sYu9#BQ6tT0IcV>H^qHQ|&G(T&|HVq0nN12L+p3ZK8pJQkI_ zhEa16>q-DcqQA88se6j`#up@GjE8$9t=2AOyY!@gT{6{a*i$R2j5Og_PNNoR9XqDD zYDMf98pZxfw(TrGWt;(XlZLJLm8KE(7yny#)6&!`@Kyvk+BI_7$U#wBXrgX{mE<^UbM9%jxY(^*E#AeMePu0uUUZr&tRi~xn$+D zmD2!GXQ!9BbSKivWnO%EYtOEfI&9^(&oFk1p})NS=Bhh0ZtUD(U&XF>mdoAdQ5tQL zO6J&~KKzjJ^Je{397kDxuazcFTJ47JUy=o@<0h`>?$!teJ%ktuOtJy()@Q8`gY0mV zTpPz?>7x)|fvHcM>cUv(>&^wMzz*QQH8i^A!|yL-ucJS zx*-EuwUltv!a7Vt@bX~i1FfCDeOR-`B5(LC>Xs+g#lu5 zUi=Pj_9Z{5Bf!=s@e-$r!fSYIq)T4%^OjjfYq2iKc3z5X%)ETKR(CQWmP!ZCe@4N( zQqJXLZ#xgYkTyy;X|>AurK_(uODHK@&d=~>TKr1m*f%ZtEZcj@?3*}opj~A-yvvad1Wo{dut(d zCw;|bV}JDMyhu5X5Rd}6B2nM?5MOzrbk*na!9Ql{`R}(>m^=36ZEB|%10Ty)v~a3d z)wOY~hguCy-K8l)9Gj)wq(0q@?{zsGb);yQkU!Lkbr*>Yssi1+><&DhX zk|h&gV*jwp&dkM~MXg}*kp(Hcl3eE%7dygOvea(z&f%&Xa9_r(Z(n-kg)$UjP-{{y2e7#gjj^?e!2#x3XNFqhMF|Nlhc$__C5^ zYt)_$b%wHIcV24MSNLG!3U#;koz!}CrW1rJ)!oW0wYes7I6peDmoBuqmYsRHd;zr| zvt7DYVMSh-cHJMKO6}FSx=gF<_Y~GX%b6WD)0QS2Z4{QF*4oSu8Q@gnjO!_=>zRJn zvn*~jv~KkFu8eqBrd`(yB5vnI+-c?98RpzrXx&+P+^_Y!Go#&a)Vp)@cwB787c0C@kcs|*+qI>QsqTwk9^_0MS3PR<$3;(dHTi>7keapy8VD^_B z-s?`^Z)Ln8-#MF)Xq!)dw!e*RKkwRV$a(7+duz{mYiihjMcaP2_x@(=1C#-bcD+rZ zR3C#mpC9!;RvF7*zH_x;>~6hObPFWS$i-_L*7 zH>ln(Sk4bHOG8BbJ?%g$yLfif~nZdDu@@fxGfr8O^@aMOVxLq)YB_K;A zAcyvwO0^Td->C>6K_TD}*wcFtD_roMWj*bh&-BEiBr;C>k^n?L>)oVTHPhLOnF0UJlTH zxj?s>z@z)Fcku4_Xkk1&FfMzKNO{<87T8TREVu|3J`anggTpAW7)?0xFR+a_fu{|? zIaxy9yM}^% zX%xo4{ku~XiIR(H_m6p~86njW^SU8?bRcF>H1-)?>NCR!_#BBq^LlRB^`z2#&V4g$-T-TUMQcHZ9KL{@ zN5BedQ_f?v+@}jU&#}3p47novxe|!fyu^pCYd10;i^+=TVLkJ5a`P1S#pD^pb8qLX zDCVn9IJPr~#Tk1g*5~W-hv_mD=o9k|w2wOgUjquh0Vx20*qRjxF=PwX<@^o^{mO6s z+P)AZUg&66=(Jzx!j5-fz?z7AtQg&)nHfA9kjZNhqBpmG#um}Q9oS4BtWHlpZ2M}|#iKU8E zV!0!cfWUmHu(QVY_|YhJ6N_${`}%$mUCp*S*NUX zO6BwiO2QEa)V=bao6p{`OI?+ax~@~f;#9$g6S5gVTJV?N;3x&e(%TZHcbqEkAuDe} zDx?%bJgp`Dj6W$imZNlpeN;+(R7j! z%T7Rv*L*Bi*imCV$ylKUVY&?Y-0LRF%2CVycL(*mQtb~>Dl%RKA)r&qkE}G6h%px^ zjOj1<#aL;rQ!q=cTWG2y@7Gb;>-QMSJr>J6|MpwK=Jgh5KMZD-pTlO+0QdEfELsk3 zKDpw}>=JFI&wYs;M&`8%9F0j5jVZWiN~%V>+I3y?wHZUTS-8qffu`((+5)8lp@Whz zL{srl;nYIY3_~+fpqZf4T%uG|1!=BTYOWiqY(8jq8!Q!%V2?A;?`|sY;Vbhl z88I&zfRyy(iq#pFL5++h?8QX!;(3AMvV~SMlDJabx*^cEHq;6*+hXD|u-yXktxAHU z2N!jm8qeuA9;X0e+VA`>o~>#aaV(Ixr+VEI?YQ3JHI4ufa^d_!v+H6Z=V6nZK+CWF zQV6pCeqcRcNj=YSCx1+*Kw#x|LhHnV5RaZui`n=>DL5Uoo)fU3!nYGtU~RNsGO=$shUM8ELo_YAhIL zl-_G%fwU0pZJ`+b)CB-;;cs2znZ5_TqGVn{e;5_XA{Zb2~g z%J`r&_}*zy)qGIvY9@|@T9++|Uc{zVFb#Q147r;QWyK7k(}yxkhFXV*+7Ab*#1iCT zy_jS}ziz{`mM)U+s{ot$v6y&Z2vuo&;D4H>F$Y~|Vg~WLH3Y%YHOawuPMl+nBYTpf zNb?5qrO^TB;o-y4K}p_y=P|#}mnNNB7)M&hipS1pv|Jb&J1y9HHCD{gdyFl1Y;|~i zb7_2ob0UE_en;=Kk=7{B`3c^maRDjmyGIiqPsdKS4xiGSeBv@`b9Pcp=rdzU`@eQj zTW^Z%O&Q;vl(U>X5j!QKH}>)C)O)=T_cJC`M<)1%CO$7ud?=kBPSOD;QWch`L?kD_ zj7;gcP-jN3PMJzg9S@-jHJ%Tg`3+QVT&8XHX6;8NtV?J6pG+V98o$gncb#k8Q*V6S zY3?F${x3D|96s)JG&i_8ZJ;vtO=v!nYo_JQJUmF*YGmHxXg-#Utbb(|p-1ktnN^FO z)c}T2k4BO#$yu#3>d9k(p$f^E%MY3>DxLGlSnyh2@L8VAmRgjZnue;(*Q?BgXDmir z&Sa`AwwKB@Fp#^2$h~??{Vv@U&VP%gdKxaJ(coDNi3aN8rcQJQm&Q5 z;r}d_#!4rGzW`=jga)9V7%^+()*S*r8EYU6S6#?!KOvC$0)>IQG{Iv@Au^9!4=T{mB$Hs8G4 z6t&t^0ESS@Ha@Iud|KJ$dAmNOGBR1Rr6nAyLEVa+4C1Ia{Z2JC6gK-2ylo_HW{%pn zDBHFKQUKCBrusWpRy)6gcWhjDoZ3uJ-dp(cZc(>w%S(FqjDNm6_reJ>>LdYmj=W1p zPoWW`0384-WjC6e0((b6jQTRbce5>by`}dO$~@y-_dcKYI04;pF53YJEf4_!N$;n> z+t0Gv&(Yr}xKj6t0IR)#@K0Jn>rW-W+pE<-D0+U-xbh%o^dNBM0K$D3Abkj>9`p(y z_5>foT@SmtDNWSFRgZ%@>7$8J$1&HV_TT1N1A+U#He~7%q2Op8<*}@9vK4IR;z~u> zQuEKzem?Qp)Wgf?gukz+H^j#j9Ow}m9O>yF1Pkzrh>k&oLF1y3@Psh5A1V!kN==H# zW@Y50E%mU~+1DW_BQid}3i~aRmUj8|zz} zJKL1q{k_8j>d~oF96VV!Dm>3!xut{7x>@CW{&oUFE|fhJ%tC!^I#i#XSQmWbHTaA= zSr;qQrI3O&{P^RJ693my8PVLkP0%OrND*mQW?PX$j=h`mHs}xWa-Mu84mP~+QeVLC zMbdAj_9OrUNW++XGu2FW#P36eXE>D)x4Yoq(hAZC3x6JO*!87X6E6b97F#GhTIO>4 zG?xJW!PnA1SNQds@qgF0TxF^Q*6%ifh0F??9dc(|)5D}(apJJH7}08suyZ+|S&C#=6rz z!^e4SGwsHCUV0=l?tRTAb=-%;c5d8vRQ^Zj)6x&044&M-6+ZEppRLQ_vGC<9laC(j z87T+e9;r7F6F2a@9mKaSmk@H}y2(_i+`Y7^uz%LJ)~`+jYg;DMBKJ?F^#<4doL7pH z>FoajZFHmDg^7Y4QUZ+6HO#;t1kt5F`CaSqLw0U>zJtAP)dYe9odc`HdYs~$i}#a) z-Sj@-m4n1{sL2jXGz8A6B-d#w%u7NW^HfsfOhLLSNg&L8Dl!F>hCyVIO|e-Cdl+1H ztf^UAd@Bf>hR;!0$Ry4X(jflVJ;{kwXMA(~aERJo`9;Lp z+r?8iy^}hB-n}4x`C(O}(m__$HKXFD+AJGJ$b%c!2fuidbl$GlbNM23>iH5Ee>XkI z71(HeQYMkx^0W@Q(SjJ0HO{uD-?9Go`mx%kfh@lotWAkpnAfG8y7RfAGOZt8ClBdiiVVlC2&^ z)ZrFsqg7JlMF5Y~XyB-%3uPAZu5@oM6Ls`oi0ulBNMQCsesXg+B5B_|ue=<#>UB@)CcQek@_NpyH=x)u z8u3)Ib*}n6YX(vP=qj%DRbQzjXt%+yokoxHY?gLt z%^Af<{=Clz3h-W7>VTFaEery^=u7 zB>Exp%~d1j9*vv)joFaa=ERF#n!+#m9)_B3e!I5-iHoT3OlA5F3AZ=l_(tx8gJn}h z@=Y9mgFeN8JW|ECc$FO$y5p{IrM~h``)EW^jk~ple*F}qiW2KVf84^TwU~YiZ|u$x z`;66_CM!Cs<>bICao;5uyghOgT=zE8Ed%*=X2I|H0t4ghTd)ST&np^-bEEPWlLbyH zYE+z4vmM?ae!z_FuYI=SI2a)CjhkPyj{XV)8e?RbnKsb+dpoy_Ff#3ehQovzL7~6Yp&a$yDv@!b7_g#Kf&+yA<`7;mn5mO7JkG+$e9a zdcclgqKmk@{{CKJ*@+P=ZVLaH1e-<(W3K%_J~$bvXIv@KF6aXu&5 zn{ldJ=(o5uyYAOtX+AtGy$wn}>vKuXqI`wxKoUS8st_V6Xg82Uj5IXxoe*z{qU0Es z&cH6ssKjBN{#mhJR8ECKKJ(V zUi!)8Yf~qb=U>vm+o7d-Df}@eDrThgo86YScL@OzmNVZ?l%E3eR#r9CKPdp-PvlGA z44-AZWx_~@FLW)6_2wD2WExqR>N2qRmr6a$)(%H&&&~`G+zav&m6uGkiKPD0f`Z(Z ze~7JGfz#bj`LcGT&R9!SQb)_OHPNwa>0FNP_%^#PJaP%;vu$-0V zrxQDJltvf;;2Env7Y;@kU3beWy0HqE7BX0bS+c-+Jd97lACA`S>%~>pg9CJ4-Q_N7 z_!P7rNz|EsVCB#(Sp6GbrZXc_C~C9v8A5es!p%wxN4L+u*|fbDqa(W`+U9GynR0u0 zUV&z>Gn~r?^Ny4Jxo)oq)w+d#T(Y1^v)@}{`6mr=xUfA$`Kgq)T|nd%KUoF*)uOWF z(j&QS+a^A;0}$Jtm=!xJaqRRv`!Z&(RiAef6U?rgg}H}IA;HB{u$Q*!sFFqF3y#w@ zE)GpbE{pIBl5?`74y~@Gn}~NMb05m~x`KLz^h%^ z!JKs2UIWUm-o|NtHCS5I^!&~6KI)sTBx=e0w=(M3Bx;LN=Sod=J z2Rn}f(^Wo5g@DsX&mIF$5&M4;Td97}7eEJq#Y;xS>#c^DoV}L<)JqBPrPA;9e%I?G zi?=FJZq@MCu=m!2dh4LQZk}N@>i(E`MaSojkCLI!Hw_;HKR~oqB!v4|*86yD`B>2U zrhoIX!TW&ZKG})*TGYQ0JnQ%2n&0~kUvIh3UUGgPKdJiDs`-gL73=X+%=h!*@%Kji zh4A==LvOaGu)@BY#m-S#eRtUx)xk4H;CWy0f*lwK1*hY|8Gnf_1`X!#^>E#A=R><+ zDGazd7jPRLP}v_)RUc5h8vt|ws_~FI7DzK1(o*jpbBohu>-roZrsKUy(7+*l;7EVq z*lyqiOVHH6QUHM}vz$v8KVfg{*z))qp?x-3e3ziURntLh^&eNzK8G4Q_B=Y!gy65c z!OpwEZW?bIzws_ZL(Xf4TxbZsG!Sx8GxV}a=v6?mg@tm6@^LnVaIuEn5DjC{45R0j z_Lv6jkNU&bKb&XISTVBW775--||m)I2V?s=*>PiXtcM-)Ei){xT4>783kTGwLf{h=D_tiC>g? zQIw^;j|E+{mAsC%e3TK)_m_P1Pyc8$|7a)PXh%9INEDj85nVkU?O_sK@-50_0NPvy z4Q>bt@ehXED=zsWyf0J3QwnZ+7YI)H4DkD>ey z=y;`|SQOLhEuuHc(uaoC+h8s#VlJ2~%r z!Zdi>KI{r_*gcr#norzHO5CcgRB}jI+!o7I(TEq6a50nktDtxZ|9B2UJQq2fn=gWs zKH&~u!cBt1O@oM`q67s>%w!1MK|5@QBq)(v>57TBkC1> z(pw+`ppc{>mb5;HI!W?9MW6gp%U6|zqOvA`ZuHe8Cux`_zatz6MD<9?S|k*V^@B0S zwD4SPrbd9pUP{g!VAH1N$)*0*O0|Zk+7nWDA{a>x32tKV(N{eKf5FAziGCn77> z6Jy81;_FlD*O5JRFXmv;~# zx4Dl3a$jj9!r%e{;gD7qYTzxNz&2=J`)-~Jd%hAjuUsVmy;=T8VxIq1Y4y`NV&Zwy z>;*3w@}7tn0MOQWKmR+nz^JLfaKFH$Dc6F(@aICoFJi7eLqRhu2d5DalHjYjR|x0B zuQwzcXr=g>;fu(aVt8B-F)ny8E=>HzMfz)>9(y*^r^DFO;R`tAei7ntytNySOXg3n z0w}b+8D4g?);IG;B!#@uBJ=h8iFU09dw9 zan)Kt3IKT>rdsm_QuDRBT3xAD&8hYqvcwQs3*3NP9@L6Fs+N+ddwEc0t5o+}r_SD~ z4uq`H+pl%wsF&Gb1m?KR1Zt1lt+-l@MV9JS%b zY{UKNhJ>Mp#Dj)pfZHMgZhKq<$QWwOI&kTKczX_%QJ~X=cRDV&e&(q!J!q0g)>Ssw zRTbCO4%O8_nyV$M%7>a;aS`JUO+<;7PMsDY-uj#^t*y9a$mza}W^D*#Ymh)|D6qEW z)M|{ZA4j$_^|ve^w0b8bbeObkLRz+U+IF4V)M(odhT7EEYaEEjL12(#`zg+L>wxw@ z;_bGB?SNcMZ_!St+kT$2!*ZdGC8k3m$FT#~MmacM+qxmxLc(?2bM9oTWblTx^0%}u z40S$8Z=GE1e9YOU?b|65(^Z{)%ZIO1O0rX$v-^c^h8U0nIHYnmAiJBKniX2Q71Ntl zID7o^mv!8R#Rhg;BN4MYau-jj-$3dXME=fXLkbb6X`?L1^-F;xG5N&yJ4)yYEsrT{q6>&b-C|C$0|s;|(cRhHQr@BZfA^}nzS3El;{ z?)tpj1>&s$u?<++1ygsUGARJJjd@2wl~G{Qdx0wyr0ZT%FeUlI9=dEVVP!8KwTqnE z$SY$>awg%N_pwX8MJQ4+bw8Qu055z%vaf|696IHRCC_b&0}yYp|fROdgW%6}5_CaID?VLuo>8diwp@5N{l!H49h0t4BG zT`H1IYD=FBHK~1-XZZ`4wTU0Tk%yZKGe^tZPBU;R;j_l8{QegjR*Y3D`k%XAxwCC? z|K9aNf5Pgu5b_uLvc5INDiaHq2JzMlBQ312axZn}M4b%+DZx*7D9G|Jn7LNt=yz`lk}h zHy7u1-4sB>_b_t_kZ0t%#E`dq^GOkErt`;@*!dKsEqOjQ&WDeTPKq)mr=e4TniZ~q zOvYx_@-5)aokaT6IxYV6FP9y7?NlhQ$cCR?A_x zxK?+=S75#VPPF-Y15YY$y-}cWalPq5oxn!(lRoneVAl(`aV+z0w7sAa{99kx?2x;k zzS*htd}*`my{zC?x2ihe@@g2RZ}sZfEp7FE@fF(9xdSy^%kao+I z**IUJy}6`l%e{GYYQ`QJSGc^lkX0wNznJGa@@)w}o3Xzf;a4G^V@W%)ztV92-N9P@ z{mg^)=I1L18|}v~Z@0SD;a0y<=Hd3x-$<;l@DX))Dmv4IWhOP#<@Fav52>3gnZnfL zU9U20?W&wrHt-4=?#GSb>S3$3}>cP?+ckX$8 z#d*-G=MBbqTG@ZidMU-ztJhav2VCCcO01Dw6&p~A6={zDTZ!FU^?kvZaPdF6ykcwq z?;#0SQ{`&-wAO%GuY~J`^)*7EwSWX7CyTzS_e1y^#2Dwt(q*5(hYJn-xi|!|l&yQ& z{o$hZnR3o8V_B)?wP5Gw+g$r1axeEngS};@ZY~(tOFa4*8pL?#&IMS#tk%cBk=W;H z4Jtxm)bOx7GPijt^6%b$i~uX$;p;P&`|PtG37}!#9|Mh=q?{pje5NN8t@h8q1eqt#=F`o0v44NLjqpNuQOal`EMX;Bpr>2d5X(nxBdsLheQfjT&YmOib}?#`BnqV2gau~PKj z^4Vtm`5L3w*BaaW-(F2%DM7z|+}IHa%1gX)h?aY;&>0HPOX87CQ~1=_8A-@Xe&C#@ z^i81)W~qAdhtl;MG!0$y+cv3EOnmQs;mQd|d1yIF%tvpyYKl+~S|yNAHI<}_k;=zt zP)jgs$?(q^mOU7~<4P>)bB;?s&R7zwRRdQm$mqene5xjr_@IwK2+gp|$Fi*w)JxX4 zGF_O=^=JmwD^K+Xf@I7^u87Olh!tdq{J?!@Rvf4UT;9k)b3@~5USn`%CaSP3KhesLe`u_>7oU4~`Bx-D zXY5g5L2IDpFO;~>yigyZB}T|L9y`3O*GC*Up7m(H+qBI?FEJ$3D|( zSW!Pe-b>9&pk1fYgPAZiT}EQeay{Fuf8xS!`Pq99wDu5R&ItFHGX7hy<#bcxD@~zyg<#ZbfTqOcbGM`$&z>m+#pFzK zP43p+Klxk1YjN@hyFi) z9n);>$SAm-;e5^FHF0pD_E}&_3$3LqqOGaqPquWmxp|9S&FZs1#a)iqzC1@HiS%xx@V=t5#QpS;*G-|`5S*&~IA_vG57whLEmmh4db zc{?t<74K};qEQFmMX5V|(k|kYdRuY6rTc;0&a-{`M~=19E3;(>`}-*BHjNv1^sh6! z7gz3C(a!y(6Zr$c;q&!wTlsER`rIz={=CBCe!<@TI@*$r$DL!>{c68EE3F5MhC3IF z2Ow-ntz(^EgH=X128Sih$PkHvV(#m94=uXjD)*n1tO zn2Q>F$>Y7`=B(bMy~F`JEa#;x;;nA%{b9~av))VF&s!hu{dvy&n}(aAh>xoAc@tV6 z<9=^59v>YRpYJTb7CgRIBEIHwz96Wt6RoeapRWsxpPPuEora&Kz29&9hY#o6$yt66 z`aO8Y?(sP=ls4f@bW4=Ac1je9&Tl(DH5&+wH(rk>E`YV9|?p zefQCAbuYp;?LoiyA=K{#FXZF^EtMyv#?R_VB!qT80Gt)fXab=tqP;i};&RjYRJ;*m zwku`Fm6O+vt0DB3yz4i21{!+kyS9{^@j@Z ztMcs>`3}*TPR*EZhvW1@e^Cr+AZCaXGr}4>CK@}T89U{0oL-nKiX{)kE>dEbSy4m0 z^wBagDKZMFstU)mUjC>fQH2v?@+aZ)`xMkEU>=JEWEd3a!EubHadhs6&1vfc_-Fyc4{VkH@_p>?J&+*A&pwh<`&(i1F4L zda5U`peMPP*mG9z1t#%jW8xcfqO4d__nRaoLXrwWSDr0dnLhawUy^(c9Cboni=?K# zr>3))tjCsO`7}jWA?1rH&|pY01*Mo_R5d^sPxSdGLBYRG!B|>J*BMq~PGFxh0x_Brg>U2LHOR@oRk>5VO+ztmTry2HYC_NA=I zE!sMQy6;b1aU3qs=XM+Br~R4qTE29UOuAQ|RWICXfDh-lnsS?#N;f9UDYcKm2dpb!gXYZV9I6TKFE$1fa z<|U#C#t(euInF%$CJk?>7H`^BN~W~m<>yVA8uGsxC}?SPuApL`G!e}}WZWm@s4%3x z!$u46=YKGZRAtE5aLm^-%hyp1xxSY3B!KhFAcL-AUb<%Xt(c)>Phc@2J5$}V*`w^*l@Zi5>Uclt_*LIEya7fDf zhDs@VWq1wg_&t`v7)AUZkNx(#!(PUnz?4Eztsqcl3AauXQwI%l_OIkOeLMU^j71Rh z28sId#PUHxsW>y5-GS?*YdymFMQCwGbYc2VaWg|nD|<;dP+u4Az4vUP7mfV}zrC}1*{)-mqnTY0b6HMZ+3Mim z`T}Els9kxIxP3rf(OSTFMr1h?uKewG`Be$Ki%u2Sip#GfE3On*aN#O$3{~7*Eaw)e zyr)!oTcCo+sgiG~^3p*i3n1J|R0$!e<=Buv!`dYS*s77HDk)^ui{dJveVP265Nud| zKU!L$yG3!E(suf3Wu!PHq1Ex-K<}O9i(o zPAP41XiJM*3vF>I#l6ttZXvF7f9~tP$Wt2T)fyHYuZ3P4ew~i_om*R-*3+FHGPvf_UBD89;6bZMUDwCHF5l^{ z;ES$M0;l61QoXpch}iD;Q+`a;6=G`TGwzk~pSpNob_WW!Wzuv5v2fOOPwsY)py2oP zKKhqxz1C7$Dd65;4q4TGy|rv*O{Tr|1byKoWz+aww$h#5PF?Q)UDxXib$wm_4R9x` z+kPbbG5v?t{gc!E(-)ZjbyPgCzA*bI8eWZuEyeTijKFr6Vfz=bLz-)y*WYLu9S`3T z{B(W+zo>&BKb5;xFMm5g?hfInyUudBLUQ=Ba)h*lI6{L2KwtxCy%Gko>j&{>;CVXP zl>8}l4k?DJLktDknw#B%wf&*5oVg$QN3Pg20i+4A}kp{ z*t7M`hL{4z44j8dy!nNNx&{SFdSv=7g~o0A`-Yqv3XaACw4-eJDmn?q?KS#;3n8p$ z_?vq$l^UtlHRyfYFZ2-j@7n zAZ2c?n~o!#si7(CeKPZEYZ4+dt>WmD+B}Z9oCyt9d?7uB^=@|6Yzp*lHf-KxQq^Zi zXha|m=Z5XiP>s3p`kzm~O`rqjXji9VWeeAMD{Tp9GY%Re)NIe=<`47ceN5(Ex8_M; z^EeIj53U-~Y76&;ZST$6QU==T5CEfq3y+EHpX};fu{L{d0%>S>A#0Ji!UTzx^?wHW-j0o?H2~Q3M?u-`h z7-(RG#&)LrE~oo1cb0{BS2=dq>2^2iX4V>Zhc9=d5rUh7d+X|Z$1Z!PiF-hq7d^Xo z5tuN(^f_)LCLTBTw(CBGZy$Fq2H#?zP-~wgXy31U|Nh?oL;3^a7YBio2h=%3zsPkH4>{e{fV3B9iWhV^xPf;&1 z^B(n2GC<4HFl3#U~+?EfDE@E5k<{Vhx+Q z@Xb76df`u*x7Y$Hn%EUniR!Oi(JeeT6ke&@JJ)YGZ=O5Xqg!pUxM;(ix4W)31zq&R zFM3_C2N*`)Ucg^mM%5P1y+V7>TmsRs-Ol9#@m0dxtM}`>BrF?%mCXYv?q6U+%Q4wi?fmp8})1n~g-26+1idIpDtM?^+N$Hd0PM|@3)O#-EQ zr+a03W_#pfBNfDzkv`h2+Mj=9bpKUEY9_>V8a5UR}=MK*I=ZXtZE> za-y$(d~9wJF+aUByS%ipvAwmsy1Bn|xVL^Xb$X0OT_E8YEc~1soc~>!S0tXPSO{q224osnhWE_0P;EPmnPYm&E-wxuh5N!UUU& zS%co(LC-~1kF!2y$U;Pf(8p^EIp5;G)#tu39?wz9ad?Oynb!*}e!CEEJt&wcH?69n z^{ve9siijSnXdI;&0-P5RDsxH@mi*Opf=mqygXj#x4h z*$uE|(Pc)1(TY)Y&h{tz;|bbHE;BXUn60UD%g)M#+B5T+GS_n=IrZb8OD$pKca6>N z&8;;lzZ_4m{$bo}2e+AaADtahaZzPq9rS50&1mcg zm?Dxw&s+H0gpS5XI_C6<_g2*_5gHySy=eMX4~p04>^b^c>0ql&J~KH`f1G)T3gw&M zJ&>#f``K>UB)3E6^?1)+$@LV_a}3%b#qaTCcB(Z$6?3|Su<=HE)HmM^V0~ee@p^qB zJ018}c{4j4l*^JEGi$#Zx0jD1_AC8YVvBQ_`@En5n{~Umjm#v!poz{8_xoppt@tOI zzn^QBFGx5}lmqJv>`L-BtdAq8SXenq;J_}yKZ&idl4A9ahoj`%nYRBfvE7y|y-Chy zQjNnHy8j4|r*FSCh?nMNn-`nlLHk4L_?PXW5(M3y*sQlK{t0XaJaC$s|1Gc;eD&ny zjR_d$y)OfXdt$MJ2e3#AEIYY|9RuG^Ah!PrY#lQ&lK)L&OP(KD^@b%>yISvC%VD^& zRw(x~6CY~HX$v31o-UhzCAN2#2(J@cSkk-@qsZ~1FHa#(Mb|^}$>mZ}bH0_j_o9{4 zrUr2_YX^d`uydizD&h0Jrn@gL_S?wm zWo8l@4Jr@&qGsv>PK~bgj^{GPg{CauUYhQ7*zVQ8obIH)I$L%Xa%ozND!M|eygCG( z@4vW-VchGL2Ubx=k}%W&@6~vL)i*at_VV2gD}T4UuNgXmE8pGb{^U-53+L>(6Bkdc z#e>NIdwXRTC(&~Zi6=?8*IjxX5wCzxo{ul)ao$HZM>YF-<9VeJQ1X7bJH`om-IqdW z4Q{1H7yG=cOCho6ZKEf93H~UPN*V$F>-7>$eGtRj&iL)6@0Y&RhxXu>CT&Z-Qp|eHk3K-MyOV!>DsV`X`%aX7zv)%!T?T=J~aP9akT6+6dVzPlB_DsIhiEe~e3Hj<*G8jfxFlEITAYf?%sdV2zK(rTr_oz)FMV5`7I{(Y+_so7k@!3$VlScw(Rn81}=w!`G> zKI!G3`5I}PotFQGZmSliT4|ePhHda+OQkQ;hCWJ^-B4C~attW*=CXJG0K8uF8N?i9 z2cbdBKWJwwpNTcRQ3#d$7&%8p{(`RA_k&mMy_LdrXzysKWPIMe%|#pbL0a?3;Pa44 zhqGF*+Jd~tVd4Djt99P*i+&9C&Yskqz1;M+a9lZ`MNZl_oE5xqSrdr4h#mhL#9ZWt zQVYj4_}1Oo-LpG+5PgG(x0%GH*o#z98(R_CLI&IS;Bop+XdB&X-co#$E1s$8e%8jc zJJod&wy1hwZI6X+_q&DfU+dv#Zk~4Fdh_Wl**UUPL4Y7=%xh_!l2QuS<7Fs@@I3wA zi)Xc+qY8FbTXalnJW%COuwVYPMiIq^e0{b&SV&Nx#1XH{P_FEBGIN!Cub|UBuq+-Q zyPBWT+#s;_6=VKN^6&j>QdjKAg!eb9zbDIf@Fl+DvdJPA6+jOL2bQNZ%5F%h#hP&I zmt``xeHCr0xcAdM@0qYH@8-n!ha$ zZN3lN_kN-7(&7beBIU5W{OmOU&{hjiC;I@)C4cFDDrUcgItryfCO#h*CpsMF5H?s>kzmCs*!cejPj@+wX4Huc-<-w90hZ{I!Y$4 zUl*5055-*_XUo@bD#6iXwO1#_T3QV{^5>JSE~l07zgjQM`PEqyFg)QSf3Zl7L3YVE z97e$}Rs}GqfuM%tEcwe#H4GX7{H`ka<*p;Bu7mv2Om!>>+j0TwH$ z@nTILgPy`%A>oZzXK>8bIdHk9aJ$KC2IXC;((@gKmk6)dM`bTDJ1>c7FDa;(48lte>Gg@iTY=a6i?a7OJ8$J^Z&j$b zI>K8M>HVDo^n(|qs|?b&0~tnxjG-V?1O{Y|1pTD&vEudlt?XlC=VKS`;{f$>Lio5K zecUL(9=u>LWiZGN42}l-Lc#tBa3B&KOaaW~Lc*0Hk#>;iXhB_#DcD~uszPV7}e1vZy(zlqxuap;9LGY`z^MgkF)j<8~5Pl6vza|R*7GD20W&e(U zCAJ8E7}9@$B4FtMG_n0J0^5Cxpu@j`tz8f@ItVMJSQ}VA_dbO3F~WoDh1IkZ1PS>kC;K zL2MJDz3aL^Qq^Aag>kBc=~#tv8HMpd!d`b@4=@N&hQH$re+z6A*oTW4g-b%hKX!*p zO@vER0$tv4j+j8RC>ztL2>q%Ei|ehh<%KEOiV&`OeAG&EMH72Dkhx|7+{FbRss0`*qn`+0?N2zzPRM-*h-++ zD;bwH@ipyR!~{6>F|JVP6UzAKIiaog@qpK>T{6B2a$V-_2ljG-vE1&E4wVE(qlCVk zgn{k^1hAb;nK;asILn;45<~1of`j!4b^Fd8z~#Yq?eoaF9rpiPqvY>8-2ZH&ght|j z`#k=iYn1%a4*%0A;p|{q0vaWTO98w!pgQRXRv^#9BXT11p_`0gPpK(@28Kt$bR5^bc`BiCE$*O+z1~a zG<`@xWU6^Uqa@PpC3sfkKuBWj_bs~HvtHzZz?T0bZK$~-e(G(dPcP)gslVc<+K#y_ zrJk)0Gr*))vD`TLw9J0%oviAirwX+lH^pSO}L|)vR+u z?scniAtY<+lSbS{wGlJiY1oTU>!V=gm7}Uzu`pzf0b<#~m__{K5_6(g5(r)QeR6Qg zQii#dIUiIk6`jsw6cO6mlI9XdY3CH4b5ECbOyIredH>+&y-#J6S4gGE-W$pG>L1jp zh~C$Kcu;H_@lFKm&7<2N`}QpEE>BO?41kI6J$A<8y@~Z>To@_z<7ej+FR~vLwTxKb z-OxG=X656!1ayyGBbycoD>^}|vcWzP?-m(4IJxo1FaJLc1Nf9q+F zdv7h;?&x)3DP<>ed2pgW4SX`uP7xYgb__S`n`H|plxImfA`LTrg z!F{Rao3Eli$&#zG2U+WUxJzo;Awo9b?Twg!yLD4RoS}qJSK`)3{6`WZeP8ZZ(zH)Z zR)560{Rmg^g!vI&j2Ek*RXPYN^8DU9JR(vwBgIdH%0`9z8TOkj2u9-RG?jhlN1S64 z4MOi%xG%Aji57j;1Y5WAV{g;+gpw+o-F}^bj(kJ6ADYtoTs?yko8xC#Oz`&v?kSQl zA8v--JGzZUqA@66BX&18!i^099eD37cQYd{I!_0FA2g(GV{HaDVYSQZ?yt-W^UL1& z^r*R7A0$$iDHh1- zBs7*m3V$ZQn7v5KUDNv4i2RCq74sUk@7(QJzC(qb>bbAJ6ki+>8Q_%Y4GmI$mHHM_F)heQ_ZiNGAlzhWJ3U8HJeHk~= zt0#68)1JS|U54s60v*NvS%;hGx9&JfSjk@#K0?9mv5ymP$*#Yro`idg9jDy+!x|1k zK*y;-qeN0|axkLfIOAEthTJdoa18cI<_Fo$&v8_P(qr;V`JKiG)srN7F4m7!W#6dW ze>_$JI(hVSW<;eocB}zrU1V{&r9rKRXj*+!VvPyd);Xg>^tx1*MVM{vngw5H?v|Cm|j_}tgZ?$`<>)CvqV%`SC(LI(Rn_z3F9d`Fxztw5SYZm zoYn)Me(uzD^Eh)n%@19Ef*73^NG$By=>K|$iQve#H|CasI)bAN#MNC>Llz&K$ST#J z+xDDXg2OP5q#Op7>S|Y3qHXH4{!sfSGr3AkW`;LR>d5aH|3np*6D$X5?q0(X@4VH9NBnaw<#6Jl25<9qIwt z`?DlFn1%3B;x%mOdI}4>I?RfyU6;n#j}_6KpgQiYknSCHnzSqpHC=Lj51MN}ZxH?* z4VuFxb$b7xk))IHiXkxRVntvM>0=0#QHZfNl(ZhP!ml2cTj1=Hx16i)UY&NnxH@!A zx>)0JMGnKSPHHhd`*)AdSLj{2;B!|ecU`XrU0pASF&N}sw<~hD+q`bITuz>DZXU^Q zSWRw3b~wZc+eW2(1QgV_?A$M^TsO1acRJlKqug)ec-&#~paA5mIB~cNPiY)5V=_Rk!a9<1{Yr%-7`!luA+pl=(YneqaFB13mcM$*Y z5L_JhSVM1_5|AYl@>3btK*9>xjw@2~&9(z)N`SMneRH7TA_TaQ*UzI1!UFPypLq*I z{j_ns8jzrO(IDkjND7m`IgW1!803>>m->!R#>WR->|MYR@X*k&m%=eD8iJo4V1@{A zISZJUfXqWN0n5rb(aM1nOzxY8fpJ}dW6}3vyas1J_>RCY#Bz*J{$-ctQ}o@gRm`XN zn-9j$3=3s^(?0k{OfYs%a5V@p4hIu<2g|Mmlj4TlXAZfe5<+DkLI&`L)gb_Ec$+Wu zmP+UYR0tCQ$U#D$+J~}qKdY#+>*L6-BFU&QEn4a?x?80>u{R(5g^KN&u&b# zA1cxT75!@>I(Q>05E2b6#Dy`(gg|0|n^A&&OlnL_yirU#ZfqthCM_p6t2)-o{@DQ3 z7g8Njh8wBD90$dP6b8p>*~fj}h^xO=dPKKM#??t)f1l`8$M;cE7~^8~oM<)M$9HUi zncgQ<#>693;wOMo2{66FmoUGP;5Hu5K$ft|M-5Pi+sui(koYZB;wW>{%tYe8QPPop z(s6gf1!W={m85tW50}IlnxH(uO+Kkkx{666h)u?sOu9RnEPpd#BZhiKC8bo`k)nr+ z9AFK7U$Ba&(5q6>@~1v`NM)+QKZ#Dw{T#rCPUWOZOP^2WQcdG_z@%j|MDoT`4JmsH zOs1YA(?nv~uCmiUCU2iLqovea`r%N~Ii|5ki`w-a{#i z&WI_w=GJOOGWGc>wR<=Wje~@Hf_|#vs!e8cfN_5F2U$$w{Bpo`G{$vi$qM?JWfPs{ zUz~c#kR9_lHE}>39qpqpZrvAc9ZZ!TqUu*;loM*45H;x#<`595niCIDY2!JncXI>V zEq<=qMJqYvQdtCa+86L!6thH>Q047>v#FH&Srr>yA(dA~1+K@-pO7R58?%kt1CiAD)^VgC2LsSI-GB<2oFzTB>IT<+MTQFNwu!X=B%=hGv^A|4m6s-6bPUIHO z^%Sng79xu?`xWxbJ+jlFOoyAey6D1DJY195qJ5{Y$MHL<;%`LXZ#olsa8SYe>v|E`ojHL{DKF_5~=b}ojYn<8qvYzF7#H z9(>chELpLPFRXe_j*ndh%5mB%dn3W17p*S_x=u?$sm{UEt3eRpCuGk@zN&ZWr&?G=mNs_d@G-Z?<^# zl3%d?dI^Bd%y~egWaBiKwJulJzNu>7dHfvHkh*FF%)`AAG#Ef;p>MX35j2yV;=XrE zX6tLgi+^~Z^<{QXxG<{C8Mj@Mxn06OT$Hk1S+KnZZKgEc{%t#4MZI0jDBO#$LuV`JXs<(U398Td?k~MH55NAs?hiu1xkeO)2jUEVU?V8L!5g6@d;?$EmKfWGd?{O+jj?&yoo$BIBDhSW-sP)xu{ zE8DjHtVa{!ErDNc=NDMI4Ju~SEAQ+5PSIOWgO{jQqxHN7*oD)!>C5!%i=VHSBIV1= zf7y6m+r-(w9ow_>J&Jp9}VyBAp%8aZ3bhptZF`LXZl;t3o(<)`vHy zKB%11U-`L>La>dPrRk9loMMK&K+wN&yB8HZ(DA7k!Z6TI<5IXyJ?7~)82{4Hgn(S5 z?nyr}m8_i?zun{dVTTYn+YHY89bD0b5f`d#BFhm{%VE*7Hj#pd;K|(i_~smvwg-Yu zn@t@&pNDUjRXC3f4}5R?rABn)!ckYKjx=vLD|u9rcKk-^m{kIqoW{7-`)i+vP3pLl z5Si`exY@!u;PYtJL->*cK92x0uX;qF(DQ&xgmMZZRER7D@OdOWi%OVKcs~)p^Dypm zBK7iViqNFY!bFS0WMJxK=FFs6%VeS1gX{#><5gBbKn&e^Qf0}c& z4Nqlf35|B>eifz;s8z70E{kc-YH5-w@6u#%=KgeQdtt!CKPGvQMAt2ME~l?g%u4JI<{Kx?(AHbs{zH6iHf&ovRN z?WE+LoM6oGTF+RQ@FRZo_8?JbI98RJ`mP@J%D{Xo{?#rq(H@cT9)@t?lwvQeD013(CjI5X&hI6OUCQE%0ZkMi-mkLd{*`1zK2w%_Bqt|XOUEZ zzr!>XXq0rl#bteQ`PfkCmf@vb)G7a?i-(HCjf7_{tYPC8rT1$B|*BXaHiJoXmduujOIVlK!_DF>zMcbwZ+chC(?;DV=q^FqhFcK_Eo=X zYD>iYWdn`iF7p4dfqL&-8%*r~8#Zvbhe)pOnhm7SlKboV=qC!213VvFh`VF3>)E)y z`2bnsKWyMwRN6Hg=s3GH_it>V#S7uTY@qli;U6}TH`o>L4;#o;rs4JAo8f|YB`4td zps~UE?oJ0{S_GdM0iF-0%)e}4>i6dj*GX1kZ+wc|ee1MoUU&sd<0chajp&oz+=BxypcQ9u~n!t=OYY7685P%K5 zQZ`6(!~VktqGyso&vUcg95?_r@a|(gfDNp?YXGo;EJ^7=(}ekVfD6C|j#&bpkH2i- z;SXRouA@dJ8)%y385Yd~Y@nw`&sI?rN2CeRG|3|?=y;yTrryf2wOuwK#%gBYF9)!J zB#(gAxM`cMovL{d>#q0ce>P1jug%O{O6LE@2HrdTuh>AD^*W+IY#{xH$-mIR68n9p zf1!cD^rilV2L2Lz`Cjm-|BY~|6$|f~|0?qr6x-1?8+fVQFUA<~j8}kl=XV(QTlN#* z&+=v!9uiswCqcbm>rckdyEEATvVoR*;L}MaE?WGlpN{L+)4!K^Y-j8YqO50~1Q6D9 zZj4Me*K8nk$dSGf>1=*fhg>?EAw(@7Dwx|lLbM9`>|8B&t8G19ui3!iIQxxk#&M)b zPL)gia$zSO)pqGP9b%($&0KA*=1f>^z2UlP60s_MA`@9ei7!I-!mXb{ZnkL&pc9!S+Q710fXdvbiNzU(jb#bR;e-+gd5|ffsQq$5iGPAOC za`W;F3X6(MuAh(p%hzN4e}OFiAKihNHv1n1f!>kblLRj_gn4|z{?vV#)*Jjnyn~JJ zNVh+nM`_g*w=7F0iN+I;b~9iNp7vUUQkF%La4_prnpA?6^*D9J*D~Xoo{GHxcs;Zu zO8>kbWC@D-W|-^I$*RJyP#fU+_^-)gCp-H88)UI%cSz9xfGn23v;XdzEM|Nt0lXe| zSfH`mB>ANJ3pDabTyTUC77KF@m){CPbxBh-P{0} zydF;@4HqGmOkm&+l$!C*@1Hvm6Aq4D8j+p9R`DB zo<1he=@R2Q3<>mq`jj!I>-o-d=(`fh=a!}2?2kn;VM)unOhVn^3LZn@sUM#)%XjyD zskFK#i`fjS`Blb3Bco|n*llB^zaEx`HIC@L(4v&lA{vhDYj?0=XL4p3 z$r$qw$9A=4asej~W1FMcxjuc~s*QdtkKy>mkBoe9l$5khCs_6h_~n#Kg8N z;r9Spe0Y>}MPMj=ga4EJX<0IkP__te%_mPTfGiF$6nVt|8Nyxuhb(5U`Rr$PoJL1z zB=(+PA;jh{Su9zj5LSAe!4Y62rOmGxQ(2zLTc0CmRHGPoczjJ3OEC}j{VpBOme4Rz z@XH;^}$eTO#9~7pv{p2j?p}g3>M|G9)K+Vnr~QPtXfesRvLIx zU&jx%jXb(ZQKm;?qs7MRyMEe0Nn6>?yPSh&m>))PGdOuW$K@0B{F#DGg7PSz%VI z?on)(t#VJH`faf-eb(3( zg{Gq6Phm$sm;T!C0Q4M8RGtbHrQS2f4hCoLgBZ=Ur4(a*BVeyH*_{`d_0dC#tH)Ua z1sk8FV@HxXDsmM8oVeCuq?oKCU!K-P749%x342p$Yi6ujgdVNhttfWYFwv^2L9`0n z6hkoerk_3_n+L>--LrPS8!4$Z!z*nn!!{?=z8{S66IJ;NUG7-PBc`Lpc&g$3yT7X( zXLbX5>edog94t_?r!XF%4Y=nbt&%+0VbfGpYvw^L*o-}X+MJ{DQ?yC1jU))##wd5- zt9G$S0f%<*zzzZ(FP5(FK;m+Tp>Y?>O!Cz|O0dI7`MQMs$*e{L*k0`L#VR+P4`y3< zlsM(s_wMbh7$3T0F6xAJ5qdLitx)X`0`)MvK!4Z?JD(4{!MmENk2GJVNwq1*)TA>4?vzoojz1Zd2`5Q%Nn5O%) zdmwqI1@a)fjya$rrsTB5NN?Y{mH_yD1Xs@sQ80t6qxid*^UwBB192|Lk#B{T?-cHj zm0g|S`Utfc!1uki1gfPiaN)LlN3%y)XAQ*HmLMm_xx~}XqWUczxYIKEG^%nfQ;T-@ z@IQKLT7gTu5Y zD*F%04x+&NWS6HnFhH5@C5PiBP2nvc?e&S*Tb9Yb_pY-_wo_@RcmJBVD#E*aO^)M{ zg@6rc5!-wT1Tu*RZFYc+DSRx}K{(1jxQ0GvwZHXp_pm-oiCCIq}Boqt*6aFhnR3tNVv3-mQ;6`_(r&_Wca7<@UP4fN|F)pAQ5Ug5ucgE zQx;vJ%6_$W3Rp(LGy1rU5M1(;kn{CO8kJBwzNn|Tp*2x{c9N0ye34Enkev|On7t7p6I>RD>NwZ#I2Tl$8)bY0b9}u@d=p=M zmr;D1ef+g32o;ax9a$n7mnoT0yb+5qiYwzwn2t%PKqZV$BupA5Ol>4A7$t&{INJ}E zb6Q;&aTCY+68G#A4c(Q-t%b;QYj?->GV?R_o&h(jME>pq>E0bi|3}(p@C~qB;V)M z8=-OE(2vY;O;)h2QeA>?is)F=xiLa zj=#8D7-t$yW}5J4x=d!7qO(AgSy5Kf!aZsJQrQkYlx`>4H$$^RRq5|9KEP7SiLJ?r zM-zlk=4`yT^>)h%c$b?jMepZ;BOIHXHJK|_rj)dqJMbv7w1@8GOCIsPJm@4%aZO%I zEKWKVW#(oc;s$wsEKa9X{#r_2k1n8j#>$Od(cC^PhGll;^<|(z>+NA&+WHsPIFc zZk0S?HK(jDwPpI8?&hn73V$dNaV-jREK^Y_`^Hzs=T|0pUdBCDCW2osKwbW^Hv-VP zi`JG)^p+q0EaXfHVH2oOP^Tj6?B{!6|}_89+8sK4P>s6k#O z8!FWQG=LEpojLcg6sY26s+x_ilJmN@1WjdES>r>03qWnRs+_2y&a6=HJPgz|4jSkO z4VH!mP*?k%Lw}u>9@;@er2}Hrg5tNTlc@tkGA8Bsq2`|YtUz%WVEE#PBzY_< z^<$3p2op=Q+DiS}`q{Y38TwQnpz%*LWe_cH(9iKwlKJ#UUPfj;enmc<37N(V7J1yxP8u==-b;I!7@2c_*}S~zH0c?5x3$v}bp z7GBdoUibXg_cCoC`dUT(+a{=p8Cgq-F4_oa+CRxi;0d-XIJJLca~rP3otSFZyl5Zy zYgmYD&=u^^cWMUw;s$litGyjRWWrJ>NiPLl<=E;v)H>TufW^s9C;v{D={7ghQV*Fr zSHZ4U$1Yo%F0cG9u#>AAP1Y^{_R#qDF!gSy+a)0~-LaxyQEl~Z$w56P&rP`^tf~KXCt)=Orw`IE*TH7WW(k5)$ z)|21X8{Y?5f?zcLZ2o=2PHjVMtpn=)pAaO#32LSe(=}Vy>0Q^oJlzdfYhP6d>_D*P zldc%7j(r;VVV_lYU&oWm`m_A{j%fHpANaX?&xMTT$#x}^KAz)NBQsgm+%qGyssW+| zc>*CF;0{C#gy7ECUU$5JLF$6R`#Xd8Xor?g2Wd5io;nXbrX75eF!a!Dh^>E+seXv7 zVCePC&`XVB&dZ^L$bsE$9O6#c7R|^z_U?7I5iy|=@c{88f^OTY`ulYqS#18aOrwt^ zM&)JwZ<+O|CXA|U1oR_E=PAZ?vU;KURXUd*hDy~20RgbCDwCa2?Et7nLBQ|&-tj)K z?BsqxWN$0m?^H1EY}OBW-CZ>hLNW*+GYrBdV4T2GJYc%h|Dr!g2qvL1A*tacYBmw; zJQ3etyTwp>L^GLMZ>3RCNxlYoBROy!HMvs@9DyMD0sd(6shM#fv!|K`8xx7aY3pCb zpaJDWj#JLZQ<{|1)O^z|&fVP!;%ypb)f&@HGv&PnBZFprLnO`os%dr3XIA2?4)v_PCVPb#-?Uvq)NWr27$`)T#88&x3#U5N+F;xE+< zH^q5H&qZMU9^o8!+LSm=bAL(*vn0_OxhcubE-Zz_T0(qZz+qhodACTvTO6FP;dv|a zQKF2Puo7onjum>rUPIG*Wcu)#-#0qlFNIT#v$$z9E1GnZI~~E>LaXQ~)!z1$)pDcjQ)6vJV@8DIotonvg*HED`aliq;N5Ls zjvdI1ZTb=-Sa@qvb4&3YvuRqrF;BO9G`)fJ8(7ucT`z3hTHOiQAD+6}oo?9KcEQz` z?lCsmJ4rNjk{fT(>`A!l;iBk07v5*<>eZd;wGmp^3)shZ-6yixC!z0an+~`;#Wx`g z3+0fYB!=zJ9()kmi(uG&{Pu8k$~tRjkI7=qjc6O=vZfo)i`Y5fqTh=pJfy^Xkm0|V zW;%+79Wu}DvB0-`IL1V@wm)LFENKr~)N2Q3k4NZs92082O!$ALgUF_dHD)YCurlpY~I&Ujg}Ewv`b2KOS7BAfagYv5**d) zivky;;!45C4Nng*&l}!gABex7Pf!3T)He(g z^p`CDC$No+Ob7|j$<7OmD9A0!FHHZh0^8cm?wqh>j73-D*O;N`k*KjCgwLes^o`m1 z;lzSb7T8#7d5jtcXYi4`5%exSEGmF6@Xol!eSJQOeHTRkw(GslCnrc*)Ww!SjTmMcv`fA2lAor(W81^KP( z_kT`7V$AFc-mWzKYYS5IC(nPe1?kpN^j8zKH&kYEkNBT0NO^wW3-o`x1=-ju^v@P# zJ<1m1e*1a$ydVw-umu_W8fW3}z2`r+Aj7==+C@SbL;1qog;=>~t?HGCQ@P86dXBEg-L9wsBqtQY=;!{`}c5|7b~P`1g* zh-ym6UW|UO|6@8vKNEr*Ym#f992ZoK(vSNMGct((xe7K&aGcFqOAv6HZWY#?0{5Ns{lskT>p)*-#uaC+!h)QCQ1-EX{gS7yH% zO=Ut}O3=!<-P$HbaL{&7N#>xP%)tMkgUYt=Wk*8_V!vyPxxzGv5ffe7%~?pJ)5|%x z&Q-zH>0jE?wzE!4wN-P@o#$2a z?!@MH3*cLYybFHxvv!OA;=9o0V0m-T;Y}2v| zhTbadBs$+NEu*#CgyxakjMi$+IF6~ivR~|dzbk*S-@!Qd`mmRNukNt!!(Q!i6OX*} z!I%O3{CC#cu8>)|k5(;mImp5VwVu?twKG?RNmx$~;&8niFkfeaK8CYma-khr4BT^cD;ei4Zwtv^ zP0-!XP?3jRMPBUw3-~I$AIOE}O{st_NN4S4It4%zMDUoz2U%IOvk&e>jr4|ti*L{k zK(L&C+(?rUWm2{DRSHl2<8?Q;{Mo!QtIC@i%4Ks0azUzN>eVIoxTYlFr&AhDOc(VV zsnx)!_cCtT@NY|%WtMDwgzhtpOXn2u~9-9iEw34bqD+A>G-qOAg(E;2YhRZ7HT z0;9TQGP!m#o8owXvFNC0zLtH=NXaavfQd@|!p$DRY-)2~aTeHu?9A!6h*XSUd&TnWGGm>C`R7AgiAI{n3$pl&u#uG*siEz66wz*w+3&sk%OXyF z?VD1~ct+v796q}5pzw6f{%pI5*P8oeId$B4Y~RQ8W<%~B)owg1a_X;b@S7Y@&E?u~ zn&{O!L04JFsc_ib$=BFoAuqqh!9M>$aQ;pinJRC4PT6A`#W*=$RqhB*ub2K63C7Vz ztVcg(-pdG)#z8@k&9!|6;=jdgnzoxeXB$In#;CLkvMp9VE1SvaKuh0r>Yls|@|<3} z_PVpv9TH%EwaCwqu&V8QST!V8v@u2b^;CK6xmHcjaSR{4QP)q1te08kjEc$dtj~`f z7gXo2MfLv-{H2$s_Pu=<-b64~=y@_de>-ffEG14>v<$~kgu158=gKsNwBJya(P1#q z{B=5`P)#vd+++V_$o@=Vl}hi+u$V|ujJUHt0;tv}%bhrjk8XT&d^nQIQIRhxyQyiF z3LS2@wrT%*r{%=KqH(pN$cNB$`x{EvLx8*2Cf2mEgtrZDJ_t#XH8T>QYJW>amL+*Kwb zr?wBXhqTHcRra-Rx&e^ISt_u95?e5}ExhLW&yItOwHvE^OE?YFo%B41PPjD?CAsPd zC1JgYs`wSGFsnWd;a}0@GJx0J9%dG3l}Nq4#=D2=_0jy5x;MQpLe8!b{R0nO8Ednu zZ{5aKZ+yw`Ji4BNZh1fXGrT5q&anc0rv$cPZIQF3Chj$R5ioCNCF&c0hHJp^Wn(jC@p3h^HkfahWY*F;*Q}URz zcKFrNug1&OpttDpJ(rUpt*gDFx0eSNuD}Az6=uy95Cxt4xVY+=7(BH`u884QY1MZ+ zwx(K>C=1WOa<@NmqcC)@&^Ov@dX}d2M*pru%w6fw-wux@UdQW-q~Dguzr*~0u=f^T zakpE8B}51wAS5^m?t~cb?iSqL-Q6h)E8N}P-QC@xaCdjDsl4~Seec)(b+4IOy=JCo z^;-2moTtw3?7eHNMU=Khz6tv<2#eL}`67_HzulHbp7A;A_TAaigRX69vHhUAAZROy zqux(2-j8M8PaNncqvR(G?@vsIwfD(h<<9RY+MoL)QHHd?j<7$RBA_sr5~<5yFW*mr z83Hidlfd{GfL*C>^A}m%-(i=*jb+#e&~p0ZwI?oA4=}j{_%TD_V&vcF=(qdaV1b*hS)qeO9!KB2cz1t5nacpFsKT0kT~pGILuNj z>>e2QFdz1G7j}ys-l<>~R|SI63lH!OkIxFXe`hmt6HWpbF|43H@JZzt8AKa1A>vbR z1UB?+e<9*a!EYi2)l4Ca9j8d=J4KXwS*crUIu=mTMI>&&6{T`i3ar~`Toh|V6uXGq zV4W#ffi=%UI3J6nK!Zzwf{mh4l=!{uvCJpM-B7C!pi)5SvQwy1L!^OIj0(axjQmhF zmMFaWXg%fNmM_tOYtXBGPbhHEOa$U*;qE{>9 z;(mTzp8E>@Ua(3TBU9iNykUj97pd|pYHKf{-WZd4A*y*Hsr5c7DF$LHa1F5EwhS&(d(8%$FhNvzDmeZ$E7uO7<~Up&>=rF-7k-@%>fII$5mgyOf=KqDF3% zE52xG;K)|QI8q_*lqwBIPb=b+&1b4epSrl;St;28&J;?KkOAjj+f+DKVLkGwS%f6S zxs)ws{wm`nh(kK&gKINPGInAHZevDIUGmWalpDzWOr0sXpF#GZdX*{qu`v^MLR_LQ zX1XE0L^)>WK7}DM=7q|^gUq{>ICbML{pUa?FF4bXB~#EP)6F)!-!UAfA?y`PT90w2 z7-HsU(Rlpc_`rmmu!4k$hMdTKCrxlp)Id(GNNzaQFG9v_lLH7^`hq$-f1=ylJYg(X zTk1Rk$2_FMEbaqKQ*d5S2OmCez7K0YXP|1HeX?9owkU2+AZrfkpdn~8FEWUrks)-A zC5CY@t9m~L3Ep%XnDx!Y8(Bruhs<6DA$E^EgW{J#6u9uh%D5jUIKTgsM>OiJRgs5G zk&AbcQ+HA9dQp2~QRg5+W8&x9y~6&7LV=C|g#O|fRxAU4!I2a;%e4XZ#mSC!QA?_qP50S7QHVGN*|12KdrcwoHu?{ur9kvEW5@nt7%7c z10w3`l^wB`laiEvpUAC~E{=i~ZW}C*&nS12L)vW=N`iq@V2YJvFEYSCeke66X@e;r zPxM-N_{f-1woDDY@G4nEti)NYe47N|(+dC^j)>4C8!2dtY#Y#@xx=eifffLTWz9F zIkf9;RK#Z7RBI+id@>lsI#JQmUMcyAM~#GNSA^iWSnJmG*`P@(|3{TOTRj5t9~UGr zi4eN24Oy%osi>!qsB1r~>gH+4TC0*@uTNC{o}*o#^`*8AwlPEo`?)M_ZgtGu8_?rf@@7zEZeHIHXB zk#*GUq}NocHqvQV47;{WeQV;eZsueAH1U8VcVcZ76>UUIoj*R|!kE?Uvv$2L z7NU3SV#{u4Zth}dMv@+ese9UGG{-in=W`I(VPDyLr0zRJxR#+TlWQ z64SfhA@!&&{aM)J5cg=Y_wulJvaB~zBtv?Ii+e}GO3bi98Et=bg1X~7;pi1I<`OMdEG=G*+Mi0$%hOmS{P?Sq){*8G z@ueZ5D^&>+#~y3VnZ+wFz;|O$53yX*Ib_We4Y#- zSm?-@GJBqlT^>{r$Kaw{NKQ$bURpqApFdKWPnT%TTuvBM0~e~tCbfW{#K6)?Lv+XB z>d?q?^~E8i*%O+{5O>HVLdkS{$(#M!W7-| z1j<~K)$&lwa`*FMr}}i2L}yb8kaBo=XC!*WeZ|)mOrNr(alF# z5+;%)qm;#(Cba8L)VYnT(QL$-jgh(ihHb~ZG|aw^(K`-3vfn3+0?*e%bRKZN+ihh$S2pd0KWuSx=UnO=_JKTiHmUUyVB1 zVR%dk2JOMpZsY$d2DMg*sL&mEO?8+R+wmMfjtE2oakr>lI&3=;#dr1so#&nDWZ z5Flp=uORd5DChe3r_94=n5$B&l4s2pM10TVG^yvi1Lr83MECd~Zs{*fL@(ePPpK^r zlxX@~4wV?@U-SfI0?s|k#s_j@>xUZoz1gk3$Ep9*p{a&TVp z6rXHMUhknED{-ADmR&1+uA8R5M*MuK#dWh=agN_~S+{ZtO#~giVDL)a{8GM(>A$iY zJ96;6(g?m{JzuwyinUq2&|JM8Gd};qe(4h??)kan@_2s|wB%k=H@kA~J(k71`?hge?2 zX+lO!z8%gZKMGafJEtvF8{M^YNp!xy`|SF3v-JQrdm27H)`mUp6n|{{dLO*{s4;re zl6=$TxoRfaT=e-FIqFGy>F8baft2S_PT1{<)Wex&SLM=uspO0A1p8^ z*;$c!ahXNA#rdU$B~kyU4}QT3ORH<^8-G>i|CJIN3r)DrDZ9FRD7-01tY68#e$B}6 zxy>Dd{1SB?wB8Ybgh!0O$FSZVjOO@uSO7gs0Eo!vf{9?0JrGIBRY=-!p*IvwtJ@fj zV4FLV$gFvg-S7t&VE28%M6}DF$Q1UZCTqMh;&*N@+>2}kQWF;e%0Ep~15IJdv?%;b z5NnQ@$_)OA3;Y$CuPasOH+i)+;#yY*`4=zHWj8_m&&vGNMsOVd_D~c9(qwTR+U{5? zpXH}z=RcKso-~C}Fxs|qaX^a$qpG>tz~3pMhKda@L#Xty_`Ikm;`8TLH&dIP!7M%q z*^={R=~#(aT`set{pvqbLT*do5|O6SVGs0&+hdxhFiS$hUFgB@-zlL(yLW%5gj(8} zR+)=`2`Bm%aZqaeVdgSs_~(_>eF5N`^sl>P@!n|z4<;0Jd`OA^#s%0`-D3zG<_45> zLzqW}#XwAwIV53=6Y!y4(=A5-h|GT{rlwqtA{2Dialw(?--3MGld#Q+IoIDG)e!ry zQbI2?dO;)#ce%+{qk+LGUq1O7rrfB{@BG09DAl+n^HMT+iA{9FgQ->W>B7iOkRt*V zsQ1~vq5hQ;Dg?!){#Pj>UVGId4{zxDxO$eFJg4?QO$iMJ7Z3J!6@MLL{F4$wjVfS> zU^FSHjk7@_ah`KgJg)zf67rZx{Z~rJbTk!DRAjfsddHL^cHBl;+euX+u&ntF*04$K z>`T5?*u>hPSbYOeaefD;VzNo2_#{(KPovf_v?NrwZw2|wOxbvKFhtQxmG>l;>R6gw z7C^W-b1}fY>!w%B{&m=B?AOWn$}tc|DE2rjG$rH>`@`0@<%M^_F#gxq|B@0?tt$I2 zRWw{VtP)A^%VNJ3d<{0kW3=lMbfh`2o%(`Y8fN}SOlht#^3RmeyCR1LX(aKS`Cz8d zyC?=W$@z6$ENDt-dw3anQsBq1(XzGcql0>)Yw$fZB{UklTKIQLh^^gVuJfV&dw{v+ zX3S+O^QI;ZJ&Uh;C(9!=C4^u7Gud*nc`1YItIHK@t*+z6yy5D3@8r@+({$(U%GF&@ z7{Lo5;&Fi@UW>Z)8S;2GC;RXnRm{@!div|>JzZAb56>qUIrVvd{;DOMakq9htyk#G z+;3e@^<_-8U?{Mg9*@VI?;n-n*z!7%c*lIFUK8zcJ8>W;j@z5Krr#;tv0c=J`-lHn zSs)hfx*T}@Vh;_AK1F{$9q$^H8ghEc{*JK34EK$Z0r>@V38? z^8p=O_DL5Xfl^pkyCR&I9eVIP&@Y=)5fL7eO-g{!wcLQ_791r3jm+~e@>oZvfwNKQ zs6+}YOQ~={~s#nBx&sePLHW*P| ze7z+1{_U!?Q}G%!+hij5ohHP;6X%}@IzCqMv?<9&mChu24!)W5DwE>q4cGLIni4s*>Ka zA`O%xr9O!0Y-Qqgv5`cP%3NWZF*RCVYO=AUvZ%mfeprbUih?x3;9SeYH67+ikrqnc z?|Pf8LV!OK^E>DjEOdXo-Dt>4rYaR}A(#72k4%qAwXp4`G9&e^(eDX#rP0Js@)jva zYPzvVA-S>@d7&j{P%)(eOJHSvjhQ|~{?p*hOaDK?q7zTVYBpB)rk(eY`^_3 zz5=20k(XLD-SHPHuA8&~byF(Hdm$mOeR|K_GM9f4@LOEB<|lS16X?wM-lPTh5!(@wXVSFPxj1XV`oiEmS@;xue83xt;qZ#zU_%BrJQ#rKKtd{1IA z74fJDR!){Q;6(3k=BP^EjNIHHp02kY{h>Z-reSIj--R%mpZbmt>%(b&>xbUA(Fh|( zFeP^rrm5fF{6gyIW_p-h$glDXXwuzAL70d~z1HS%TWKtuic(e}kCLbROH8TnSDk2R zen7YQP+C4(Q|dT!sPXu>m{Q_M2k(!VvbOSL^E}GoGT z3h_H6&!8R*;eKhdKY?On1d>b ztlaUII{!Udr~*~YDZGjM--S^V(xgY zz~GIgrUi!w-+?MR?u+>*^|Y%?~T0P2IBw2m&SRR7jpKOcOw0~xEJ9-@$5e@4MEn6zg`*?A_Jl-7m@M9+2l+E z1ClNmkf?Oz97-XPLD|HM=zQ@UYQ+I4M{^NV9+E?AC^Do1zKA{GA^E2RyB#@~!B@#& z8}~B4Uwp@N8ZBRk8v3RP$^DrpGGd}~nK(b3%UCxsV&QU`w1%9g9ZxlClX#iDlupDt zH!$h|ClGfMV)%27N=j-Rs=yvLf?D(L^Z)nFbL;ZK~0co{dP^sV5^{e6q3`wzl!Q#45B1>cF0B1YegrjTC9z4$iecT$g#F6l=W``}Z%^l7MRH zm+HSiRNqv_6GX877+M&O1!ezy*8T$XzarM!4~62xzVJ!9+5)qQqWq;XWU2mnBqdUn zI|4mYI9M9^4@XkMQPk>#)df@kz*=hxXNr{m1J)W#_HR)9-?PW;uh}!A=kT8fbGIIN z|34ATRS^NK`8cC=K!dp$*=t-MGJk9PVPoV7QscSs0p4xqSMUKy0SZ0zGG;a#)L5~5 z#Bzw37+GIPpQeb2-#6!zDADcsbZ}wx|858I%i5EWauC|+LB8olUy1mO$YSV6$fZW< zGSH;j1^R{-*YlHcsqps3g-=uVN2lts^oL5MiU1QM(v`Q9FZywY!UNs}!l(LH7YN6P zdDR=jh|%J}qy>Cw6o&o5t3yfVgNsp+6)66YPwaqULzR*|`6iD%h&Ymp2nb^Bj|qWM z2`LD9Q;X|t6yP&i5|fRD*b*v7X+b>P zs=ZUQ3%4|pH-L7OO;pC%Q1)XR@yJU0P7>polp2FBpFUmWN{}2M)iB3%(L%Wg>`u z!>hZoOZ3%o@&_>yTcw?-lI5fov0%|f)s$Y@hl&KGD3`sC9PXllP?+RMhY^4q0Ybe$ zCfdQ$P3&osv$a)0(zO9NizVP!t-%+l7olUlrA>$Q64XdnfyXoyhO?GeHtnI7E9SV zq-U!NjykVqZZD?il%KBw&Q|tf^}46eP$>TW2xRYLkJ1y>a&TFzVMp2j%UElFH~D`z znETJ!^Cy`5|LpAfxBkMP*#pgb>iji(a=*t73>&ci4`vT@pU7XcXHPuuui4XhnQ|7A z$G$5v<^sNi&YnEZ+kwB1q+n3;x!;M7``})s!%O7zV*NL>2YVzRI(sHSE>~G!Pzr>F zL?|qL)%g-7 zCW=Gg*~Xii@_+ox1%nlSPU_-KRd=hFUOUv)p_E$D90K?-=g|64xNYLt;CB#-8~nU1 zY-9M<`PdH{XXyAJ0YyxlW?;7Z4EgaO%mXA{FySS|Rf;v?-WSV(U@O!^$ z)IREKqW~L=uUmMfxJd@>N^+jMWJm)h zG&3MrEFMVfALo#%b4f8>w3EfH+zQnC2BeADc_!cJ^)TR1I}}G}Fumo6=y&`4db`Vu z?cyL7Srv(fqQ~biG3f^wNI7jXK|`ktjrk(x(w}8;CGW>Q-A#~BSO-Y^ zm~79o!qlb4`g9B(MrnbYgj{|BWkL@=!eYCSbsxJLxzMDF+ ze;v$0caX8%*t6jH${DyTE`)Lzk#)OgkbX7Y=UxuI$!)5f_Q8EF3*kfd_b;*=xhcjEdUyoOxBT$_lr zPTAd5k9l(IIL2>jX-X{si|SRb+qw!tU1}$2tQp}siImw?>_+yyts!eZ=FS7zUvb<9 zPL*7S%wa7D?l=pp(3`wsx?9elt?i6az0m%UZ~(HZZCwy|h!0Cy#T#?15TL&f!E9Mf zHDE4+eWu*+begigy_||0-B0suK8ACCJ-_)A2vMa7?DPE!c3G{yTY%*R$KSaU6+Yip zVXA$E?nV_yn*n@wD`;t7`S&i(?08HGsjpH~$=`V1PYDTJBs)3otGC6>W)WXjkBP1X zx9YTH)laqFRtM%SayIs1H~o_IIAQXMS40VZg`DjZqg>iD6CA z;-jz!=+-^f%X{6?n=s#d^VXXP-e-l$hg=DoU+|`2_C`eaJn!?diXi)*PkjH*d!E5l ze+S~~TKmUs+S}*LSP#$B@B33hkIBbZ(8;a0#*f9xZ%V)*s?Sdb-hYD3pWzN3-^iOZ z--l}6Ta^+?85=-656v<7Xcz(Xg#kB=-d#fGTukPdZv)JsWUEp@xugH<9{{!b07GFw z{}+H}Jiv_-=*|rE0s<0$`7NyZDBSt|z5~j``>WpgexLP-Li4nr1F~QThVKEB@BUz| zNlp@udsH8RC;|BZ7bT!TmGxF^5THKD5Blwe#e0~-Q*4bXxB zw00=9mh(Z|01(gsA%@&x0p1uv(3YIp_x2OQyWG%qxzPAC^8xNa0eIt9W>=ng3n4>z z4-AuUJ33;O;V22=qWNx|LJ=Q~!^PmWN8_|8b1lvUv^Y6Z@cx$L&VukrqA87 zGP?W5=0U+pSi~xRC%Qh?T8_$Bq`+Z-JjPTfOlIGjn#qAJz=^{y|wOZu0VPPzhh2*Y84ymNG34N4)X&VudYf)8o zhAk|{wtXR`%ux~nnr$NLt;(7Q%E`?Q>C^?ukII7jyGf|VW-S*(cR z%!x6X17>PC*?Y5=l?Vno`_?Qb`tt2r!Q%;t3ZQQkT5?+6E+WQ?g*kFykaD7>vI$62 zC8rpHF(NiT5{5#J#aKB(&-6A{ku_eJCH_SrtWUwL9svJaIf1Rto!QXFWMNvcv3E`nI)HJD6B$A50H)~#7QRj zo+rnwe!Q>Yfe_!KXm?EI$|4fUZ4`3#&TmFJT;MKkz6TP8H4w?wXYWR>y+ECL-eC13 zFu4s^lOQj~B`(>Dw5%@3VV))Fn@NV@gSJ#*{ya$(U4ySNOFFFqYy0ubmoWS+BP_peSeoTwv$GvpNCqQPDXLt= z#af`%WuzgRFYavSk7IF)ZA9M`=L!{=#0E2{0Gvdt>EJSX@XBX4D*X+sripENO=8m< zd_OM+Obb`#=cc-W^WkcJdtDMNrZWXp6GXtdch1FW#KkJEnOQ6dLid$*FnSLU@Y&Qw zO}Mdn2c;rRg|h=iwzuZnoyoXE)y3pR{h!L`oFPTivi08s>#%=UW{ZYaIoF+27qT80 zHk%~QHe@!5W_(%%?g4eOXi6U8wN3>i(LaSu7dGPN>O2Y~Y%8Zm6Nl#jBIZN`aThW; z_tNU{G}Tl!dqtB9_5~$;ik}pUaU3&)7@Dwd3-J=u!Vi$DY5efTYC}x&WhfjB@G3$E z-4phW&iCwjOln#BD(Bhqti`H~a5A>pT4|apmjd!oOuMxleu&BecNIBAT&6d=?%1;+;l6I8W=xDCrqK}qa`M)3o=!vmJnWu%22EKkwk zU)=>%6{qF#X=p7&FdPY5-B-IAVmc+A`6VpC-ZuQ?GGUB5lSg}sN4u9sGt!%arPH8R zg)23^OS7>|;Mg_o*nLP;i)mo(#}VVfk@=+&Ktt$?Suj{_9Qk1d>J0?dYYs^p5z(@7<>*2`0o+243EU5~*d6PfZejMxYJZYR&cgf{i!M_Db;Dyh#|O8mU-Lu3yPbRd{h zI-2$1n6p8dw{xF!SD*KmfXw^4&j*{&gHq;0p64Ry<|Ciyf$j^TC=0PE3(%XU#OH++ ziG@rQZ^{f{iO9Gw5XIRY2@|C&A7w@YWl`9Cy7GDYSrw&vWV-4Z+-Sbo@VwZJvLr*W z)Xf1Ukr$y?e1j=V!*omi$4gD_OB3$nebTeD?g(?wGYjUkjUQ)Mo@aE-sn`86ipn&%nWtnU1V1moMvtBK=ri*;HA*a$kicub#B5-ju8+EDv@}u0DmXzCKxfL%#;^ zv4&`|hODuMn)(;k+Pe0Ea2=EW57vrnv5v2?PMEq*0GfS!LOwj)Gex^xi-M}Z3~qy2 zZdG3zL)n}ffuPX0F8x|r`u?&(X|c&Ny2%FGwGZR)P$=#J&+uEWZX*UGLP;hyJ7iAw3Np!Uwe^S3V=>xR#>>C&@&&xS?> z&Y>RrcrWV-pnam&*<8iFTfmJeWaR`J%=M+;bFnzR=RAGZ zIDKf{g@8^U3D2H1&R#%gFllG6%Ff=bo}qc3pkB1 zN?7JcA9z5lxk@2*NK$rD(Rxuodcgp>_Cv#mq-W&m1b+-kk~PK>F)Vb!=$m+VxF@{L;r-O!wPOLi5OqR^=6#a% z6}g(u5aq$4hg&0KDI0Cn=dv_*_=tVnT3}kzTX1?Y*A)lX6VKgKu>ohWE5cHJ7EDk zH3K_60iK^*@SUDCk=dB=*_s=hkvBOy4BQUXOWd2>3_Tm4oRQHxI*mL!<3NIENa6f- zGF;$GF`nKP@Eq{x##-bVsHhc9{p0ty>kqm{U+EXy=2k8+ znXl2t0p_pk?1k=<)U2+)bVVJ-)9LfXH7I)wJ6G-Yr|@{D;wN714QBpMQV4aaIh-y3 z76&RxsDT_!)tS$Eq$J&1fD?SjPEg$LEI0bYXs|%84c0r28IQ3V%`Dejb1ABxqwY<& zd&?c3X@sdycLy6&Ns3_}zqW44R)r(ePV(1vMpFp|-qdKYy!<@(nWwozz)shW#y66b z^!s)#t-XUUuciE##`K?9>w3T!jVeJVQj4fqEn}Xpd9K$luj6zKP%wHhXsHc|Luz<- zVnf+a>a#S_5pvOVSThZTBN_;(5F%@+8sMX(>8ZA(ev@O6#VDyL=R~O(3~a{<0ur+0 zB_f^m(lckDb>sd(%%&s&sIvuNO2Qad8M zNLN1zdOU8}$;G3uJ9H_sXgH3fv1s0#gc|wIc7jh^j}8_sEyP)WWbnL3lC^?)H%Bb{ zL+Km)VHet88dF8H^BygNuW8MdLqEIJ zs)oN=CferySUmbM%5;eTbBrxd&2EevLR&q_{~n693V%(xnwC|BEdQJe(J8*3RdYPP zp3@4TyO~!aC`w+S+Y$ZbX6Cb9+G6$iSi5xbVtTvuwBgR=VzvuZS@nX@F}M3ZO5Cjj zC1w(ty10}I)_Ecb;~CmBc**`f?`ftq76yMj(*UJvgn(yY}N*MTo?bqJAb?S|# z_C?;UGfd2E*aw#RODFLp7EA<0my#8hhz0*==Y_zTALSCF#`pg4GrBF>rK&!}-FM*$ z!{LHB-5I105qt^!x(GMz8Sb*e{5$GnWN+2bf{yCKaNzWS=WOt-H?IWv)&%f>sC8r5 z@MhVbl>?z0TY2HV4w!R3??>!)pYQOY)GK+EoDD-5aMN)tteajJT3M{!{e7$VoqnX+ zqUxfQKr}%C3cqmx$%#)po8Vh+98x}##r{qaT;A|VHIfgH{R0~rbmTRW+M4)3`6ylu z;dnFz+Y-^bo@ zNQ1-U{F^gjryO|XQmv^W2(p`0r3u6OE|(4?Y6zt81wtI16Op)qWUNy_L6PBaUaQcJ zElX#-jNo2?FN1-Q7f^}}l{a$2h)gpZq2C!7Q%9&c2#X-QiyrZkq{2>{c$5!g;^PAt)+1+}qHs8@x zFEA6|ED1>?+)Wua%jF8o&j6#hiDkc0!oZYh37|J{hjRL*L#oH%Qz&Q19qi-ZqD;ts z_@)rWP*5x~lSo@~(eYLASG;9Rpgh4VX&0D^Q2&lSj;4cTc zh@urrcOXHJQ|upz5DF~{yiWtU*{Mtf*%HX#WmP0Fg3u~gf8$rm4kj|n8<8RTea9tk zNxi9Q`2gfp9Li|uys4eh)iL~LC%5zv>kzd>8bJHFxY4Rt+)u!7^R{B?-Tgr{frybv z#*myGh^_{!BfxtIFG!v(VGR>ZIid5U{mB z(h_iDs`>nI+XkaCmo^i|1|eyL3XUNNGljpA4j!|$(V>&T8u zUGqrLLaZ3yInGem$gf4EuHg8s6?nqslyW~qHBH>U`QGoP6(GyzP5jHdK2Fr{pnxS^ z;@1oPy!4fk5NL0ew5+~En*7u`&Q;?Ypx4gaaPcxZgiPajKa^b`XccqbrYoo102k|n zn_UjaNJ3m=xFV43x~|VMmoSQ!P?d1nY{;r-43305LgQDd5;$=5S-rz z%01WNOV+&*vF2eXtN@{$>xs}bdThoJdklc51wz0iQXaGg{xzW=-(UEra+Zfb6ajuk z^C0Rgb*Ua~EaTfgbhITOAvo5?V_O)Jtge-4 z>dzs*YkxQUHkL|9iYI>|M4zx%_V%@M`k8YWqK&Y`pqmDGBab-PEJ(*zJ09lZEj25T zS^GOXh=ol#3nB^lxE5>F3dZqaA{5}{P9k~!;uGIRk~x7#EsWNS>z0WE4n)PN1pT6w zM=8C&BU3d88Hw*@I1%h3>Ed8hG0V)^@&Et7N|$cx7<60aaQ7RoF7!NZ-78&;F*)LU%K`bEyJZ8 zy{yiT(KSgX89H~cE?7~!Q(Qt)ceuE$qc|cObBbIK7<%uo2??ILoUFAJ<(&1sY{ZyQ zvu{07_3Y+$-!J-;%+=XRUn7g7@mEr4lVI4@0qlb&OlJUQbDw;s49rR3eaL|3|+$}j7I3KYvyaUj_h|Bj2+y8Sgv3>gQaT4FLgGE)y@?Z?~8@x zdG#&o7!f2DMSO1kDD1zGkqZCLH{c4e;&yU!yY|Zq_>K8SjC9K~zwWoi+y}%fh!}Uh zgv>{~C^QSEKUk~E{56eWpBF}5z@ni8L#(cTxzLAxC7fvQMgv>_E7RgF?!dhFE3@yF zvb}uDJrN_U()3+na)1#(wR8Kdz0akarfpV#aBlXg6Ss0U++T9m_kgjLZD1*r&372Q^-Ork&PpUy^s;y5BLe@&{=4} zw~v7}-%+mDb)8fkHd=_--|A6XVcS9Wa)$?z*~zzGNsR0>-y2y%MVA@wFtkjbqI$Vx zN_hq>g!);__i@pfN?$dey&?xo>R;;;`~qh=sxHAU2|b?$<{nbcu#;^Z(RiL zLIie#8x~IF${zce5j50tDSfnyyz}fbu%xfo zkyKI?&6d~nH=WDYn+!b%d8K|3F z7jkqTIdGRc44-yPm3D%YCJvw;??d-$Nb|>u?=%i@Rn&!T3>9UHbk9owBp5a;@}+NR zAiAV~9*96ch#z3|ClA&aiW}?Aa-(t>S5CX!&#E?XXD~_onV8Mn_(wu0F_=9Sm?#r3uV5tHiYNJ9Yq0UfO`Hg;VpcItz+a=d1k@tNa#J*=gkOPptvmb1`pFJp& z=YW{w_>k=)>b|;Z)Hd%B^C{nkHGe}S|Gi_rjd#9TM*d_%KBzH244fa%S`evH(B@R& zqmrLEn4i$gcAt=o!t}laK9>xPt|^+46I6)iQuw8@aB$pUX25{sps<=cvjALJt5QUs zSk#>8_7lEPb}*0DC5!$c*Oj&C2d-%!*q$F;Y)c(C4lQuOvYq?ReR8_SvZS1^Z@KWZtOJ z=4FDdoEG#e7Z6LA3Mlx z2_8IPkpc5@-7Z4g=u1qXQX+%spCv`$K0b)ObPWI@=&QZaLSIJ!R`6{h)#dv@b~f zdS14$=O%vMwxXmmLIv{W4t4B-?)DuXKQL2MckGVP?2$4&Li}`JHK>FDViS$5MR$rP zz;R9Ux!` z6v%&<2&i}B1qzO;qQ(e`yvR7o-yy#46RWP1&qGLzSC&(75|Xuc@_!5PVtX$GFMnmC z1;Db)bq0QFEV^^8HBha+A8LD8Y^yJ1Xku%xe5D;`P`+_rPH)QFr~vN^fX{ORdW$`4B>${N5Oj$~Gz zSdhk#dWYpB#1q!$prxc}Ke$uLk6cCT+Wj@S+deVt%~Cg-ZZh?OH+0jv$M6B#sfu-O zjgV7O9#(@_{Gz}v^a$*cqzssdme^c_bnz_qY{Scy%tr^`wuo$TW!v@AUdg3US&{_0 z7VacO#Bk3A7sIgEKH{}QT>D>={-mfRm-@?i-Q9%i*?r1Ckk=VFnj=&4>GVsBe~aGe z%Z*|RheX5k6xa*(c|aJgrMsoz;`@G+q1FgnZ_i5qY9M}dE&egbi>sP=uyc9c!W0cWEKKzjT(qiN^k?ws{ZvB%t z)*?Fmy4*Gd7ig}ac$mw#*H9nINJ6O!-?SI`)vQ|te^#KlTZ?11sA$6IxiptM{S`RW zsEJ*^-zg$onoggsb2g&WYO_w*`F$q8_vV{A6WLJSzlF?*P&u}&AvJ?ee z3MVB)*@Z(<>sPaYi#2cVPnPKSrL9eWp3Qj9Fjfca^HiD#CDRQK$Kv<&$0v9w6=ToK z(e2E@At6NcnhxA#5=cNA3&kR&-{{RM4>%4iR*9J`9Dw5}iJOmdkzAL?keBCamlyDt zN7)R*Tud~=tVO*jzs3w^JTFp$(gO3cTRyDm-nB0?lS?kVOjB8Sg*MkPk8nXCUQ$C`Z2l~ zTu8Ub5VW?R9%tYlQgXa5bFkJ%kdHmut&O7>-JDx1u`HLmEYCSITfD5|u{~q9%=WUa zHqjJ>Gg_v#@#7I9Wa6-}kFsO7vQy2=_tB%Buw`qF)%g9WUhD8WZRr+VYB^@FwwH!Z zPl}#1C@a)t314GNxOFdZbZ_Z+C%iQWYcMpLe)HB~6-NByL+GYUU3%(^ma#$#qiTA* z4ael<^81#x#Ak_OkO0jUMz;T{az1O$E-RchDxaWw#4A0I7*)>I4_n=V1 zYXbV~Aj#{5#4U`pYaHvtxR8U$@);5do?bRTxtL}-dFKb=8(X>-1LOL!CSg1G)+;s% zlbx;{Ei1dbcm>M7L?IUu>%=%e+1afo1Q{tD(tvli669PmGGt>rS7)eW2f6h37r6JW zh1c;?=o}W=T%*I)FP=e4nAq~%xMfoL$-)Gbo!%K9d9>4;hEwMOS>NaY8TAcw;}^+l zfeUXTk8`Y?r?;qQ?SsniByi%|4Pmd*5A#_*OXI&D!Xy2xD-V~`*BXO(vO2o040>Kl zeFl3xgH!LzxPF?*WpH{g)o@r2mF1W+}(58K)AT4j; ziR}>h25ZGYA;h{mS|Grz%qvn2z4YFF0MG2e?an{DMLiG+9_ z)f&_SL}l6oLJdaMK7P+`aV|MBXYIEkD00$UDd6iPHSSH@_8Lq=tfA&CR4fsl39>hx zEz{_7AQ-BlRjM=`36c+1IH;@pnhEeB z15t($xZC=j{g5-v)Il;bt>{UBK9R$96YKTiMAiG=R(03)leMxU^)ZLQCbe?U^m`;cVMteBV&YhF7~eY4)TPwy540HpkAtg(L8VqWw-a?1P> z^VtBu*yx`abYuF=3*x6RSMkElTayvE=x@WNg-F4b(l{mzuu=RnXntZe1SOS${6X7J zBnH@YlN$z#3yiTmrQKaX-mg9w0})ZtegpJOIQfg_nPmGps+y8LdeRcHB0C8}2?^wh zx~2ODNmhUbvUs~7s=P$&*@4wmx10T4C`j7)S<~n4?K~F{(IDS#p0qbf2U(;SO;fg> zJVCTp%}ZQGb4Q4PKRb|d@) zZNoAo>NSXYJ6@I0dB^f)`F=Z5@#TIeG0*$~N|Kg7{PL4j%s;%iZhkl_9`ZlnLYpdu zE~@|HJAQBKEna~iotCYW*e@efzn;4Fd>ul5?)8Uh5D)Mn1L_Qg4^ zopyf1xEjyb-`<1X)Q#cj`P)89Gz23|7r=3J3=~==>OG(o0=~))M($SIGYIdF{IcN8 z%aL{}3Mu?yh&5F28tYX+LdId8AxRXjDAq%4usiL+c^MY(B0@@rAy+O%RW{C z!!J#jb_I>rnJ&U72XwFY#(!ywks(~*9oapn?+0Hw;LOTli2XyG-N`X z#@Vpe35eP4ft+Yk3Mr;o3x=6Uk64ROhflKt_|19V%w=r_Sm7#njg)H9Mq|@}p#(>><(eR&#|1 z&Jk($}MM=JQRbv6H=D=>1 z7&}KM!$6UM2{XLeqSvY?S)7*I|DJb@ntO(jmWcgupK6}B!Hk_riXGjhafU3FRR9ic zSH&>nv$OW?E+P*{IY*Xa;sOgIUvA zhb@(xJ$fSKewsk*n|JIQo{=d+<&aVKwJS3wLPLKQ;@oq0V__Wet|oxrHmlooQ7+tu&O? zfMZ%V#(>(%oI*hUyd_fUKsP#^U^zBhU+T&E&?QFXKqtvo08&&`tERn!c-go8u<2DS zRGuc~e2Xs;wB6&1mjsGWsl_p`-aTpD(#{UG-*B5>{!@-|-$m;q7=7M(ygV#C0PYw& zKQ50~Y80jZ){G0Rz%u&{85e}7G$o4=_wx%`j+DN>Txj5#5-b+cE~ymZ3Lg`mcpNsj z{bW~1Oj!5NV^X*F46WJVkmaj(pM|muHu|k0ofhTx!M8>znx>bZ6za1?8vS%KmY=u1 zV3V3AZ;HW%fvburrHmJzfiXI@xd~nKYPuk}?!CvS`|zCJUN@AL{dO@>%;ab|cE29v zIJgaCo6{&BP3c}d>k<&NS^4ZJw`68*iP7^5QN^>@xJ@=wT6oSy0H3lg@gjPwu(xtu zkxJt5#txKo9=~-ruZ#w-E6fo0>dc2mc%vUSXf> zJ^$wKyz|mG)Q$M>TwUdL4;oc)u+({@kT2O5>USDJ(!KtB=M@hk@+81Vx!;8zFOm27 z^y}FF@wDPhphr%DitwZp-^f}x~34UDuhMVB-fp!r$w4KifdMu+=9D+D5 zKgo^0Z>~up8461nhQJ^AUXD@I3DL<2`{C%)I;zWfAVMwcwrS?Jg%}(+7Hmq$Nq_Ar zD&?morT% zj1KgUIciKcxY6W%q;8{de;Sy>*bS`?ol^K?hjn6qI(njK#D)(C>s(`vG}_d_kcA^) zvp0eWDFHTD=+?$@aUY2L+^Gsml?X;?Fm8f^3)HIyfWG5m@sc*kL&(kW$b?OLYBIt- zdmJwKL{am4GC{H#dvVNa7R>j;Ozz>d3luC(37NyO?Bj{(Pr(?vQltDyxLz_Y5@t7n z_CkcL0eEuFAGCz;8QczyIwhRGuamf%BNgn%#Pb5Cc#YaW=&x`?lID%rHWbI4n2dR} zLhi_BX(3EVPI)>+VVscA$JkjsoxUO{iL9m1l9qPCDp{S0Qihriy?`N^7olcHs^og= z2idSnog`DasBuJ90Rp$>k$81JtWF7%*$!P8N~#Z7>@H7|;r(#MVRk8E2+f7m%wSc< zp!BN#bTX&Jt`RljmH3bi{VXkJ$*;1jI@oC1`e$_ZbCFb%ZOVxz^x2vyv|4J7mTLBu zN%mAgU_V^`t}4@bWa2|)++$X4Wn_auT8nP>=79TayL;zpz;*$4V5#A}PKqm!g_D5R z6_C4r(2{zO;1_pl4umMu+%o1!YkoY1RW3IP+cNH&xN+Ta;}m6@CTA+is@76RRL-)y z%>Ims83Hy&c+k|F&@Syj?(u}?itC~pJf*5-{+#^V`te2VzxvWk(_P7Fu>V ztJXRq1B21&)DnawfVN2b)P-L%v~nfyk?O`2TXt(0;^C?sxFgI$K3P7JF>n3jZTT!qDBPE-T{ zWi(k1QX-FrA!TCWfktelsAG&>usP{Cs}sv>Ofh>H4DGYBu#2?}H}Vs+LKB!OKOL2% z35I|tyx`Q*u%ghbqM*Rp!*ufC^xah|Cn8(;C>p5+iY!d5bcDEaY-&95thcMeQ3R4A zS`-4NM8)QaQ)vUAN-JdA;0&*f?@Sakd)U2n_A3Cw!$@j6O^h0NKbiZO9ZzB{stOEi zpsUXuMsKgNHF`DKAX$6@?z58);;Q~Dk}A{yqi&r)vA7RG?!^rTa`M$ z;D)&#qCa^=D!X4l0Am8v?fwgx!hIi8I4H9OQg%;e4#PKYt7ve)WAjTzg-6uCqHrw2 zDz)J@oRVmYNLBou)x@OlJAUHFXz2$($yVPb5gU<=#g)Ix6(?=Zj4#QKZ)uKb#(@*$ z$}1F&`WMA^0BZNAIQXWSYJ*>%@-HY?u=RaEm&-E$oZVXK5#q2)px%{#2x|t_4pf40 zCu*W}WN1!>Hea}Y3M@6Udx8DT$s%{O=cF57|VBZRs z4KE={_U(d8k{k6~h;Ylp>9mG*?rZ^zVzt&)`BLH zK_D6_*1exMFy9pw)DBC@^ElD&dfwiN($N$1we+R?aW#@RiB!rBR zeMX^10MrBS5c!%eLBmVS2Ws8)>Dkh6otcX6C+Yh!(t7aHs^!_IN!V_-)!qHl?ojUJ zbIvnB12t?V;kmczdwqx1V!B^QzVE$!Z*X@Bk4ei%aZ4phD%LL<*Y8IZ7CEGM-8$r4 zeaV>K%p9QTsw3(mzUV~C8TjxzVAbYvbLM{Y&<6(+I``>rrcS@Yw0LdrtmY()a&Gt1 zAM&*6encD;>=}9)7W3YZM`s9`YQPl?vmpfV?>@T$Aj2})wUd8)RZ!)m%33l&!#Cr@ z;G_r`EB<+E1M7|v1|1@#(hSxoHIMOL%Zpy?p8lwzA$w+B;q}%)hB3Me)L!XEVsD<; zGp<(gKFy`PmRAp|lC(gL+(fm6IA+`Ie_8FIv7xY-uM z#mhKJf$@e^iF@-L$!fVeJ#kgbws>o74wE&Zj1+Ug;bcb~1@;=L@L;zJQx`9N?`J#; zO?xTb*_i&?=(ISxh=H7`e$tD9chU~ScJg*wrQ$p^I=FG8K{Lo)>lmpzNs-B%RvSr( z5%GobH+-`DorM#;UWy4)?+G#6X{Pf^Lk%#e^F+=HPpcys0#TKRO`kVQB?|6}`!YC} z(9l?hP5K~Te%k7tKLD3yJ47Rd%!!s~+B05!(N}OWA%r%0N4o3}8iDPKO&W(b;uFL; zoZ^dN&m!Cgki}xU5e~l7T3NPT9*X8Z59L=!8 zthk8H!sGxLY7hpA#KM#>V~VoqyOPX zN1rWftQLNEEq{c%ki;!;0AS$Y@j1*B~GnGR=BcW#V0fVC9P%xzp#53g#~uD*p#YezzW$-xd@(etqt2)gl3Ur zeJLp3=Gl<1^w_2kBebrtXVgmNX^!N?F}Zt-ZX*70^w^&tbLIw$Q|`8&m$#me9&R9V z)^Da1bWKp@bG!^{Q?z$X3wKN^YA=CZ$OR+^`V+x*Z(+qH8ESsE{DL|GFwp_g<4x0d^dY(9VH#YVIV zzKD|Ckw1RXiMcu=I7_5{kA-?!WN=(~s^JEB0b|$0(ayjs-(>UezjMHL~pNVs( zGuhmsb6gjHD+gbk_y!xHy91)H{YI}$0k4A&uM-Kc!*lPH?M(~$+uz+G1(kx70MzO3 zyZz=Kb!hX3QP0@t-j&1KqFL%Pk3U-1LzC-aCjs{$DDi@?J`` z(i`Rfhp|nt$t%hTDa?jco$P3 z+w}i89rNGT_mA!kQoK!lxpZcUGVW3ZUAD;rO-2;jPvNl*JY&J_jn;I4Qc*j#Vd>A3 z6N|)S^MZKh+mVVEi`m%4CFX(~m7L)O1Tq2)<|(V91f`(Cdi=?vX5-H1T5japiI^vO zq0yQruF2W-?zB%??_uwXO3Cl>?cEQ5{zv^M>=lf)U#T+Jce)ajDl{2^;Sj|yjO0cTsM(iz;6rpVyjf@O~W};`d?+vEz z9MkiFD=DiQb5KSq%&L zT7Ago%#bY(HP$06qNN(0Q^6{`l^0EtA3XDq!MD-{!!USRm6pg%k~?(#YB!E32ZvV| zg#|?y$mDJor8o|T4U+(eyB1a4mduZ9-GZD6E<1j@qp(_g{old1d;g?lN8!Blcv;&a zx^g!*d$fhdj$+}QITVM;8EQj&dz5xS0f(=3ReaNsRYJ@J)Y-3ql_*)Z?N#e>^u>31 zHH*BX=)mf&arRaJfb=|+{6zshhqLInz}aUtW(?DO<<`E}>rv|L&?0SC2*}J^6wk^D z7x$5DLT(rx{t6)@A&oh2r*QX^k}KOX8YI`j@tK>jjUt4J*+fZ3{M%D1WwH z*X$mCc&?Qaa8AM>t-0GyoM8uqfh(CTGRsJ*>vPNWtRP`xPaCzggB2PFwHzb`!L=+~ z7&t+cb$S&c)GgUlq?>6^{dv6Iw#7MrH?yh>2JG<&yy zbi33>TL<9R7#w_zu%U0POWJb}cZ+5!z4xmYf8OqYdSLu}+)Kys-1(o@clq~LAH@}e zdiihBDQteMq8r6WjbPN`*%8M1ehEs7Fh)7K+962XhgHUT)e2eSt)D8NDw@r#E-lr@ zb%b$K3N}G!Y9CEE@=Ot^3>h0FnTa7a%=e96Jr(pigP7-lkcv-ARZ?O%i<`5fE$gAS zcGZ;;-zO7sl9_4gHT6tBPlCVm3ScQc#O&d0z>P4M_9eDW88_fW1ev{ zoSjezF}Y^#eEFJ4{y7+g3g_$;dO;hiL<6y5UN{!8F&EH(-h;I>u zD*7|G5@=GU7R$C04@%)NO8OxSk>W6gfSQGRGseYSxl_XfX)7CH$7!|}tSStfz0OpRCY7?d3Fy?|YZ-KMGqYjYkY)cj9dqG1@1ppm zyQ-uVY6?cQ5KZq=vZu`rr`b;)OK!kxS>-oW*lJ@-uWMGy)2y|eXltt0b1Gd0peP%t zN#U%}P^lLEl&r#R+5%rkPjTBjCsZ382ofkm;_6(>6@GO3x?-EPL>d#k$X@w^IX5sr z^QN7V{mNYFaon&suobU8S_#H)H@+}=_uvdV^EKY|{$c&C|Ks0(5ZgW(4ycp+h7avh zXWkdb?Ti@dx=O~+#?|DrV5;PdLELs31r>1m)b8{OwKb+ir?vJ#W4YM$Swk1RgtnsW8$jgz~ z5|aqw^&4KQPv6d&NH!Ap8HPnynHqcGW{<2LwYKHHnc7dSZ*}Chc8n?6cm2A>z52mu z?ClkQ8im*^Av0D*aE{tJolSf-D`RNV7)9CMwOIAvTf{$3Bn=)PLQpG^pU@WK@e)0UIJTgP|%h46lwzn+z7{xZ0y`g@Gs)CQ_5g~ zV-+IlxjH#hJB8WkoFXi5&C)Ef71!FN-Hk>oM5@H_nq&itw3oWZ@VKUn=HB=6W93$+ z>lb4Fl^#mE;mYWp6I%F~W(61HmaC75VhWCpq1&)fR3dQidJ8;CDoWRuguo-r)}s8F z3!z!wW#P_Fn2cF}tX{d=+ifgnO}t~Pj2bhOAxt>SY~rxqy`WED2~W3eXA!!Tbkb2zP}g|E)QO zwlSH7Ww`{Y!V-?|1AE!B_;x7D_>JTRfSF(r`FDzi zD4(nNL&ST7I~3r|;~5$GXp}HYpS0?s{2&z6!jV)W=Wr`wcSz`9>}K`&wb;b8q*z6I zQ6LUP+L@$6ar@2xR_@*rKAOTa(0eAbgTz9byycurzH3E4D zpsCcFtJsbF05zVHrFa3&`ym##-6IguaXteYx#}6WVD1SQ&Siq&ibH_C?aI}QBlqOU z&O7EA%r4$18PB#)K@u92HYSS21t?DEs>R{%=EHL6e}DCoC!+Sny8Ff)?ERa7;~y0T z7||nJ%{LN6vvz>lzh-kI?)#A&{3{IKEEOfJ!S@jxAA`;R-KrrOWU3%p9>Ixq{(zQHn-urBWZLf;0p_v+^#Gz*3+2>A6;eQg zovX&13-S0z4m*Cvd-?zYD=uC}K8FW4#Y&-nyX0Y#E)#!|u``gyA3T-`<5nutl-kqc zMj*}t>F0kb_&w8IwJalWDN{J3QKg^;2Aa&2v{)aBzSc2R1X&x!&#Zc>Y zPRKZCeTvkpaax(09y3Se4j^&>DQD11+SMy(d_zPb{aZZ1@e-t!)-0z9k7fMiZV(aE zjHPu2U}|5)wzi~7Une;TldK2~fG^AhLL}=KQ}x^ZuMyq&=@Wdtyg+vtW$DP9L4>Hl zC~xcaw57*11g1F0(=^V^9MsK%-<-y-mWolwakS;YuM3|DOwv(G@-7?M zZY0>)9vs1Ux|H;|2S7<0vnPnydOIF z$tEg>;tbyzA?pNwU#&~%qDmC1N_dY7REbMHOhwMZzwMRt9fT3UR-(}`fco&ztuhIQ zGR$1Z94aZ?)w0U0yUNa-L)wlUe>)jtTjU5V>y_WQcvTb<9Ai>d7TJX`2TqpwAghOD zL8#?&C>Sv%stVDeGU_mHXn9tn!!$wRqynTVW7DZGbu!qJNizhbxV(sJnv>5;EBYtcQBYV2OE|9NloDMZr4 zGAziu5&NNq=$^(z5DiKw%OS%-&S(0`)rIB#*X7#tn`DMQ#K+&eR(Y~y(YkhNvqadZ z-LpEFqmzDD9*UlDL+*G;o-i)BLZRiC%{(tI{LROHmOj|8D zHT+H-TT{3v%b}!_T(X5e_;Q5E(j+|uZs-fjE8So+>2Arr#sT@7$LjCZTi~Y*XLp8Kv~|xo83j;Mki(7?oPdVNZyfv^Dd)V><`?a)ERP0kpp1g~Jr@stMf@fe48%;=jEK)JSh*LF&#~O_NQ$ z)y>k|P5-)wC!(tVdb2%E)u$CI?-6?EECP$@`70L-l~CGRBeUXl#R)zVGsvgjx`t8g zcmGvLgnVt~Sn7tu#ZYrev1g7aCUwz@CEx9)gz6+zU?VdWA@GIt0Xmh3@6pGmstuA8 zw;Abfs+FZ^A_UeX$qAAi^=sXICbA6LU8aWfYRa~&)YoYzBhJxM9D%46{Shvc(aE_& zmHs#BDG$JaIV9~|A90@LPJn0bt%rZH%ZP7@#;FDWWk=S%#dPs!Y6~xal5tsMJSht~ zC?Fd$ka3eNwl7mVX^)ZxSb9x|1bT#iMt!rW`zY979^HJ5T9w~3i&|AK@Hp&sYH(j1 z8cESz^HKR&~r;NIw%-ADE^}bOSEMQ5X3)c)k{Js;RH8M zS3%(KB_F5Q(Y42G4q$(6QC?+a7TJq3ei1ve;r{w!9Bz;S5$mn5P#H;fAto7l*=l?@ zB3$$=O~^IHdJfMf$t0U4yA3Cz_)g;~rnUsN9eRjwR}v1?1O!TrIUg)TDVQ9O8AYA) z-AYFnHohg}QrM2jEs5bysx6~;P`LXNNx`gy6pQRr)zvHP?ov`)g=0u0R~ieFpSf0j(ZGS7xL&9W9k@}Cxe zIPK+)QlU8XPgf#`Yw{{k1L9D-pg}}Ebuvgkrqb+rayqZgfCB&<9Mr)ZloF|HpEg9ltmY>(W!+Nl_W} zsR6fTVP#z==6aTa3I)cW=S=AxxvJ7`1dKoRTCLj<0ykQ;!khgqOMQZehJ@(RU5^20C_@luoY-RW>?SGL=O0uedwt8$u0O9`x89BS zSv@2ht;<&>!>;AchuO1qR1=dWyC3$a&d`X18Zdn71Lzt*(}P83!L9(hUauwjx+VT^ z)-?YPkP-ViPU(Yck&@5VVFV2`y8ZX7SikMqYtGiKW3no^g0!{QWnzy^;;mJ8jt-87 z8Ty^KrhaV9oNV<@xNaTW zi<|;kQTGs&wYp}GR3}qDIUMT|oZ=hB39VcCH<#_4Rv6k}#-?LeIeUBkaFzk%RPQmk z)w^4lH_u8nT%L60nM(IH@cgiFD?6d)qf?#!KE87>8DCzxbJ2JNUCNd}E7hA_6hXcW zA7&*<4RCA~59~a9o|re$Epl~}cL~6-`ANUCtG#pX`1?TbQ%)T#P69!4+HXrne)GDS z{Jg7o?sm^)ujb$ENFEsRrcFruhT~!1FzH+dz0q~N;W7H$4f*2xKgI{$zm>-gZH9ko zj%Bceq@&UDx)Beq%O!t-LNT$Drt?$krdf#!bTS_za?pnG z#!Rx89sF}^Ot}tQN!>S1DITs8d?nut-6?C9Bivzl;G0^hgC2PWoE`cXXh0u$OXIMR zTgLmMuNhrV9_CQD6;u4Y)KP%7dp%U6O7<^av)qX)kv+ew`<7sZLy6=oByQNbozo6Z=)Gju4-iEEzz4j( z?EDD){+fh0xj)ms(}mBstkK6Dh|%-tc;Fx7^oC&{&4V=5mpM&cBim&V@F(}@g7L#I zj47ks{*Rt3r8oatKMy{k{oMRH6&@9u&iiFXNs)o|0@|McxMrdb8cTsQO{}*FBW(~1{+1UE= zUi;qtwYRkubG#q3aT0s_=Q8#uB*|JK@gn?26O{ON4l^!=m7z3~6?_A`ZHA~kBj&p; z2%pk1yet}tj<&fZe131Wa5O5}QaYp>C}^7-wiD-u z4o^_4`Z7~5QbU_*%1fJoszS{BDL<(g^K{Sj={W+Gq}#vEpsQLjh~#(wK78>|bLUea z<(idANv(9*+z}*7=s1cSax*0Nx4-4&xsoRea3OvM(D0~{l?L#*^`misj4F9+F9j~q|dEojOCqNvYp>5;^ zkC$kPWuBZ$tSFX(EVlp8yQkfB{I&dgUZ3lnqvLL;6o3fV_p~^F^Dabkz zEzb7FzZ84I?1m{Jq1l*ZlmBfDE%nzr|3UmdYSe-~_3T=jF7i_bNH`+pPF)H_aaR~= z4L!;%%dzPjDWms2dy*>%MYm=sh_FsR$&aQwvn(c(VLB~I(Xu`*%}|z{EK3IMjSE1W ztTU)aOL;LVh04MKcDFQ{#C~Lj7Mr{!cIq0GN77X1jG%%|h|AicJ49V2?Ow0$dmkre zB@5A^wR6@FrUWTNZz@u^*NkXppIlGrDz9#QiO*F>QEGOaQ}*ego)&T^j1-ru0tT_eveZOPevn3#E^%wDALH^Aj&yqh*#cYn4V z$5C9)UucSs(EhVxe&S&ThS3`~T2x{2+j zU^48s3Klz0Fb(ch4c1UTQLPH(?N#qd1oI0;e;BjhTWwp6@y`{Vu*=D6q<*69X(5^U ziFTh$aqa)t;`L^nyx#Zk^k<`o+xH#&?JgzwapoRwA8GHwygiP6@k7vwnuO7eg-D*u zTh-fyqs-yzvjEvJZHdu$l>lLqx}OSJv5Vw-2=45+PIiH0-pjg#BmFbtXHehmUCgwe z2U4TJ1iP4lHh8gCq&$l$Wh2Vs$#ATEuKkv%Wy5uuup9OJfe zBP$Z^k$|YqE5>CwZ<4ID%?SUiP{+2nQ!vD5o0vc`aUv+w(cuy4lxDHMi7s>9p#yeK z?z#PsK^=vzZ6#sI+Z^gKc!(K5 zsdR_N6+it-%bQy{W8=M)@iV9NzZzSnYy9lZ>|oC_##1GL_mRf#&yL3!TJWIjQtba~ zY?p{&xb`XGkugGPm*NqX7--xkRKuuLKdVxkkW&(~M+-I-g`$?TqFK$u8eYZWhmvdJ zpUsCuZc8S)PdT9z?+SGddyQ%R9W|maeL=x_N%Hv|vR;#FZB6B3Uq0n~R>>O|P=!mi zY*8q-FVo|%l-I;EX_ON$XUVr#UHMkN8`L!wR+@ZpT4-O?^my0dZGF5+%9No1UXzkJ zk~HcgoxIFBqd_MT>eeZJoUlYQPPYt?J0)oTPK%a|8+BbF>k`Ny_9%CrF=T2z+u*n5KKm_weuONl~gUAF@^D0csj&60)O$a{jzDY^H zw`D}(?LHifcV7c!f|t{VSsR3{^|1^Taf5SCDFv2gn5nEsyIvP@hQz(Ftg|QvVhr5d z{Dc@UQ{;J}r1?sfKsE_WnP#rz#G!gSpI#ov#JC#_a?ZZ}9+dxl)e09I9GtUqNG(|Z zne-flb2#F*XM?Wq{O6*$iJoTzE7zySwVX#_D{yuqcO6ba7XFiUWxS#A0Roj`aTD3Z z@wArAw$cY_bNY}n0Z;o&)y0~c;9m!`X99~iJBr8-P7)LWO3a=yil>))S=||V+=+ZM zr^-LwB#Dpay`7@e1j6a%-SQ$LMgLO$`cf&G*%7Hygp*CdbLzimoLZz38eajZRD?J- zW;3A17um95aU)?5OKfXnG&2M-*v|8=ik!t#| zHm5QqsDsMMtf!gGQn+II)Wx7P;J-WD!|etSy-NNcSC>9ubo8%nFw$)%M{zMvXAz7s zZ@XGxT@xuKOumvbkq~HGk2deI)*->DgbIsiuh`h9R&gN`yG}nl+fID#I#)bi7LZ@F z(M~oW6R)g97fSTrcj>Bv|qaNy#$SX=B%rIw2m3Le?v7Ho`b5e$_=P`K}Wqy4gsG6_%ynysDt|nzG2ys*5JN}tu z0A8S-@L+LX=(MvzRjr3FY9a^O*u(ZX;GG%@nve|k#|c;c~0Of(fV!@xbFE5fpW4Ea*2C=|T1P#IZ zYJc5whXgYk1aCQo%*#M?ht@;1;D28^yfPx1ck$vQ(Yd*ioe;pGW?-cV3blP?H%}sz zF)yxS=d4FYh=CLt?R8dDgh&A2Z7EGS$Kohs7 zODAH#SMW(kWIAQU(bo-4rGdBifz<^6X?@pvd>!^ocqHU|rc_T#^64L!7k=*&(~=K< zpi4-K8ViJ~PtAYh!;^+)d`rOxRUcT!>)^uxk_a|p2q$7bRwO<>CQ(E&8o`^wYABr- zE99-wj}3?0h(m}{;`3IF19rXOr4zpXHKa`6usMugclW~E06}#O^Y#!orsJ@^@JP{ z#zWxbb}ZuwXw6(R;|?EH$+gnGkbq?7$u~L)c2DU+r{30H#;2a?a?gv0ZEk3@4(JIL)}j2ijU13qHjuWHS7a%@IBowd#iFF zIyGh0VHhX&6`qcCGKGuBa8OY=C+ zB9zyuGu>0J5dW-@$t~V@B5t~*F!zCg%0ufTjnNJuIb9XO<`){*8Rh(_?Ic5b{6t2= zM6qyHff#Yg;}bD~6sX||i+edDYz2iML{#;NDhg*Ycg5FrBfN`<*YGjF9aCQy4$7so z{AtwPygZc4PZmM`7}SwWM!EnV8_R#4AXZ3a%CahO)hlo(&iCAul;|q=&5EUFw8m}8 znQQT^fTtay&4uC``&OW#dN-di8%t5lxj3u#(@UcRLp<7Zf$(X@&kvs>*}XPY5yB$K ze(|~I?iYazxt1hI{k~$sd5KcCd%0LN6C`u59T|P{H0}HwO7CPLMrh9?=(U4T6(!jm zRfWOp9LfsxgIT4U2)8wEujvcov;uL`X2n^+hqyaor6!t^Y85MdBpH-X+|(%)>53I% zYX5@V6*$fbjh>dqzo+W9~)vme}|J22rg?Z`xp2rBZMjx8P`CD z`5w=i_qZG3~5?6NY}|JOPp9Ak(^nJbYV9Ecn9VA>Sm2L*nqVF zUI}YlHFwSL?`#stM||!c5X$nv#_!#WDTb%SPQ)~e#CGUy z@w-4Vjhm<13TzwnZYhvQBD?;i&{gOr)c!7h$F%LiCe8FP)60iAMw(bKOEh{vgv2hj zx50bv?nPN0Igc-rvwtM9dZD>wg#u>PWQJi!8kw_~F#JddK?a^^N4bO-0xhrw2M>Fs(^-Uf@5eXt!ii z_iPax*nR6`)ZAs{c<8CaVP`$YREFF#32*f>58$lg1SkfT90zGDn1-4IBxGGy@ITq5 zc_@Je;hZcZpOIo-F&YZp^R6pK@aY(-!1L{0hA;RAk7)t`hl2)dfpN}6c#{1wIG*=O zz=C!Bq2K9z()FeIoA4B(tgzB@!oXvep)-2CC##%9A_l&Yu~x&EfK6{XxgK>}Mr-0~hBmG1* zKV&3?kbuX{qJqdZJQH6~6=ewTG~wa;Jv+4ru41RF8UB2v#SO0sJ;ZadWnn`bsApbk zRBV6qU2<4Ytx-*lftkUCofX3^m1?1xo>uuSmuX+sE&rC!cNfe5wyat!;isEHqmM=* zuSCey78AADGK@;*oxVrdhVW$xq|{ETgg$=8IQHzZud~|ly2|}q26Yz!U1}}Hcnv0S z&3gvm>BoqJN2!qY^=e7Y$8QbzrmPS#l|0a{S2g!hma>PZXZk}=7Tw0MS(aSxhVfJ0 z9dl3a@AW6r&9ByMUwg|-{ni&$lj$n6gNO;6n4t;tRf^enB6F4E)o3ZS=iZH9ws3#v z_Ej!eMd{HZso9gQy)(8XBWqq?=v{woPyXJXMdJ%XE18eml%YYXEL|!-RLl5~mRZ}q zQ=4__RsIsYK2oZ`8`i%1<;#1pdG*8Uz>k9alNA@M1@D10BDA0Pzh#1Rb65D|-8a&x zQ7Ux!v>oHN_2|84k>&9Gwt4)1VdS~#DXqas?oQ>REhwQap~;e77MlDnT)W!MQUB?n zzZdZ1r>E2&c|4GM2bBaJ9p8TUGMCWHCX?464T*PW^LZoPG^r)<-4*-eAJNVuck4=$+nQC*n)shJ!Hjli7LY7&VRktk>l-b}bMVZg5j@BE0FdiQ3`q@BcR7>B z&ga{iCsG9M;$NlO?_SBPXF!-Qr=x+tFn3PTcL1Dy+bh4f3DNhpOdYOlr<63Y606il?y?VS=~N!}pC|acn`)se3?#y1kmH^cD*X8`CDr-c-^U1+PbUb14&|MpR=n&7jI9lkHli8 z%%tOJitqLDMGJZhKzl3eo4Pf;CM;=;7pVKv?c&+AhKzt^2PYIEwxH91nhtt2(Ci7f+cw z#iJ>p@!H3Qm&0goqJ z{54=ALKwq8E3xA_wugoOB8#_M=YLkbMB@hOxSq#4)!9gY-~k{QYoIwygf)x;&?XuP zvWW||)`S5@g(`$ZzGJIYqiku z_Yil8rg=eAhfS(YXmCT+boc72!`gc1>h#Wb#LwMd9s5UzCwr$b8*#sLHjW!s4*vdl z=)S$VdwuzLa(}biT#8`~6%BzRGxZ$Pi48@d^NHJLM4ApqZE_A;cU%{JjmZ#WDU?va z8P=%euTuM1DnHJhEn?CYhK){YDObHxE6L<=sis@atfUGwG1TAVjFv0F!jZ0(4T~jVkYusm{(m(uG00FF zI-;A$;;ALUIXdjr>8wHDCRhKeS}3IO-?eyEwkR5>BEtyLNYhtKR@=7X*#TU^JUKW? z0&OqLD|%P_L`uSP@`0=AiSB(+i`0O=U`?yg$goJ6Bd_VUOf&Zch6ndf<}TYCJj=;Vqy(u>G2>$ z`Tdn3JU^LyqaDpwO5dVyZZiRhWx!$uRm?q_Ab2gS!&Dt`C9MbebnlRkYv#685S&{) zn`u;uJ;_i-LrB6}=2)c2k_FnAquiy?3}cJ$*)UPelT)W;{>&tiVo~%I8Yu&hba2Az ziO>&+lUjp66&uAMSt(WyxCz%M7LTOO0z)Lv1WU`J&#GW*z0a!4Myr@=nkUbyptUVK zdgpar_mikH^l~&8>ME5|T3O~YEetx29Nm^`d4$L%T9y5St4tKFgTU6e-j-A~J1b6g zNn73XG7{*nj=Eck6xOn0J}Inv0=l%x3WYsxF(81`>M%N(|5WC;%E6}ERO2k{<^rpl zwKj)!nstq| zd7M9AQO9SxhtohH9?k2guhbeMV+@&y1X*FfAFpQi%5963U4`2^GIbk+nd#DEK@%LE zQTv3>T2ZhzuL&8`@v4jUham%fb72By3m)^~-)Z-`+T0MC{U7N*66Zf2puc(@cWE1je-Jsyt$2kC3D%MMaX}Kb(d7orBaGV3Z_Mt+gZqcZht@N zE{Uy2#4NQaiE-vJ1KJHwW;e2Eo#YD{{@u8&DhbC13&~j3N1~HE?8e61H2ZwhV0VAu z2+-T3(NWyc-;YTEvF3KKxw3lgoPE9Yf4ORA6FvT`O)mQONPxVsdmWi*Cq3!#oglAc3EX!0%O2`hFm@mJRGO(&ZOASz%Cu5jm84(Ygq;^w}e)Ulj7okW7 zfIpRbP&4%rn>3VN7dI-yq@Z3fqJk41fk80Z%t+Z5ukx)St@D8?RV#)*-aqPJ=cP>{ zcaIZ_xhpAYXFBS)+07jK#?&n9Khh-Crpj-VLzw0qkD4b9JG+aX zUN+RgWD^x?lAlf6zKNd(&>62wuSWbAFS7$$W5hQ8@s4Q<+#|kEeCqubjsWuuw}xBs zB+~s{8||Tg>M06pN@+7iQsN#hTn$DUrY^pw^BF@PlQA~!B5ria$1jTJN1ahUt;Lpq z55_g)F#Ukzv?)^~#7r~WuOS#6i z%U76-;~F~k;NN;?;rzN`k{VXLtXso&yZSSAI*ny+Cu3iimy-JDnyQP~?cn;EA8sOO zfx9iae;0_OZSWxKfsav+evtx$9b+%BQ4ZJ2ve=z*o}$|hY_v|b}lD`GLD$0wyt`0#a~dopXiP?Q;AZ;^A> zn8wb)>|(mMbzL4kR?NKM!YVekuH5ttx z`{CgG5`Tea)dY9aPQ4eqwyjsR-)SoI!|SWG`BBS0LxDQSll1S)zHoZ=MSU)`Q^%a! z3ai3m`Egh5hPo$HkT6o91lgQ-|R8rHzy#5&tm< zcKVjz=P}L67|LhuOYIYuE5}8|V`qMkK7$AW7u~NZ&k5hItiSHQHJ@ZURHGG}!usVg zgznF7bO9HGB<9&J3n^k5K9 zIeEhdwzbD_FaaQt%|F$X3M@QgO}C(7t=z~>>VcTty1;qLmA!?mVNWPa0&LW z+MmVe(i;C{5|=0Ij%46}Cic9@n7WoNLHr_{59oR@;HD5Q+oY5qk$jh+Z+ZRh*wVOh z{gtZc5Ax^ZP`RfK7p0>T(dUb}*eABoHw19QzKjLR=2m;HWc?pA?)TeS;G9Y+qi^_* zVhQeNqhB0uvyI$bq~E`!bUE!mY$2}k8YfsfllM=>vOwH^5F*R{COP~LrrLG6XFuP) z`Fj)Ny?KveU?j%nV4TU5zS;*mkZZq8oMf${{@qTT?+z2319yatfaQHnOI$bIQ@p5( zmj)XxZq!FlxxST=sdU%YKz8u9Z?>eL1-FN<8`_bF)Zbt%Jz{pPAlkxd4aN&G!YlW5 z2}jIuXWCRJa+s6CZ6JlGGkr-Q4T&?IXV6G$5KBuC8_Wd>Vj*#BkuT>fGDmaWL!)`g zPaviFffxM(_4NUbQhh3!NT9l|y2y=(DA}xdJ)$~WC|a%ux_Xd0q6D4}6UDc>5KIlS zaw`KA3pH&f`sP=3ue1q!G_j%rMabgCdndz$LJ1XM+^lAl+w}xi0?K>~LEZpGxp0!+ z+wkQY%u8KYxN^O5a2QWqgF-5x3gkH0B-iZrsoc7?RYuY1Km~ zX*i`OaqTYLPo)D=WySV2IUJm*uQc3SKJl(x5(S3piR|DP7Le$9aGpE+?RwA(AbLzn zp;Q5wxKW5#F7@3k962TJAZah%VKWClyuT@)!Ofm?Y2bwxaO6CAejl7A0E}q)5(;q$t?_%_MVvhD>{s1Cw^vuSAUQb2BgLB*ymNaFz;^r;>7*YZh`1DSm zg!nybcsycxiF8UXd?Y>S5@-ZEAgOXIA1X+sDU6!fnOGo{7Do$R!Bg}D%&4Y;xX10? z#ufRAQ^Jl>CHXrg-7X}J z((8KO`lbmvUc0mXzQuQk5aK}yCeg?}xD$oA>3NIT4(9QfdC=TU{hj-8w-bcl;&Qt& zvP;KvVoz~yJ7cX~$+~JvW;1e#f+XFih~7)FVdD9r4iaO3jSLsSw5drBbC(1R~yvA&E2zl3M~raxTOz zb~$u`!b5#Vb&I0EUPb3qDj7Y^iRxrq_`2q>3KKE$%XuG`RLYD^VU^r=q~ddH6FyD_a$0OiFd7t zM|l+V0I9h+ga|DRF2uCG`XZn{kH*B8zG9?k2{p-3Xa+r3eY4yGkNPAJ@rIXy|9n8S zhbb@}ui2B}%!>2W!y|M^Uo;=Q4g;@|6sfQjsfiTnC?vWHB;7B@yn+@Pkf|P`<<)H$ zDVF6~w&nGQ38#A)J>TW{B`Q<=)Oaf4AQBGP!!0N>WjmYoc}tIP*zZ#x?t_vjzIaZv z!K`^G75bKxr;M9h87=g8Fq{w%md%~wV!5R^@U>)yF62U&S062@Qa(3N1nO{@cwr4a zUo^UqEjDB+{@ez=e_@=6ls{jmH;E*_E#cJgDoz2!NhMZ3K^guFGuCw3$fw>`3=;>Lrc zy(x3KmHn~R8q;YEwJl4rEr-0V^mCi8NNdG%8#7ZwO=hvwQ48l#i{5feWRz-?V!P@{ z`ycM$Pd=85UPhmz+GA}y!ZX{O+dI0-p;eC^6Q5nN4)Z+8-CZBpijo8Wrs!*hw~l$I zrP!v8V{|RG!}ZFLE9Y*_cYS;aCN&7n14vo#gT3O|;CR;)6c6JoFUyEbn7&=*H#g7Btk< zCPK0MxyD~nTe!y;2xYdv)K*41xWsx%DS8Dck`S|cMUHKgm-D7<+_5yo&>JF?3Io1` zL&ADIPOX%3TX3&S`kw!|XT6q|5i3M^dE&%%;xCwxqDMuBafS$s@Ar$iN|#8+_m{;L zjI?~Ci5KQd$A}l^X#fRMNWR`Q6J2v|gSHQPuM8GH4sNvc28qF3Q5ryf#@qDm)(Y+Q zgPi0Va1q5gnm0Tp+x*#53u;x|Lw{~%!)%mlsD=bv)Qvxd&TuC(U=17G$>;8QrDG5{ zXHYTssI%dg=1&H^vfu+h;ph)yX!682ruHm33bVx%pGNJQ zDjo~E2g%qbHu2zsx}W6>?aacjojDgx2A0IggeTu`dia+Jb;+x94Q`gZw{jMK%9jr` zoq*}>kw>8h$>(X{UwWizC?ywO7a+bW93V*DSmv4;>Bjiojrn^5%P)O`ViK?OYoOu} zDw9->OF+a_$KP81i9Y^hP=;t86XMe54*=1nv&j@_UdRqwfJ-bw_8s}Z+>mSp;)**WrUqn+K=wW2 zxilm_&Kr@oC-g$95bRI>-dX61SLSZ_GD2p7h?S0IM}vuPV7_*Xkyf)Aor}O>iwYOv zzZNi z%sY>$`-uW_u2e1c@m7F3jvVf!pOaj^JHgtwKLNv6RMw;=L;6m>TDTTo0jf^pK|!YP zcq(H3e#L89I=&$)3AO#s0Fn@3+aY zI?cLDv zGn*puGm+-sFKVi=;?0ZgC7kMMfgQGY!^*t0Mal~1xZxV$QFF@K-`T${RvXy6cI_xh z#0_gh!|@~|Yw_lt{)QH&Go&L4>Olw#@!aYni@N-k_Q*3=<2&=yviFla_fuc|-(IV` zag+xDqK3%mNv-GK6dhT7hNQ)BjI>9;TB|ft)f+#N99sGJTE-lF^*_=02YnDf^$$;YeAzneXl@E_&Y)rPE! zf~ZE~0VSG>JU7=@A|9|5tAp2^7xJnXsK5Rwygijxz0!EwX`VE@$KIU$Qp8Vi4_t9i zscbFN86AA)o*};E*&2YAa(&9z%?57IWy0`2=o(d@-L0P9`ro>CpK5mB24ghHJZPisrO5>%{pfi?_PSi|mY%SeDfC z`3P%g73H|`-zS9vqEaxT&N*OlQ9}TCGhrvHWB<)tcFH9K?zdO#aQNTN>WK>DXHmn+ z097MZvlE`KD@C*56snx$yx>o0VMP=rOSB<9uZFqM(Gaz*Uof_ah@p|7ps;A|FdJ>q z3#CTOCM`uhBt1F`6q@0H=%59Tj<8NFD~nTi2(PsPrBy@KE34|;+O)n#JH)9a^dv+y zbhcGgj#hP!$4*R-%#6+bnq8V-UR+sQ-I)5lwZ5|nnmkz8J2~1u+r1e6bA5ArcmMGC z^!FK#@Ct(hP?PsF7!9Ar5LT1l>r+URk^zWJk&YtZRahylrDc|W&lrR?fVNQkNpS`3 z^VG09LMmt#S?`iHnLd+5v(>frYXW&jM4nOz>36IOlPsfM4Y-1hc3%dqauIV`9Y#&O z-lrXc^x7P19tXNFJ7iP9xi~_L&Zh`#pl%)AuYpk3W{%(Oz`fnZcH-*QZePU5H%gv0 z8(pB+j2sju&yYXL^l(xreIQ4qZ!b7 z(6)sNCc6ef8vZ0wOTp}PgS>cK99B~ke)AFFwVvpW`OxE&9BBX1NtqGPcmTZNX348& zomR==@4P(MDUst;_4B!bZnV3+U+Z|9yF2*RUH$Q6z)8+)ruFa0M8NexG=?-PIbydE z++a!>G-^5ZPMIiqrq@*XiuHQL2-K75u8}I@+&qwPTCVUq9j@ZKdHS{6s_W|*<;?`Ap<{0E9~KVe;vPBWU5FUbtz3K+P`{V*;BL38QDqD3Q3 zly*ukDE2eM;>{=Q!B7g`PAFq3qUbSH6t zux0w1rH5G%V0M}9Nku5Q?|nn4cwBo%n#opo&E1|`H^#wg-vC{-K51OAEI(;lAws5R zUvHZ&%rN57nS$C(B+!+|JU3{u$xhZ$(@sagWi+Z7+U^`UK7JXlcK%E)Z{4AIGLxxI zH><;{=uXdEWUh~!71>LpUVExG!o0I(K>X2gP7WOUl-5f3$*vAYU*LNQV=Wo28|La} zb7|(A{}0CYSl?#DEx80BR^sxzX3WrIS`HGzXK0;}2c^zy2+g2~slU|Ww@+1QCTH;1 zkS?eKvk79Qv=I=;v5%#{i>Tt%fJDd`TJbcv{GAp@71&5^kt2T9x?)w*LO^V-Zr6ewjV09(qj@ zihuJ*ujdM^nWgZLLiwBTPn++93RGJ*LUY&r9|i_F;V&3lPMqzu$5S49-ex=JM2_i| z=Y9IZUvIc9ccFkJwr~k}zmf}*s|lP8@DttO{5x?H6^f!|H@5eAFqNI*FSYvAyw?hT zzi8Li&QoHqTVwsWd`IA}&ZvzBiWNMkzy<``F2Kn@@vms6 zI;h9g)nx5{Ng-%`pxm8|6@~M`ZjeZ&A<8{Io`+j+qW~~!mb$oD*RS5|fFT8g%Y+*) z&1d&!$v0tDi3&c7uO-tZlGANE!aY*fvdu z{Ae&W1R@W~^5BIAw^Ki$@BqGwT+xR|({a~%&`*Re{3wn~bW zp-zhHG-J(BSe^2?MkGVuf~(Sy#Jc`P&dpQdXmqUtcbO!8{#FUYE0LroEjkCfRx}2d za8*r{?cDS$-@w3IKBsL6=QM~e}U-OHw?F?J4NlDXA~o8}x+)BX^BJ=pEJJOG3c<13pP*TCvv`UCTsbrKa$?u`pIVzjx{Qf&@}Hrim@3;+9eqO~NFF`x zrbmecNm0iYU$Y(I6G?VWWXbT7>Ra2HF-L!wOaY}mk!4aUXF6v26QG6|B#ELm@2sn0 zmq|VUj`2stsq#TFq1>eWRSnG#^jtepeSGcZ@a+s!X?Fc)pXJcQ*U^ZGNFfMmbZmT?T z5Uje0*_v}7!)}EI+V;c~5d-^_o0QiER_NQ6O}qEUTwNr!kAb&BXW-fr!(kxJXF9F4 zDIROna`M93wlcEvnP0?>1gE96m4WWVWp@2#P~HN;T3*HcPG_2)#r^5YlhBR`6*sp! zxGz~x+j$Lm?5453bKzU{A#6{@BgVgc=uLu))LOz)MDb^$EH&OAhTgl9Sj=CdSw_sX zr4pf`p~GE^_x6^ToCE=|x=1bs&C#Q^_kw?!@O1eCwyw|ID~Z|dCWY0fA*-%T`sPM4 z%)L{y_TDR=93Ytr&0IT^NS2~iYwHe;Jhgpho1ARZ==#g+@u#)fzxNY+K)`k3UcxhT zM!?*lF?r?ha@<+#Ge^!ynvcIu!ZPqCzgI^%9BOIZ8gzzBC;wUh7Jo+Z>3lgg`@Veo z@2YF|MBWBjyMU(iyc8OdK-QeV6Cs=JmYTfgTl*qsB3VIAU}#wCzpXbC3O;`9+z z6=Kp0OeQ8zbb7z%1ZWANnEyzA7oz4V-NPR$#j7+-7bs89Pk5)HO@}d+K>J!qb1@&F zN~}cy3JeGl8m(hgo1%ctxf9=oeHIFI1=y*-c)kE)V)V)?4BaOvGP% zgWC}7?JafW>O5q5K$!F4cgUb%J&r&<^?38I*L*TQExtJm5mrgz5TVH4L@P!uxp|LB z$2;wx1LE^|VXvJG{UD4CjNwkez^1gQ783cwg{Uq7@4Hf8rP2^(p=fzec=P};y1F4+ zS`XBJ7u`n!hM|E~?xJ3x2y=K!IxS#E)bL87aJ_wSyJuJ%AZFp5;Kh+RgZg0uEoKuK zbFLT89}51X7wdo&X`CR_r$^mE7y;go1tUhJUrGD_jJQvd@Yjob)vAJauaE)Ip#-Sj z3B~8?#kZNqlBCC@^Z3mYYm_Z~%0i4vt<|jZe7k5Kc6ScOqE0|?mP7m*=8KfL&YgHv zs_k^F!3zZrkTJ)0I>Z~iUcV<)tQu3Qz&Y#53C1Z z5x5ZmaoM=xgZLUvOn8+;nBBkiV=>d1_!oS?aSDBhr1#z2^Y{u^4fp-0dTaYoN-Ag8 zZYSeN1UofXrWEJohBOqlIL`(%!eHSTq1MDouf&tPR2SjQ26UWvVXCUZMq>!n_+C2fgh@*6!QVG$J&^_A%8GzgUU zXJWV|1e@k-g!y6FJPW9uw2t;goLrI%i9c0I2hf*ZO9L`(x;5te;V%)x;_uu z5G(<=2VJUa_+|((2E165lEsjDJr9yKKgjY&o#$stpcQ+m4dG(>3sc@&LFBBBCeF#_ zX*C)aXb#c5X(IlCB51U|ZC;F)zRzt+3T8lb$L6(-B(m7m!I!nt>>#qxRkt`F@Q$7+ zl5qD?yQO6&`C>xKDI+Pd(ev?C$BgYK9kyJFT#G*Xycs|P<70d_Hi`X6-4vh~*fq@u}OTH4ZBlHPD}4ofcrs5L~mI4;6EL*t7`_;+Z6 zQBQ4!dVP+nixJGxElz+!E?HMU4Jbj#nVL|VkmB-Q%Ah4a;M+V#wqw=Sx|(lmjIy`G z%aZ);E~A1p^Uk3BIWljjL1Gu#>WMe^5K^M&;_Ka5dV#5i2cFD zp~$S_L3=$|A1D8=wwBp_nN>NFNxiYK+r)VlCs5h;Z6E>y^*!?I*9eFRYzQx(8f3Cp z2&nMyh(W<2p<&^m7d>}0I3_kOJ|QtFIVCkMJtH$KJ0~|UA5u_QR9sS82K|rirKYy7 zzM-+HxuvzOz2pD9ME!qf2cDdsonKsD{ePax;s3w%+~&hs^7X}InL;jyD?{}qzj7sl zG2SUOlui}^5?Q_sHy6r=ef|MF+yS z{>u*ZygK~-zZIMQGexc48jSmYp2_ulzw$msQ7X4J988yLmK%+=H6G2^n2+Qrw>O1?|`TBx^O8}IzT*nu4{IKk<1 z<6m8!kGJPrBf0;zy}b1<=m5#)$!L!e}B4w{{K!_D2V?9w_A*|aYy)~Tpr2+R@DG!S$P zRuZO5dYaE<8YStTG{u%IKF(4y2()NYujm7It(olP4Il{gqVEY54cBnbT;!j1k_0hCDm9eQZQhg4I_l!Z-GR#-Tp0E$*aqmc}CA=vhZGmxm|PT<+B(rimQahZB;c$4+VRAk%izOzpjnkUBc~@{<8#M%}cOPzfHxp0cN# z^*?gZKJj-xWdIZ5?U$bIW@aD$;t>^a7=&lZJ&y6uw?lJ071y{(sQwb{2!9p4$ zAb3Bvl^szc9;1}WV6%Ihd`CG##l=v{>?5d$T1ARynClh8W(u38CoF|vu;OZ5>^53+ zoCfuz{Fk&3|M`!+4#PiFN`fbUrq$&Bq7P{3+F#G=Syld{=YB&vYxIwv`(N8jN#)Iw zUGvG!f0wBLVF&J2-md!Ip4_f|N9DL%4N5(^1?YD_6t)T{*}EPRy3bJ9MueR{2%n(#NVDSV2ssImoIwmr>hA$&cFXIQ5USL z|K6;4oc+CB59fS_-)*P9___aO2R5HQKc4@4CVzwfz1ypXKR?}`z5E2R@(@q~Kaj}I zzoRMTArZrVpop9Y;zZ{mGXj2M7@P-@jOL*T!hZheOvd_;?FBF6MbEAD?-I4`Jd`Us zAIl2ROSW_#CN!Fl?E&khd^iu6z=Gg}1NvylF8*^Sr^5OeL@pxKq9Ft&fPNN(i~q=8 zn*U`7zUaA01_1*+85hw&r2?|~f6rvc=mLs8z@W&|MU3ZY0p%@h@WVf{7py`mRC$;b z*<~C^sgQSaROXd#2((2$DvB|I??tB6TXepn;pGO1Llh(&j3 zSf}mZDXNwHh{4ijO3P>w$3JKCf7yZI@}p*CS7|V%VxH8YQA?4l^oi(Vz7qK{8-uHi zh0$Vx=AkhM@2kvz&g4P)ap#PytbL^tk@=x<_qMC-^A|n$-ao~rtDL*h|1MFlauME? zN}wuC1d;v8LnFkR2xDl7a%VI2Na0s*aKff$d!Ce~# zjI%%m$EIgz8itq6ji+zwzU-`NHtxz)s*-l$I$s_)oEkzrd<-dJKh| zw{1xq$Bpnv-4I#lOAB>{cXHG)0q&IA3|oy2q8&RylwPwE@C`Q^UYnA%zH2hy*O|!U zPZ#|Y<3>%j!uPh+CWmNKF*?PGNO)6hBL?ICbB&d+?N#nREhEg_{BtHdc$6=%UZymI z)H3YK@j?YjZd+i=RnGS9hC5SDtz#Bdj^Rgyuo!65{0Y#WH}cEcTxmOBor8m^;_8LB zKvQzjsfyAFD74BQSI7Gs{dT!gK9-K+$0D-iLvOuLE5vwgiZ(`~lWPwug*8-QBt7B^ zwBj398Jj##I5mzET9F2**3$w5ZFlpt}sg?w;=_2<7~$)Qpr?i{S(8VCm3R4-T@^V zCS2l5@>xkB7B%Y@9LFPdkJc>NY$y|>ze;6CHh-rr2`~x0$_%!fCJDG6@T{sIIv=r! zQA>lUF|ZIwC!s)$N~_7k`f?u$H!wfnPe#Z>gCC$K@|q$ee(d(Sf}0k12CjWThEKY` ze1Uw%z&FA-d=V!ETLF`cRai0n>9wP#iH#z1nY*rZD<%+HUF(TNse1Cbr>{7{t_FTm zt_8{P(PInEr2#DL@7cV6K?l2JRN3R|p%q^b)z3Va3%bgq7O_lnAf>a895+-9?{-Cw zye#I&VGz{(^^fuCFm;CVgg#(J;|F7~M-FJp0VML^|kR%DYj~N8BmA#Ks)AdIM2k&-WNSc9@-8 z$HAXp4_}JyGBOSg$jBBg#T>LUqn@^ecfHSx72Fj_(=u=k$(NMi(LCn&I-XJ4EqN4UU)oE#^p&+kxPFUxxD%o#mOHH|yL zmv0PD!sms3zHQSqT$Sv7VZ@oG4!G`B{Bq87U-|9b*>6_227q6<_v{|!?F1_26`50& zYy6Shk0_~Y&RFq1ivX8}0;AP^=#hUtfi8n8=;QnA{eG*}?fKoCTLClS%X;#<)72bW zHaLscZ?3k>O<=`L;ldL_oCT5dLvW!@J^ znE_0kUz(rAD>euYy5W=pbR`}Ed#Pxf79O?<-A~_@ZiCs z;EyGSVs|JXVZnRaMl5n688X4|^1T%HzbGz*WDAA>NJ1@p1K+iLm17EZQ}@?v2{j}k z<>S?LCJa+Y3NvR46EF|kbPuy=3A0%Uv%U*s3JY_Bg*lQqI|zk)0K?txLY+&)JzK)R zriJ?g!UOa`{+^)VmxdD>$PXA0iX8YX=U=LAn#dHG#1xSxydOau4q0}K|T)=6SSUcteGIx!?3F@1yJ zL%nc4$(Z@Om?O{VC8pT(7Vr%q_F^IWem~|B2>vVdGJcLljEF@VihX^Lfg&G=AsmN= zPS(s6)k+foRzIqNBvOw%z6~v&ggL%xA)at3{(Wma%|U#NPy)4l0$D@?fqnu@dOT}- z0wZZ+3kZW@D1iq(k(W7UTr&};EK%YjY7-^Ng(-qf8hJk=Q;>B6J-g;`u5=h#4n# z$UL_uO-~LZw$*YsP2FB5%b{Q0KAEk*6}^GT^rvuG2eWQ_s?Ls^%E)d8p_yG|UN)GE z0|9_O!+_DUm|d`=%`D~j%xHaZpuJmWIy0YhtpECi)bxBvOA8_Q@;*CpB1_#J{r({9 z;XdGzpCdLkkw_S8OI{S64Bg!;yEx9KSO#qGNnnc;YyZ&x6 z9`ZY0pM=A>>U_fi3^Y=-cp|9>jA6g5MLMh@^31xr21z*HAjU{HvJ6Ofo`oz!CQg`x z>Hsjg$$@Yw*Y$P5p8?iM*0#g)4SBp4-R_gV|OOydF;$6mRK?x|-(W$J30p=R_MWC*v`=duYkV z_F>6}LDfKH0<0=D5)+zD^RyLiNX7Pkh)&r-!Q*_#%v@H zYr5a1Lt3ujrmtXcZ?HTn2+EAWu!85Fr57}@7+}%VUg|S>y5$xa&{shl3s_p`@4+ye zl7u+(25(+ZcQi#zy6#2}?H#I7Mefcmf+)|b9j6IEmS)NHWOn0lA& z%A!n-WG)TUd8bs74x6gfRj#mhoUx#}AXUH5=&G^oY?O68%XERep1xmjwAXp7Du zGgUutsUSB6OXX^Bn*1oyBGTk+eJm!`Nh45Tj1h8feF&8j38!w;#MDb-5hxF1aiG1; zM|fprC2KA*>o9-l4@xhp#fLW!k$+vXsbsS&R&g&fO`$xNPjOEs=6Baeu5fDXN?b>rk$%GG!~pKyft0gy_24s`+*e zw3QMwWCr?FYO|HTW{Xs#DC9=zFCuS;Dp1Eqp&kZ#h}8ux42CFt`=0f=bfh!Yrkzp8(Hg=t)6^i=SZsZ0=iyeqN;=dKY=QE~ z=}(sVgOzcst~2q59ucdoiF8o^v(ZRK-zH1`^03LGD6$QtfwEciV6OW&X7A-P>+EBf zfLm86PO%q}Gpmy00Rx=ne3@$W}rz_$bog0k+*NaRv$ zB%{N6XoBwh^KFMO-m?1Nr(ZU1Wg)2@>Zwph3}+$UBHMjSxgiLaWnH17v-F{f(+ z#o8%A4#6g042zHKBZkqMo(7E3EcS;$gQt@z8W%QERr!h?q zZr%(O6`B2h`Xn1QA??=IGWdy&X83nx`-yiJX=OOWU|6GIw zsweMa*bOt2uJd3T4^j)?^L=)9Q`sFG+$(U{E!f+2a1hIVcGZw-KOHN~rtxmlE>w;T zq$I&KHDX6L^7a^a^Y#etwsNtO*b_!h_O$@qJqvKl}GuwOQ#Ych`N3De0+0yJodcB z#StR8Z^$yfefUAljeVYpuH_O9i2K$1m z#oKt$4aN8(F~-*w^F#C+~x|uKJ#nZq)k^b8&Jye z0s8_^mc`ZRP72lMKWoYK$vgW$1~2r2f>4Y#7y(qwcYj*?ZL9j7gP7CmTo|!_9g}t+ zyyiH_?K=D|bP2;Qs)}1D$cfJ?3En$py`tPwI`x!NIr-*)GR1x?_`$%L@|@?r9|U!Y zW*mMoWptv0x*aip8W505?BKX{JECp5izrE(|We;@?e{@#7BE3};@vpEH!8H_Ok z#~KtH=T*GFwEbZ8it-O9(tVNKrUZx2`!m<>QITi7ZNjk2kh61chzA$yl6dLq9}6FY z)f=?#(=v`fw4A^DpP$xcjsm+b(AG{CbA5lQ>;g|O*JA%dKX^>Q^^3zFc{$C+ULOTJ zXx5C|vi-W`OVJQ%^x*;MoWY-2E6=&!zQ|r)0^5*ph@hazh_GN#TugN0x5U`gp@_n)w(*L=&dK`4jPdf4 zvCYwqwe_*3#U1Ez*6!)Z!Dar&?Ct&4-QL6GZO#&UMH7^$oFxt?L#h-;He&#a}KreJc!Psvf*apXT@Yh^k3(kHhxQqwJCw7k@;1`S`?Iq;FG zTTkXQsxmYzT}}T_U<*H4uEp|M8IDI@}^UJ3&boxPY0v$sGc7C4N#|gcn zw25qIbB^OSb=_a7jXNqvI5(I7O>BMa@^NAk)}HPMA@XZW0pI-K)f!LiOTbMMtTrVMlvh>VE?kT_$c-+ zrY%?6Ip7vpXt#7LMr^z(%Mmttxnkk)XuFSeQ^Ju}GJkQaK1p z){rHIM2CgSTd*dQ>J-~bKvH(o^D4cFGs0`jEVI5fS!1QT868+;e7V{GN6#&?lOw&m zxR)Qs03R-agmN5~a__ytEAxs`5=o>^u{1a+PIEQ@6=#P%l$C^h7lD=*C9sr1lg7<# zprvMahtOIKvh|9}94Ko=`_kc2^^YY`IcqP`$YD|E?yzmm=pj&^d19RKq+v=2)1?02 zVw3yPN%Q*Lz6z)3fj(z}=Wym;=fjQSS=U_*dQJD! zv*P&=6h_1I4kXC@Qo+5#;C2t1rtf(_hHeG-0M-@W#URqBr&AbR>Pl%2sS8ETFk1;* zbZsM^PeoDVk1V#jktuJNG2UB>`Y~bF&vk9wnRb6B0gQg+6C$W!muWRj_UmcBMR>bY zGW8=8PyT1tcO$(nb-QQ_Ut{f?=7IlvVtb-6NaK?2HV@hbJFhW*{tIMR>C(O9@U4Do z-u%vZ>apb`S9QA`_1?U8Bu+EOYbQ`~ymc$vcKmKXHj(mSFXvlT+d*D)j>u7UD#znN z`LENq{iZ<`(c`4|{%^*N2U^(A-Tx=Cov8*~r(Fez!-@ZBX#WD9wC~0E+^i70h#tB_ zQA)BFJ~R8@ZI#46Ke*(qbv>NbpFKaFPjd$Ry<4jeczRl13wVYv=fY9YB#o94Z|yo7;sTyozw{kzs7d3TM3yy!wEYWZO$ zXN|;Cu^nnFMDd9RJpSq=%7up>X$q*RaP0O-d|PQ1tI<$u4PcW#4iLtGo`_|>Jwh6a zuEfP%oAxycgD=#yNA%#-2QIuqDd2_LE`8pWj(nO!SU zLOY(;@AVAG=5n=y{yB&=(80|fqbvDOVv98CR|i5Ddo78=t->p#f{!fz7*UA7!Gi$v z9p`&wQ1X$bXPC~SF;1|aNXYn8$nBkdAl5DieB$4YEJvU3(O)zNWdPoT-fA}TFQ&CvNsZe=JkueT9 z3Bm5EkoPwGlv=O;vi&X($=Jh#PRvyg)P)@QL>sBs>z0v0GQRg8q5oA_=ozjovm~%a zupt{QByuaA?1O7L<52%%$iJ4yAFD9C2-Sf#L5dY-p{8<*OH&y-0*}ng&p(DrmW!IR_9uEQ#d({o7?xh)sm|C1X;62kL0a{bi;W5M=tj-hi=JEd zEzvg(baxF1xO#!z;-mU%Sp!65-3vRKzGESIX^@po1=*`yWDDmXbf5sU;=y+D3x$Jf zv%fY9E*ot|tjOF*dhek9%K1~A(oTngHL_7@UE(^*{fG&+OHIsYYCG5-RXPfzXo1{prD-?LJY1r zMcKCW(WFqj*FYHDV?)JCs3;SgAMLgBp$6(cxl`D=D9zZRj%d-8aEzNe7b&scRyf`o zcVpFyLC^!YZAd1F(-VZY?$?4gkG|===!r?PzJ9mp*+C>Gc8p(h{a`=W)zmcK|Fy(a zZtNz`je_)doJC913jRi2Ad-~P(}cxbnLn3V9Aw#k`Em8`hR_OYCVkQ*bal+AIXZ_B ze(v}fYA{cB*nF)W+D30rKQv!}D5`Q|na2Eb+ zs1^`6!{`hCHOj2$T-!GHs_weN^9jQj@$O zSA0ngDw&XNP$)wXSr$e=aar=$jwzU)+F;uJPxBdW>oM<#$GGX|o2FlWgOXj3@o+)E zEtH516^XZF?t2Pfuq~?2#kYO|BjX_jf~>TVH7^SlkIN!@K0|;xb(5GF$%Ns<ZGYZ%eACPIW})f{mARFPiWybgewdKXxEU%F~33K?2?z>Nn5q<>fv z2U&$q{ZU#;4syL_7{Y!1m4x;J&+m3VoSwvlFEOx6_tQ^eH<<`bDJ(ejV z9_XIj;^BcBkq-DLvCRfWcnwD6>wPZ-d_SiaNFc^?5z?6T2zY_QoC|vIF)|cWM=9Pg z7&8ULFmn2qDkA9+ArS@9rIFgA0ojN>wMtbsM3m_pa zA@J<0fZ4$yr+EA7A10V?lttm*za+snX<(`*l;SJ=x_!kePyeP+|6ZI4$M_6_oME*$M5Nq(0S>Q zzXneW(axeJhU~5ypD0fNSy zVi+}P74*@pBa$eb)iayl&75mq2_Y{7!K!*hOlVG7YOilogBgdC`4`ZhfF#-TQKjct zenTl{1AK=&@1kq8hS9VjMFj2)2}O1Pi?F+3iYs8(H5~{L+}(n^L*ovOySsbi?tw-d zcXw^v-QC?K!99W#APK|X=i8@d&eYWWhpy^cYd!CEHphL5PW2!;AO6h|T10^Y`1U3m?Dg&Nj` z_FF|^`$gdW+^?E8QyFm;;K^w@$W2^7U>(4tDh5$xs?~HD zprvYC(7u>e$8eOs)ci}y820BHMXqhlp)Z|I3v5C#^{QSpsD={P%?Q>lEBx0n>SlM{ z#&+HIW!)BW{q9TMF;o4%VEw5<{dsr&S0)M<0g7Lk4RXjX?c$`*#dbINmW4TK7u_fo zU_bF;Tihmujbvx}p)CH7q+OC&IBsPc3ZK87a`S~XHY~qEmCun3!%K-{Y)8c=g~RC9rDvsh^}i%=7jP`(Iq*-27EK@M4AGy0lagGNl%>$a5hbgi+2 zh!b%I@Ty|BTfkml2{b4@n?rhbkPwYoZ7*4(J%cjwQd{O=k7U#40I5D9s%sK&Gu)~9 zW37qpuein?oaoRxzui7d?03)83L1n9)kiSd29|cG*}S%WudY8M?hFy?{QJ@o!Q2_8 z*csW=`NN@(2-fz%&p|9!V~-aY=ifzD!(FbA;_a(*)~bgTK=nowc;$;y^r?#F(MTTA z8Iwqfc|%xz5o%S`G)v7mnyI3yh0r9FD-KZYHhhcMDC!wthWAobdKBo|4aFt9t)B$( zO)!zFz7i}Mwyu&?3<)xr_9US|qz8n;K6L3j?ALN~>PmuwK!a)6T~XkuaG!(P3}Do0 zr^=5S1g9%SY3A5-p(rmu!qIK*_auY^34C|%z)+9&ab-IUZ|tQ{n=(nLd3OK#Q!vts z+E3pvZbFE|uTZC}P~V!)=$TG((qV|uFon|a=fGhqqhTha_MZ|SQ#-w7YtRa0o%kBl zE()1$P<*1lv{I3dwtg7DR|1b+gb|{Wt=*_t`ha%p$k(eb?30*_%p7k4;9Q3m4`F<3 znLiVRRIJwD^kQr_tLI;<4)KiM%W6}*-kYj6jf}Ell?E3u&}J2f?Ot$v$oiu<{EaY^qgw)G&~lWYPtpeAy^EwE=eg>;^d zWqx0H{?usxe0DgdcV6*MC2I!ynR(=56-Og2r7KnyyJ=)%*0>EmJo|g#ba9H9^+d}~ z@_E!)=Z>a^T9$05wDn@5jaTGb+?%>h)(DG!YFl_@!rI`knRwaOMatYbU&kf1AbvV| zlulaUGXI!6YU2fSGM3=NNYX^4@Fb(*Bwx^^fYS=|RkP^ay!ag1w?B$y4k9NgQx>4% zJ*Db0*&dHEv#$a1MT*s1MvBv`>5p=uw;f4V_nEzR(v?sC-6XMIcv%>baFFAd)23-U zA~mzE*xD+DYJwp#0GMJ5Y#lI^qCkVmVzzYE6==T}={BcG0k1R&M+uN3_fU?jQ;z?k zyql(+R40;mS0@p)C4l82htiR%)Xq5xbk`sEjGhwtliHG9v~D+8?KGYBXRL&5IzVoy zZFS`FPqIqh$XR6?tNkL;k2e}K=(g(WxF6yytNnl}(K-XnrfcVC8+!}ysj+azJc_@& zmGX!}aa)`DU%NR*blV2MeVxny+Q8jgY~rWm5w}@RucVFk1?2k0dQp4qED`JGTaCWr z57t4K{5eeJ0jHcOOPJ{2va>6@qx=ya;~-tst?69HI@M9Qhgbgr79a(y8T#(@@n+XZ z@oei@M>$2?f?8Ji;I=q6ywZmSKS+*uZkEvtg8kNLLTq*K)YtXbHqL?#UXvq!l_SCV z4uSb2A@n1$;A#={V^Nc1>EL5Y@?&o2WBK{x?Z3z3Sd0lV(jfUB`5%4~ZpBR~3%y2` zJy$0@4_z+PNv0}{Jv(pXpMM0xJB>*@rYl-CX*dQYbB$WBs@o%~5{7Eo{|)>wc6t@A zsu;HmR~-pP#$>D0+p910ti^@ z3zD9iVgVEDv03G>n<;<4CzJh1L;JD9_G9;GE2ZGaXk$m?;14cc%-iC$1V3b3e4HSg zs*`#{`_o46$GKlcX}AZaSI*=g6|REl&~H@AQmzX*nr!V6%FYIVCCKA;$_TsbVEpnk z7BeVesc#xp?49X)LvX6e%B}fk@w#9L_f zMb1A&%FmaX`v*;3SH}LgH>Ve~A8zB5u96ooaw&f1P~2rFU5u%q_Z3_*4%`VXSfdNJ zF~1c78|Jw_chya6DSzuw%zt42d6UNWpf~Vfg7M8lY|3Ea!PMo^()7_>_0eqa!T8IK zt14-wMF^rR>W~@#m*%7nI%JV-k$V3GsGA-dnij zt2&1}$Y3u0+uPk*$o&ub`xe!!*1cD?gAj8hznbpKR?KSsm}RX-&1Y@>>7D-O_(Q692WV+|*z^O&$MebHln% ze&eaJ6+}REfeC|g0Y`?1X-9{}MZ_jX#l}U0VYDMMVxp5=@(XjaOR}SKD>74xAqny6 zk@;n*1yzZqwOKXQke=S&`poRknpkbnd%AlZ)Gz}|>TBp*{{PN%|7T*m2ZDKoIKaGw z{XY7){^1+-s7n!LKqPvfe#Hd^RU`_le0a1gC1qsz8>zyoFFU1ZY!?&Ls2-N(aA+NR z$~4Wb`FJdnq68^X3$|iL{r6{_%>~-dY!3Kz>CuLm9DqX%&}bb>Fdm{(dV2_QCs?b| zsIyTH_qtqSIM3!`- zjI(P~W3h~sGQv!m)YFW##0flGobPM<&#Be&Ii5APMQ;kPs46^6!0eBtsqcW4iiH+s zz)o(*|6^kN9)u%I`{TaA4q3RLB;1Gnh%G65i*;HEuYSvXNs&8;8NxMPUseP^9 zJNRrVYWFf&wtCRv^VaQywmkU4AGIgiw@(iw`h+gII!^sDG*zOS*y9Ib^Zz@_XHB~PlE6oO0dkj`G5N%>QAAgno z47geA+M`hXO>dPJSfLyFUaJAms>46q@1?kDe1fVoxD%PBz1pUbHCTq=aKadJ&!U3?lc)bVMfJX7O{z3VnH4$0r<+SZe{?@d-XyYK^=uaP zyYRfXPrjw}eEy8q%4UQ5I{SEZl!VE?|rr6XDEl<}7UZdw93*w(x?-!*B z;^%!o-W+r1`#9r#E{8kxH*0LV)U+D4TFvwdHiGSC3Vlp@VMx_)2zCE3cv&sCZXPyg z-}vgj%}}~g#Y#J9IKt{v$)(#h+GD{^6f%&%%va)M$3v7Uq+US;dUh1wy`+UEC`yjJ@d za9Ak;)-?=6(~W%q$jqz}&N}TaDFz9}(hv_$j-{3C!pV_aE+r5?-r=`m2n1FUMMSoM zHQqr{z(A$i0ZR5l^uH#yiF&BRyYxXp$#c0SovrIyif@=J?_xb7U5`>Si~$jxTt7ty z7iIY`*I(|D1FLc(O4s#}@FX(IY6^kC5Dc-XRLi?80=~y`sDE=ewPSb_&6YNX&pi|N z?yYtfqz_8zp+yv@GEUp#m5@L%LOgQJ-=-%}g9pWwCZ1Ctqy5js7IK8ps8+5~o`aD1 z4+dWX3qHAw>Eo{)ueAQ5I(=j5Ty@`w>VB+YO$Q#M+p<-FI9aN=r20Z2B4Gq&CsYk^ zXF}Ir*@jnusOzr@Up1)TSv}{3)V-{Snkf@glIvVzdP7cUDs6DCJx zd*s(pfYnuVvG_I<+rsl^OIyd~98`u1OZdmB3Q|J5QWaKYR51DD{gQx~v^lTJ$jK3BBhvA zl*1XQ9v(INjlS)H1ySzsD1Sl|P+|n)6UmiES8*o9!c>vA-_9~EH{Q9Ob3iBbJf&8F zx!jnuS%$VrVq%OIbi|zUSRc+D4Ub_wrdLwxr1zTO7ik7xldgDc7T@dF z=O%-w1U>nRzB0x6YRv69|F#?V;SP{t9RJ^aZW*5uqol|mru|aJ9%(Y1neEJunXf0) z9}K!eGFq**n2qFP5mt(IqTA%+9kdHa0lE6h$sOi7Dp!v)ts?R%`na*~TH8y5=z$A46HRx~iAwnO2 zS)M(j^H{4TwF@zBPIk?hD(Rv2r}jhUE`C1$m-B+AVVvX$-&*^)+bxky!rp%!v$iT8 zsH&bq_V6ZxlldKQw{ZB<+{aCJZ7JGc;1n7tCH$%5oa+{m6`UV}9Be+m{`hGorLZXQ z!qg1AJOupJ(_?XoVtcHbU!!0mjDIe=9x_bp3q?g4qR0TLJaz8`L6m2g17B{8z)zB{ zx;^L>ypJGrhMA!iitO60o)!rTjA4;fb?Gz0i3#@L^=0VRJWUm~gX=fH1#DMz9myh<-Z zk0&btV8;!k`e4bcZQ)5?E{^2u3!eg%p$7-!%F^8eSFzafH02&sXxXR&n#0KgbiuLFEyFy{%;=TQ1T?4wSdJyrVEK;eS^ky* zoJ)h#VEI3nv1qJ__LYRr^BHRpaxU;Op*_-if0W2iQ6AHZSnTkIi{SU=A{!lz9N&-_ z`Vea;KE;I@P!H-RN%3CT_{1?31KDg2PT5Tx)Wg7zEAR&_SmO>`0u zim?xGq2ZMpx;VbRDPbQ|G3=y;AgeNVQYsmQzG8BIfUp?O*AF6(6acsg3H)&I7rBtA5(@IwSkn&iALc?@ za-s zY=s*Cq!K6fjzCZAi>__lcZ-<66yV7_gjZ>Qpaq$`bqd9Q*Ite>Jv7}N9j21tWjygY z+4JvcF3yDdhM7-o0U%5LFC_x-2LLU8rdUi#u}K=&S0!rAi`0nZ$OP zI@Fo+DVo7VIzV+k-RI8-1Zh5cV^nB_`_6CYj1WSKw9^b+JfFhev?R)zf0O93DRAME2Pou~Z?HjR;XYTLp zLTe(g-E@N4$JAV2PeVS=#a6E>7|@w{=5vy1{sm5V5prTV5nMN_nkEHumu{w1(HXBT zte#0qgtLLHi4(-uv)!bKCi+#p2u~gn2~6?n6cNcY2*0|J$Ds7jDfcn2>kl*Cq(xm$ zZ`aO2_aa;)s&!ZK@mvx&t4d51stF=UaT(J@Sz|JlAaz-M2(fOptB_5(^tfve5RHF_<#Pr$RHDwzre}wH#hZE;Rs7*+|F3}a+E?a%-us(d7>XfkR$wQqp8s)7gIt<_Xn zqh!gVZA3d9j8QDK`3T>kHC9)z``1Bn4U|&Joa8ayC2#U%%(Wp;(wh=hTQ5!x7yoKo z$#!eej$&9|+;a!hpWS2~{tj;=&~1dYTbs(S#ESe#5V~z#-5Wy@2^}R)v(~Mk&Fx0? zSA9=nQbJkFhGff>MkfO83IIORg#dAvf@&+K)$q1GqOB#~D=POmF=Dj^x=%SBN(JUN zWza+IG;ui_vl-`<`;~qj#!icW60U$ zIBQ*{2%~%kZ`RhP(}LV4@d-3nWS|s4sKZ>jx*mb%=Lf^oV1$pWTT}I|0uAN0#nZ5( z0uGZQBcNYKx-`Rk(!r2{*(^_{d2rjo!l13j%Y6vwWvbITZeX##+1YDA-Ll;gZ%e35 z$7%n@N!=!T#udsPE!>*|9w%f!9YL42 zY1tHd_v!Sz1GKdf#1EcmU1(|J_`O$fIlpM|3v@6jS+E#Krw(!?T4!N!+Yec14PjZb zaFzvA>LBDH1!n^&W;Q(@Zs^nRGrjbZ6}E7xR|F{G!f}ZtP5Impdq~SU`cyOrrHw}S zVC_{rNjU5(Gd9r~EOBR2e*9UnM6{`x zDRQBsQ~|@@J7F9ulF4vxA|X%8Ot*1%9^>q@wZdF=@UWr#q+w;{A#YU7F2`oS03H3p z3-@_y?KMM|FnA+M);RWoY~Kf`?`ex5A;&NbY8t$HlMx?1lWKC~EO8{0%*5}Z%KyXz zo#lEZ)9}90Vq(fm6oVtz&Zy{{5pHa$>Sm39&!XS<)|q9xJp5VaV8xK!A^0tx@;8J! z0EDK0FN`K7Ono=*mm(3(H^)7~`IPj=ZVk6O1s8X)>o>7H&d5FYRXA>Mmf8<4b_P-L zz)TpJ><=53CNwhNpdaN&xR$;Ggx%IR@dQ~7#Dq1T(;cSv20%<9% zBUEdWV84u6vIJo=jCmeTT-KJiZg&d!PNe2FPRR1=^S9tbwaMune4LV&KT<$%b-h}0 z;|WJRy*DPBW|*hfq50cRu-+=8b!fmQ2C(~vr$5fA_B;4DTnk=n?GEhA4EJtIDL53n@71ObkDYKNHOD{pp z^!`nEdM6j@@J7Y|JMaBAQ~8g)K*jG^+&H_vW~$A8%-XP)w_*sg)vi<9%w$%YTieWg zZgg2wB;+7C?tDgqZj0t84=Wp3{Qm5Hb^2}pGQi{e2g;YPP7w;2ERnX_$R*V;PGQo z)0IyoqUGG*8`1Svs(=>R9Av<{k+)TuBz|>Gs0BqP+8+6}y1)CXGHM2Alb!(+uh^(= zhX>cIyS>+qr0Ig}#T&Z*Bd_R0gIyJx$GP8kPStI5*$)W=FD&%d=2rpb)}wu1#uG<^ zjGnF}?x|ypvNs5bWT(GxKGWPYM9G=YUjv*Ild@7q3Q}gJzPzWeM&;YJXL@ip`pnaV z0B=$G{rde7-7i@Oe%e}^B~M>EkTE)bgQ5tk3qkR6%fKjEV}`A~dn9mtSe}X-sst;^ zWG)b@7vE@8E&TZr*-6R=FbP?;51&g^Wl%mM!o1gB!mYp&>edlpu#y8XonZMz@%lUrxwLmfs5X|AeC_riFMU5Ah4ohSr{av7Ya)6t?5gM%^j!% zX(TylR5kavmCmVmgl|Bsd#x5$My<8?YO))YVrM|>Vc(Aa|4{@HxegsPe}X2HvDj(~ z6}YzXElRpLPry+d&0sDAt*i<*!?`qsEZ zbI_+diQD!dB<^0q+TVBTPz5xonNr<1_T&Nz@Vl{Wy(ZrODaQJO>l8}PX=B;wv~B*& z4utN5QF+hbQ3M%&6=2QpD1wN5wD4jIYYB~I6^$YHG^*-zSX{n<;Qgs?1UFnt4ry04?RXckoMvE3wfMQ>`38N>k1*2bT*5fY6-uLpa zc2&#Ghb$Yc2l2Egz0(K!QRE6rc~!p!6S(pPH==1cDmtf~CjF16NlNakxAlW{K>uS0 zN}lh5++o9)Xse~+oRo{$nz3!$yY-6XluxygV#zByQ=?`Vk4qST;!%z9vXDQ>@LJJ~ zy&<+jR|5#-dHMiprER}w){XU8sj;cuu(NPe4?26uD#FggOSHN}0MW$O#{pAa9i-dW zG1cNb?$k@jCsULzcyhZ`EL9^Zs_K}bWNxKND0K}V@n5Vv{(~a0dVyKC&*Hci&YUtKKC@ETmt4H+z%jVFY>>QJ z;Jq^I|3(qQC>!4frr5M6}dNbAkpa|4r-kHTN*YjjY4%BA`k3xbrGQqDc@H5D1 zV#3R+bd!o>WI<6~66kR;A$e3X2vxF$(kRq#4ja5!2;yKj>HY#$Fqq1XXsW13EU)GP#3lu89}~R*yA@~S_HzU=9(mt-PXU&^lJ308 z5LzA+EcuNw>VAwosa93KhGKvT^8cU+ZF(A}- z!)>I9{FhXnc~Y^)3SO?CgcM(r6BwoKY4!P9PD4{fO5eu7j%gs>DNfDxF%$mI z#Q>!RUIZiQ6`%Pe5Vr)tCKN}-vm$`rL4^gmvHGf?Xgk`!&f#J#O()X$+|_}sk`l_K zT!gR4QxAdH(+jV*qF3CS=JkR~&ap_H+A9nFRJSS2hq6#!uEcP)mepNZHZU~{r=Kph z(Dfn81jNWzfmPbDoGNa3k+*ZwdCT%2=oIgshy0wesUv|01z%XT2@{N!+P=BKb&s^Sc{9;N zf!-7?i(0H%nWg{^X}(g-{Aqn!vcFc%KBKU?jfy>rOo4TBLpLsLB}eo3Kuo5R9lKf!w|{UA&n7 zPaLjX-9C<=Y#F#9UQGFIYi2f0E;?U%VHeo}xi70w6@W!SrWVa7D`^xP&Ec2>yvHRv z8muL`^=Tx)hhbg~Xxt2e_D32aKP;nZtF%FmQtGF1VmfeSIm6eH!fN!+ z!B6Cev4jR{SpqhBluHn4;F0Trs7&dE7UZInRe07chIv+pVG5^H2j zvPaW1UpOcc2(@Ch>pYRJbG>8!;U{dK8pl6I@gl05bm<5t)vaX%I*XFxPH7y4Wx%sd z{?D>EOhc<;;{#=%?tp#^_CfvO3`L@Zv$yvKPU%&Z_ zWN+&EEj1S>5tD5RWb2}(q6n)IDyJY(BF0$`KIo3(;s~QOZSPM; zOfG{UZ!1DDr;Pb?&ePF>W3Kk}`Y1rf>yR|Bpzq()!k3V7B&&^&vIr4c_3fIq2u?qB za0pg#K7Az7lH_>Qpn6UT5mP7QfT+-{-jejlV2BbuCAOHR7fgc$FpVZKd?>Y z5*9KgmCNqJ3>8{9R0SeaR@`rn4;*(;u z*#PmndzY7>!*hd}-6E|xsLSx92&gFjj&s{CAu(6@38XFd?NOY#gK(Cl0b5d7x)t!C5EnS{EPgo-6wh=7$?ZVoqT+q&ugSw_n21ocY< zuK*w4xGr0CCe@iFD`qAte=0++lqCI>1@AKBRb`5EvhC;@?@%+~49Bw0BI?RVfKrpv zIgC<|8}*x-3b<6f4_jB>k4ty1yN{MJVO7ctoJe6p&rUb!N>4S9&5qC~9H%vV zPPY=sIJ%Y9))#G&wsUF~u6vYwT~jDfC;953HQ5q_0HJeACT;k{Yo75I_D{+Z$2y6C zAvT9slc1b?2FM+LBgD35P)=jOd#c2BYB(Qmn5<=}$PaF(5QS!f+Ib8~4?WT8yp8;F zpxHTI@V4gV3i$U#)5qonL{b$rEb?i}A5Kkfy;QEO#pX&)-MmcU#+7sRlzX0y?n6}= z%2fW|nT-48O%R#Rd=%@L%2T??AhUT7>Y6Ng8Z*N>;J`Wzh32a==2rU^oM#sxNoUlL zD@u&zbQ01xTc^{EW-#Ju$WR;LTk~w;Xbu9hw6rxsR*C3oMFizF$Ro6v#&pfcq|t!} z@W?7pU1BR9JgcedsiU+5+Ll%PMW3xDI`*v=uCrywGCrIIUuM6RFjW_{*>jHJ?TBUiVbM@D9_|kN4R&_QQC3&Yyfzv8=t)&)Tx>s7tjTh3| zxGEmjCf8=Fz+*k>6`4pGQuAjQX2A-p>huo1j2s}AokIr5AtwV;T$x^k%cPF7W{44C za3^3xa#<8WV+H0?Xrxs%p)oZ98rI=ennb7{6Pf3Z%I+Pj!L+M==%B1B&mf?cALZu# zK17~HtVKxY6S0ABOI%@XP+cMDiaMU-UKOFA<@Jb}d(30HqXk{QFdf~>Z)P$=6RfEd z$Zva(t&ZwoJEU!Eaqn~iXFbbNV^ls?oxLim-=j)h8H5I1|G$2xMK9REWm?& zzDMCkn&>o>3D1fw%Xn$xcHXIbv0IGjch8ati%Oy#IXb3>$7-a-wyMvQ*3;u4vK!wc zgJ!y1hfckm7W-tvu4XHD<4pyrf+dylv{D{RUA21?vj=T5PZ61FWnF-nkPwDro2>zD z2)80oG^UK}K~l`KWvM#(-rQaQ2ead^=#AGO*ws|?*v zw@g5`andlA^YyCH8(6VCP1wo8PG81BdefQ;1zT^u$;rI^z4oHw z^*L&+4YwJPt^T>uzCdNGxk}B!s)eSLgwvYWZPLp0(L-#=Pppiwg{(?1a;NidKd@_u zZ{NX;u*!}jhGYn)?FOW^nX{N!`PcDnE!(bUj=T%=y7Hc~EG*f*{n8}<|4nVWWW&nA4H^S(~CnbyeJf5Rf%Mm{u- z7CcoVb!RvZ>ezjDwE?k$*-CI{AuhmPc4jx`8qoAh??UTco#Qw<{x~}6qZ!JL8<7uj zv?N-yWF&(`w2vdn?^3i=W->cB#w}3FSMen;27b$P9bc~OrQP9Ps4)n*RiH;ICoR7?h987T)NlMcKAM$WVaT(#|#Wgm{V3#A{-`~=!w@n%mZC)Qi}hd0``-3ecd2JDj98KuJ)c^_bgUa)fz5Do zvjM9#8}f(7MK5Tj&!cTUh&Pm2NO@v=2k1eyOGw7~!^b<*4LuC}rEs=mEMxmIXb)pp zhU|}jO`+ooYL02E3nTRQ_%63vjehDwp#PAXv zB%sOr&^$I{*8ejA|fhr}3ubb)*r@jDZnjF!0=n=LnLzXP0RMRG}al=pfXR%ONseo(BKKfYc{q z6dUoYiA#3NNw>0c*&6_2khJ-wsvdeh1U&Za->16I-*YhQPk$+SiB)HFSv1n`T~QK3CoKzch6J(}&xZCx21Fw3f?} zHjKZ#z!r)v@T^44RT#ZvcfQG19S3pa5wi!^(|&ohbHRFo>2bB{LBpzPaWIY6sf~Wj zyTGmaiu+bOmh*JKl^2fuG*6T%72Y3UnhQ}d9hk1cTF<}sY}8rBIPbY$9NZ2Z(o6gG zOVzm7e&9LpO%qq~GMy|$VN4e$w&^#dY3!w8gsp!9`9b90?IX_p$L_UFF5{!fyr<@&Ak75C17Htgr}z;_e@ zM%x7(4vvO|Ma0A>#JfZ#M@Pk^!#KczAlfi4sfF60;@rgKxVXgsL$Q7Pe=)Yv32}U%Z!z zS4=-I5O(==5%HR-Vk8xajMsISQExitO?ZHmXM24Jl|8C&YpzJ78c2DDm1As?j}Z+$ zwit$!cgbn~KN(xQI%PQ@Z--V-wLT}+0bpAPdwN+AgSvw8Z%n+Q`Xs0`8sCEvbAv^> zceMiI)kuf6auWTcN>?a$CYYZ{ZZg^+>12-f>2N5aOP6#giQQT ziHdh5*F-3)uQ$dsy--n>apXlE7ognZQ$L)aj7!H{4mH1D${TP`Aap4B4`X|^Cr0$U z2ylsXo6jIvcVqm)O1ddeamYzmnd(z}i3~0cUh9TunU}jfl>1hC0!+@#(4A$-gLCrr;1@(I?tc>#>FDb7bk^Il4p~#s4}({<>40H;RUBq zEHnGcr7ojj2VcDuLXIS$e z#&%T)I&GKujq1p*ah(|mR?27s^+o1yxoyJ4A-5N@aU)%Y%V)5B4^@O?kgNM(3;Vb` zM;Lt?-H_2oU5B|Xa#+xL$Y{1V%w+&-LUNXrVFc8_Y;6fO`}?{gR$SG*2lq1hcjm47 z?us%I6uqNte=yu?Ktx(5zFzZ!plf3Goz{=8t}pk|R!bAieAPAI3IOl1kZ_ z8b@O!pGl5NvTf)G&yhdLIZsB6I%lzZ7RO2`sckDqVO{L09C7C2CYOVI@Q#hDqUbm|3=rsP#ZkoPQxVVXj506EJc%9g}Y^iCTCDm zQQ}d9VUgf97srqry<~WT zo=tSFJeNNppzL2KL2<=32y`r1NU>|n;f{9Yw@#$TB-8@Wxw1(;_LG&ls74zlgL_`}oR0H&xC7 z{!)}Sj*1p3`NP#o*1LHFUD>t>4tyON$w`8~wBDf&3D25r)~<;eqig)SV!7*PDE7rV zS=)<8^pYW3GIbP4RZSC@LSstl+15*NSxwVxQ6&(iuv$X=0PVbv_wNMIJYXWygStLl z1>=@!>?#@;V~y>XJs~MvG8@B<5s(gqHZOSMsc&nR&GtrgeT-q(8(As15WeA%{jDK! ztf$A?G{T-8)Ry5@M)an}uD;Q@IaZXpZ^MlQUSBK&OGMooNT4naFHt@;XU)~xj9q*_ zi*_Y2^N$pq{3sMuOHT~_aA_e2O~~Re6M>^lEJ=#2Q!2BvO^8{uszS=~potLBd5{)x zB+>L3Omu7|A_=5{O`ocGq@MU1ow;qf6M4bT2qR_FccD2tQKmD)FUx#h7T=DVABL-OD+?p1A1#48?24gzNqqq~L`hmEJ?z z%!G8Ry!dB0{R7Ts=-(f)5tK^FXbndL_$#IjdOoAy=M+d`tc<}{N*ufKZp#|N zd+!9tI6rSpfa%+kijLh;tIvV50wEWcZycx2*i^lX<_P9Qj&G+T0;<=Jr%};kDcuBF zv+%4%a(y?U$IRS>4%m?-1U0*paw{HUn7ONs8`ZT-fvN=TJW+5@oo}@>OL&?+6McQg z&j-X<^r}N7K~mUa$Ps#|%7kO6+iX#69Hj(qrlVA7RH@g}7K}9)a-bK|Xs5=~l_gwR zmyuP=s)>kxbq9G_!L`~9k9qeSb7EV5+gO9esQwEUlfTa!|2467Ig2YZ8rfXPh9~z? zJYkcvq9Eej4#-PR5vqu!-gLfz7|vqKns*DCVl@b@I(`k0CcuMw$>jkZkKLK2MK{(T z)7zB3znyNEP!Bty_k(=)dM3j;FV|TIV##>b&|dcuRXgK9l4Y5lKlMu={pz5sOLx^I zWF`Cem=#vL56joNzH@qA_Zp-(EMB!vp2^+AE}Y>}YBWMN&}h_IrAJ&z%tpEAJ&WV$ zW~i#;TLHV6fTwa_LSh^y^(DN(vdJ^0=WGI#JpyFeP0rba5BLF#nKBtm9M@4S1^e^Wj4=k$+=Sz7=LVb)^Bp!es5sflk%EhLYI zwVOEQr~dcj?|JSnOJDU_g*!F1Vb+)~9s}N0`!|~xdH=5G6FjGGC3f&|Ha)=clQX3} z=*@RUPO;BOt<0{PFlVK*ut-D=PP4G5MY70$v{|njrVCGYQt0h@F>yVE zp*n7{TMh>cj4WtEdhN=+vTa)B>{ZN#=}M9bsHWniYe%Sc{~acz;7N4~g%v>nyo5@h5O1@cm4!gD~Lj*|RD*To9m0vvgt7IimD z88J%K2PDaF^tBlw_8u0Gapg-oWcDm&s`x0`zfJ|#6Y#i|2m;U-Z0Z2ZLQ(G^3KM*l zB?5{T5&ap6(&4uhEw%ykO*i}wq*BH7?BX*b-Vm9wsJ#zq z`%7XmiITCWP)6MZwQwa{8y(}$1V%ljyrqCe)I3g>ghtd!c84Ngd>Qr1L`|t_U?PZ> zBuPBrAf91Tc7d8vNjEpa)k<$|M7Br&>Tu8dn*aG$wQS3MCJa zi5ZSx)Q=ZrCKdUSH{dP`nv9$TRat?L@lTcdcZV2lNpJ2)k}#^7z3dsym+UT(i9>`2 zCkdvR4DY{oMQ;s+j>uD|Cd{qiE|gMRSSucJ(mfB$pRI_=rf>>waw0D)WF8CB-%?de ziQbfApQXy-N^!e~g{LJ6J}!T${DpUWX9KZ0!S3D+7ecXmVYFFi#y4-}b$Fzh5IiV<3u;ixh# zrq7FcZ|kYS`%*tN_?U6x*7i7BXc=4axZpQrVvzapOC=XFQdO1=e#lWbpQf6usP4Hk z6b93x5%b%w87%4*(K9*x8wi-=kVH$t&9mj27^a6=QhI??_yr1O9g^8G&Cb3HP3tI< zaT8hJE5zecJSonEACjlSPk(>Vycm=@5{nGXjD>e&)JaX5UX1t|oX>p&RUic`iZcN{ zmu1@Ffc$I7FaR`RKTfb`?0Z`VGepC-OP4{v+{@0RPhHR-m`gTXV)ua^PPz=XR29XI zkyL=m(u_aLgd-nE&wEf+UoUu0Q##R%ct5#D+WFR>J58TPAleqUq0z48)N6dzMJ(**d=kuc(>ypBZG25Dh8X!rW8 zm9&aGI%X%(-CmOJUy>vb62mg!C+opw-+dL(OaITEF)2+eo(Oj&A3% z>^IVA{6lNcleA{e){h?=NuRCS2>IJ)WQ;Evy5*YCyr!dF?g&j(P@P4RUH zs0{1y`At~T2l?93RJp`e+Pl&yJ($kE!y69q#fY!i<#J{Fl&<#p@LNP2TT(oJS9~xM z6*m89XziG0SxNWJhS87ot>X=*_li#jDrPZH--b5|b*&Zm+7D~<+O>Ee7*L{9xTOt! z@giFc_p*#N;U?qXz51S^+v8k(MVfRifEfsm z@lQVYG(;a%nYW<}vniIfve$t$f3OXS_65ClCe(~KbfZmJtD$eX7e3aOQ2wCN9jn=z zSl|k@f9viFl5K!9SXUfT2zWn1*Ham~@yE(JIqU z!;i=V7GGniAd@{~!c&l`o&|l};}5PKR}A1efMPcPS3+kXI_}-;U`^0y-P_C+8Qx|U zYqdbmpb37#WHb6o|KxgCX~1CYzyRjf0Er=VE_ADrcdZ{MW*k83_BQ1Q(>|N(KaA97 zHB_EHJcjyG8j?&*9?C&WB{RU9V^-xbdmXUA7)xQ@5l&O3Vo}0LrM|GHC&Y8Hu<#E3 zHT8+|`?01YOKP{MFo~vtiUV`9=qj&I4q2dl6_qB@!1u+N43SUGC`10tYbVIYv(&AB zNd_A}cTctiAYU;v!t)w8YgnXXwL?D((KYw$t`}QA-q9wX^slB=j#{SrRZd7T|3gw8 zn5L$?U!@q(1%M#r&|I_Cu2p@ftGZu9CW6qgQ6WBcHZ>-Uafz&C8lQ1F zy+;@(c#z{rNV0r0PrAsfN_L)JRx5~fbfpW<80+ECNa9d-P`be@*fh+XoP>S5^dTJh zL+G2?td)nt#l z6=g-CeRNN9cIUIh5qT*|QlEh?A!G_P9&qW<%Gc>{Ao!-FaO8hwW4L%Ej_BHz7%vP& zA-7auvY;&jwx|kd9c_v$>XxSd60DSG)#@cpX)u!%={qDP7VpAI!9o5t={b}EEd3d!mI2HPbX2Ahauab2t@j_v2t8Hp+~C4&&dljsBU>k) zsoXggKn?pD2|q4;IF!|~5&8F4?lFr^uk+KjI~TPOZ}3mlS$HmGAeEv<3=QaW+?Ugp zZl4zUXNV3Jwc;dgt zOZ+p;&~+1CrcT!06I;>Q!F^=5flrK2Xv)~}S;jGaQrx8o@`@)^k2XrnQs7t%k-7sm zqdobA@yr9WECf5`94;1G?%iKRw<@svVqEJ($m+cAiZVENF>VD#FWGGJR6%-n+1uaw zj5Eia6h!{jMb9`S$?rCxtOYdj$|s2yz|cxu?$ImXczu5?xjvz$7!$ix0bhj~7B}te zQq6}}C$Z8R)+H}rONafF3i~Cfgi)!tDHp#X??1$bIl@Ms$|VUY3rUWU`(9zAa0jY# zwtD8YN-i;cky>kqrm!-;jM;Gfx%p2@QLhgBmLlYzS*ogt;|A8= zu#IRwm#+z-(7xc$tBw-t48%z^!w;s#&7dkQO-WfLBrVpAF%gD|LEqtj{oMcQ*tW?~*z32Zj8mSa z6yw`wT&3;uMdQN}8jf2MrE43hS5SQ}kbhb;!?zlp4riZ0+_QjJAJzWC;$=n?LGU|` z2Am!!x9>!EX0^^rr-HnmxNJk98C8_9vh%-*tyQI^ot7m+3i~t%AjkJ7<>*h~p=Y?R z;mpDP&ukEeyOFc?F1Mk@7g!4od2iN~bhG94I}Jsz8E~BJPm5y%g_xCe8)c6@q*(c9 z&h@BFf8(Px@6#IIwL*xT9Tx3Fz^P?`L0Q#VRZZL3S+&W+9Wf*AN69Q`N*9j~@z;yp ziZ!FFOu@JYTQ?hq3pz427PAUS@l7FmQBj$lhbsj10RY>? z_I#}YVr%nCe2#=C_<50T^ufQcA}fe>t=K> zlE(Pl3O)^rZ57Zr$i1H#9qm2{)%+mSE@VrgL@eoM&m3)@(HMaPrSgszh?**`Ax*aK z;Jy5>c+7Y4ZeHuGfTppip5|W4bXN3dOqpij^rcpq!*2%}E{R0r7K*`*Ncxd4&j_VQ zoPP@xu*OZ(W||#3tMgt1GHYA>FHaH_Fv~yvYzsRsA>&t2nf3Sl%a<8{0>Ec{>RVl-&kKH<$`hs+MX+tW5TDW zwqf@T?H0JMpI>v~RtHMJ;j1InsF?}bWy}yQFC(jT(2zOAd{O1}si)CchA(zF23Q25QG)2IaA7hhAjCRHzCoT<=Ka2&Ta33IW z4*QH=#*QZBM89{kaPk2PAQbHc1X_;H;s1KadJY#&a6WpsPBPAJqEqF0$kAu~3EONo zg)YGiz$vJIigzia%_W{@%9>5#b_nH31Z2GjR}|41xyO_d$ezTw^wvr{3{{cSvL10U z48viZu$d|xUALmshV=Xcl@Zfo98t&~&|{J#ltCK_?EHB=DWM@w$-wQ~%os*f#uCT` zhz*Y6sN|4)G_rRD@j7(I!Hi8-VIe82#n@;gf*O1zYsAADpNFPT^otmVlSviZFsD*v z*k=@2TDY5!;DH?q4Q=cz6a-0cS zPgS3*flGx+RjBeKkqVh8%Puv+DKfdE+VH@o{NEhaXB%fgQy;A&P-+a*&wXCmaY zP&&R?I7Sq~Rd`fo)M(y_vu~^DiFtSb4Q#c!nVTHLTrceiyCEwQ20)}B3oTvlNx5-J zzf_{722@>t0GH4Peiel1qGdN8>19ZpOEF?{w$H}i238C9wofU8-@xV z7uX@;zZQa%LJ9Xcfeu~YYvRIB4rs{xPhy+CCti$#%KUvi<#Z;wJzV(O#};0M)DCx^ z2g3$tqCs*Gm2-Z>qJ?TXD~=N>p<}l+!X27{i}{Km2LI2Tc#G~7LuM%B_fFltP&&o& zH^O<-3)Yg^*Ex+glCYA#W*QY63(7Bx`3-b!ayqTRR`I_ta0WZah}XnRD{#iv939Kx_|I8yHbfc8 z*b^`$p2yJnrIF>{HRnfZSJd6ILVxFAsE%cNXvF&=-|;^Fj%>yGqn(h*n@=OFhlwxx zTF#5%<^*U&Ab{S`9I}m8>f6UGrW!O-R!X`Z`O%bT=X{J>0S;oNBx7)X^i3U2sG3A+ z>n@0UJynbM3lW{nlv$DvY7SZz*sI7Sd|ZSq9r`TPo?h0v#`i&D=!+_ej1D4h&uw4# z*FFzuL2JDZsj>ad)THAgpjFTaWpqy6Kjx$ z{)=!nJ$kL1<2WLkZ##qS&PAtS`^&yC)4ZfR`?8`q(==v7-KC^%%2R0W{J}K0d7>S6 z?*5SPR{U_8Iu^$_C5G0^!| zxS?!j5CW^_PyjCL6$h2uwf~A4K~4u@#VXe~^`N*6mU5!VIWzy`U7Eibp{G z6$Uq>ugD&&cbF&m-Ov3L{gIkf!p5pf&9rz2Y%USXU(6VHLl3~_Y)b}*7LmRTc+8xT zX%KPEPyuX#LO-a$jgnX<5b&A+K>8$hFCzw#6M$I^o+^&P#^s9RihXe9-i!!oN(d*x zrbOJOh?NX1+!t^75QCtXfaHsY*~Fg4<==)Cf<{DR+|0YUz#`p5%_$eu#?NZD7&OpC zLqA07Ey)|M8KhM~MGGfTfX%*>M2o*bzz6}|y&DHW0@#9}%Uv3rio|+Kyj*OkzRC^p z4=qvOWa4Gl=q$Gcxy%#=TQrnsAwM6hEZjgzo3PxBFpxOTWklrJ7Al6(mOqK^fHJywF}!L12CFnyx$>*@c!R;a94+9 zSUg2*9`JpAK0_hyiq9Dd#FD%>{1G<{TV+|1$Rx=f*_h(u%s5zoaP%Y9L<^;JqRm5k_wXHy zvx#XlXP$H7*U?8IX!vE?6N_BD*YUWcIJ1vJ-@}M)2);Zr>M1U8vok=yUC=N=o6ygs zFuhaq#?u3iW&J-bNq1>_W(_Y_p3W(a34ELl9!>;|a80$(nmE zz+#FBqEG2Lpp~6Bwq|h1XhXexrXVMwUF8=^H6fnzOncW|T85)GdBupA6^g2xG+!0Y zl=+po*vs=JT=yIF9@!SRc_*b9upUd^53!8`O5nB4b<9(+yGoQ$V*sg^51nX38E!Fz zQnpJr%a+=B>BwgwX?Z#zzl@Gq>X#Tb!jKesDF*4#ISsb3&jBq+qjV$*De~@F0l<+` zCoeMuZ=szlr?kaVr!aF*3m(xlv(Dl#s8w2D5oIhrbZk8;(xsr989aKhrfMe%z?D!P zjx)?eCS-dnNjE+ot1dPdCT(|Z_uj(zuM#l=8zprwRhkUUI;Ir-SeE~71f7(2X(iv_XUn4rH-R|ITi=? z9~dyZlT4z3`VCnPsOQ$TRrx+%dCz(dLfW=whfa*)E+p-xRa){U_rf47*X$Kd$FoWv zelvMl*DYL2u{O(?0>_;ak)LJ4xQ`CV-kNODX5}p+o}N%MBv=oSw5(8C8KZxr^0Fn7 zwt|wEIT<~WT}EU%QqTv%_|Kpm>jwpKp(?njf}9ptwR0}C*H#l|PjS;i{If=|m9o@< zK^f9lQ6}}0FoTIKix{gfP&~)xed6h1*EMOoE**4;w8JQY9m|nsFTUf}i`poniH4L) zOoOc;l`$P@d*6f_bhLKHPGRlN7zbbKG!>YB~!{5}TWgv03G&A>9;j_%(8POseb9vR* z9@F;XY}2()wPan<5~Fk3LNRwJG38ygES6{%HGnp6Y-(YzT74eSoW0X@Ku{otA)T6Q!zULoSy7Icd_q zbt5nbO5eC4?Vj-&Q^$C#L0L7M8yzawo6b6nlOIT_9d2=2Iiu18&CKa?6hCw?_zORP~^&rdjO|OM%R9b*pJKoSS zm{#ZdC2ulYljz9F8`a}0z58zjDRxqAC!T;#UXPR0n37~yJ2eT@1D#ylszx3IjeFW8 zF@#(10)uJ>?Gs(gqtU6Yso(H6Dj+!-QyP?cRY3>e%LB*PC9Kot>59~8h@Cb8nCv$b z@uin);HpX*d?@j7v8DI8-ja%Fqv|DzFD^L04m`W%ft*oY>%BUgcK+#b*jF7c(Ww~w zjm37T!BwKt(M;HuAZ+$HSf$J$BHgxOX382p^ShpKW=+*+&yId-Wj&lQ2DagMc5m!W z1XFy6Kl{9B@FOvCvWHsd7nu;(iC}21s3z_Jw@`3+7+Yi9SBO8(Joa@Z);hza^!x5R z9fN+iEIbLIsQ`2W5xkr%5wv|1RiC=GMvgdLeg$ATna(~vrVt0NXp|EN`JgtNIdy7e zqgzCR3yCbKq8cN!-HI}Oc9-+RS)8m1`z45u;j%n(bLwoe>Q4wAeAniv=NcW(j+exOQ=lD-E9@h`!T@nvFV% zyb@dL9bC2$#9ACz|=yRecOpbA8 z_EF_I{p#7HylMOFC-elQwvn)Xk~njc`0qr%4=>pXs}utz^V{j53v+lza*1cH-PDfc zZ_vt3VwgyleqCf1Ak0)H^4NoKE& zc<=~zl&%`Oa^~{YZ?ONkQ=Yx!ptx6I zzgH8z*ZF!6gx=|^-mBX(wI*Dm#!2FE26>t9HvZ!iUr1ts?13a%JwvFi^7(Be{LA}^ zt@o4Qug{0a&+#s>vczd0HgVepo<1~lVt#o*VMO~2rqOrbm(JW>tlQ@}jlSBu(KdO$ zn~qrB&ap$6Ej7j`DtRuedqN!*EBfA0G5h=*=Ak4Azi6nDsQ0!pf2a5t^6t0SUn+N6 zJI`{iFFE-yUY{Caqa)kF zf}CN&uo3T_+VGgL_~-~QI665pJvK2aJu)jVCnCD&T{@PLot0kXY+n;q(@Iv`Nk$;oLUXqH&-?R*9 zccmDOClPnUJLN1HPs9?`)^k^xR{+?XP**XG7*?c3x|FfBvchDBMFjmjBi`BK%Tc-r zUVv7HF&6X3uH=SPp_OZMX`qY$tg5vouI`)Q5!P=xot0AmRyD4lEKl6du|FE~{BW66 z)3s1G9d&7_kvjl0%R=X)yPJF6#$-I@bIn#2L8F+b)Y(*@=0u?5+MJ1cJe$q>%K(S+ zveR$VuS&1zcY}0cj~eq|L?G{lsY*SzlU+xJ^ZcI1tg&BO#YLKOGKEUsi*X8%8pJ2B zProa>B}5MX*GzBsh1J| z)CjNhdtH++IRciNpIc!&cKFp3%F+bt03f4p=&3ULR9N4J5O6iHVAH8RY8!QkH3FbO z8GyYMFy0`9w)L0e4?_iu)`k>5Wb z_j3M9%i_G8&^=qmLhKg=t^5ElUP!LhdRmyO>58rvpOt!wOsJX*u4$=aimxTWm@7oC zvcjpd%Fi%v;;ibt)sLyhXyMGRrfufstghomzq-Et-^O{vpn@Q}FWblg#aC7&nYCs` zx(?(h*dbwWYE#%VQk?3>@(cY+_4J0`*yzI5z?*?;@&C(!)sI7oyNpeNz|B*b8=Do8?O zEh+f$Qeae@X5+>!(=0^`G7;F}Ti4IPz+DqJ_d8b?sF}vmKG|N%ORRy4Q=ZkC`}pB0 zSun_LWOE1=(vjhx#a@Y2M)VlaucY*nte<~Wh_=Y+vljc|lBan5i*(;N={Wbtw-c76 zA~aw1Ow6vo*C92~e?L-~)QTOczou1jR$GR$P{-RD!7K#D=MgyvMB!w8nK+;hQ<@;& z=%CCKI;?ne8TEOU$ju^)VA$lbAbUR%=@vK{L|^xi|9l&Jzfm|;GOZ!ZN|+HFpuDbfk9HNRO_d$`RkSfW4bd3m zC|A;TjFl1%$!f(YU(-6?VYeD1w--gss4_(olkt{#FB=k$W ziSKRdaxUW))>eYkSnT+%djWLDVTRkQH#EX{kuI()XBd+;IF3j%E{7w=?7b;H z0_q%ag*&}D+?4Kn~gQ!{Qk4c}vCL z7K=zFv7*S~=Q8XrHqAW!c}iSbBhtIR91woj8o)CzOXUMO&iDf1*BLQPph?JkjA6N;mB zawdkDgeV@!YJ?!=Q)^h2^m4;})CA2HnM?SYLOmR8reP5k26!oADtq(~UC|S>3FY83+%bqx4a=Q)V1Sh zN12FUtWgw9E;CJS>F~;Z6f4WXtr=em`d_o?L1e{LK`gm2gY$6*PIqNvkt3$942c6`{E?tr8L@7FngJkc1EB>4Bbi5r)SI9h^T0+=KvW(m&H7IB*u@{trD+= zdCP-pOT2r*#KN_XX+Q?47(kPk!VR2(MuI#J)-UE532{6KFTlz?QaH?Uq!SJwVANI| zACyNUP?8SHQP?dYD(X|_HH!X+Bvk&=%7fFo?PHG;CztNMkY0CJ#{KtHCB6%Gyb-6G zF}l8kpw#l)^kiy4vaL#XR%h5x_JDQhf&O1H3`LL(6;NhV)v^#%Z|lk)kEh7`{!WLf;Nlp--#M z-|pM<|DJ^Zc-@Eods)o?H*BN*`cUNvy*jVs(?AIk!%uXPV4^Y%yP`-?6ZI>{tWBWh zJf;~P`PlI2wLPl4wi591$xB}yN;8T`oVSY6-G;;{!Rn*VN^b-DxB96Y2}A}L7zyVK z1EKZN4)Nm+an}y%3t~xL4LL({?|1Pq)$%yeMs}J*#&FelzCu^*0N#(HJAi{_*K}nW z>0({DB5y*^=R!xdF?5l^CN`l&Yi=1F;GI?@b%=EHlC;@c*tiSwWGS5m#Ld|z)KCte zK{KrS2FWgnPIfIkhQkwqQ~g*vGz?-K0gQ;uFpl<#h5!rmr@IfU_ z#{K02(!F~*2ZMna4bC%D`uI^aW$ zdPk(bdX(xwly`e{Atbu1BkBM-u4s*jyn?7gAnvjvrp6~Gz}vg|B#yN(+VD7@yhCv* zBEIA)c${D05*Ys>B@o@0tK?24meMAs+$N4cF=WMuNIsF+H<4sL@%`)P=1J4jPvt(y zNUxW)`yl9WNHN!LpQb}Q-He|hgWP_F*OuU)LYa&p7o>pWP`{I$k(OKylbqrHwI>Bd zg4(^04=pMIKe|AK5kg>jm9lk9By)q(WASDB>HEA5#>q;m$*FF;hSHE_Y)`+8-CA_K z8d!fIEpQ}&>ljMpjto{N0?XQjZ^MK~smFQPqNqafy?w3R#yuh70a_8^Ai+3J*$l_^ zGy_fys9d})V+K86W?E&2g^w8PqXNexWk5%$GUNN>vS$gvvrI3m!Z)ibGplmlcU>UE zUk^W;KnZT0=l~LZ`xFfu85KPr-$R%u3j94}K~EijdUvIA4Hq2WJW|_40)@g) zzJ<75(86YvB%-rIn)RH>b#OY0?xPxx3nHd~bPBu;+DX5nKqm@qW-_8JZKpLF8)HyL z7#fFdvQ&{KZIyi>SXA~IOH>H0F~L6kiXwkqkar^aRI|hgNPL6>{ECtXM8yVlp@E*T zeAh~i6w0ul1OB~+Er!$oeG$$ z3Kg77{Hn^bsw6wA($6YOC#tevs!X$p%z@Q7+|l5M_~Fc`d-;N%s+fx>zuUEdpkQcr z3vvv^F6S!KJ0+uecz82r} z?3b~g$K+Zw3aF_EE|Ca<;=c#y-w83xWzo!!;YuBJjeU%HK%39P**iEWe5BN6x1KQ|IDZSwyoyO zE}%tiO&4s{L2K5IZq+iV<|?l>t~RR>La%#iTt^85cV&7#RZJhJE$oyP0H9b%Xj#c! znK2|;HbRxb#2$7`?R9S94$oy)9hoNj;a=zEOOzd8=JtfMN*8~zW}ya8RFG9fdxBpv zZb=8CM>|4GNA-GV&1q-BYiAK@SCuV>WOU1Er?qlY{9B|CR6a^6J6=_>NGZDh3AJVA ztZTWeIubTp;XI$up!w;kb=aUMu^e2rS-0FF@W{{_U|U;Tou8VSe?5`E;@`Z)*z)w8 zP2>~3$J8@(+6=JIX4G_9LutU?P`c1CWQxuY_MPvlZz2*v75x6h{QbrL`}5?F z*Ufjb`45YydawNk*|DNk-dRB@M!*5VLuiY2*uZc_ssvXl z5oDm#1cjPq5KAeBcB{#gIC!f)js5jg`U*;=|De8qtG(Qi0NDU<&yb#d&lkfM9QUTa z17HGxZoOX-;$L*-(gfM8&^j61R`VQG<7- zs5x$Qyph&!a89^U`7n>BbJJd2RaGlA)*#g8K~`;QFxFHx<{LBS?JyS5Gxm)PA_got zevRJSP+UbFrhJ2pf3Dq$hQuk2TcVA}28<`>jL%GtpAzNFGR5cVj^~k$cd1R3zYSIO zOoYv=A8pbKc)4yLp#M5%+C=7A))MUD7le7{BiA;?i6(2^g7RyYX*e2e_%|`2K`nW0iV24UUf{zQq*4`RG#nlboX8%piKKAtBFZQz-%(42<@m=&(=0KS3CsNcWutet2 zMKbG76+I$aqHX86(3)$u=0E-9e*oqd_vO*g0BQj&r2o;~5Gblt)KH=& za*3~mGaF&}ClL!wW47PhI$fdP?n;rh=>5{LdTZjDrsT`!($e4o1n^N3(|&w@KepD- zvx%7b_)89J;$nq#Ke*}1?)21zs|tm>Y?Qlfdspm!LRW00IJIBq*I&9la=Ws~R^4(} z{r+MdxquhT=lWp?)|WnhHUH?1%N=uvn+#?*knz55!YaAvKnC-guC5(w$$1ZPS0v)z z;&@pmBf-;sG9Fr2=UxP+xnVEk!+Y`tTTIK{lCiB2PJG~t?ANo_TRZOer__}z=p8oK znFgC$*zQiF!}i>#7moI^v=_ehllC8H zh2fr?$WO+;y-mly;m3*HU8cV!tkC6!OpWPFF_oraGDA1g%0yKWSN;iic7bn9(2@YZ z>mA)eRk06hUu#uX8C<^_uKh)jyBDE6#-eAn_((KjzNfx$o`53`Kh;`Juv!jT@g5a= zn?+Y_qM6xqnIXp`Kc=hu>GAE@clzT&GkN;Uh=-fS;34p&MLBDU1VGE@=FZf6h*37; z7yVc7$;H5ei_(q2mFOPXDFov~A{eTSIQ^hgo-7vP;@T6U&wp_GI-sg}H@fX|kk~Vf zW3*DoO~n%Hn~Q{3x`wG{u{{DK^W9<0=M(f|@XN)p@x`#p=w|Tu8TI#j{+f=%_h`kv z$DwBDP8MXQlx(?KmWb6&r&_yB_gut#-@+KngUs14W0>{+MjY>_hf{0Gj0=~_Pd3x^eFpv?p+c4R-S!+RXO!0WU3kpjq6H{|~1 znusDTy1mPngh^HTSJN`E&2|E`KT&vvWIg2q7mk<0(6y_RGG~im#-e zY_!O}HlcX%WP7l1?r~-#nS5SudxXljSbHf;u^aa+$$c~%O(ZKBG3i`aE=@nj%3MFX zL)Iwak^BAoa%UQN^su$}6=^w2us476Z8d{(=7l&Rjnunzu0Yo?+xdjFEie``?Q2Q# zQY89P^7X|;u_+)=+Ua!k%EHWz%!5cO*?@2WI4Bfx#V?YO%A7ISt!6?XZtW5OZ!PkFB z-~T1#;D8)8P;8rX5;+S6YWljp|IOeRpa+VJM{g=ZVNl<_W3bL(a7btvC?qT*Dmntz zPCGF%K0H1qGd(UmD>g(sFBO^=nU$ZOkd{}Fs_j%?4=!qs(6WmuflVptsq5z9_9l1tkB(1H&(1F{&z$zIIzw){?ysDj z?ZRI}A0w|&mlspww+sD?b#@mCDZhRRek17S02^s^`8Zwuh4$-1gg3oz2IyM3>mpQK~j$iJ>lK9<~vCMT_)`1U!LF`-5`LUnNT- zUug&Z%I));MC6iuPp*P{XK0VDb! zRrfY(RpNO5J;=dj`uTKtKE{*#jX~#amw}RTae#2gyaM7~5L|mAs}wT3_MwnE_%8$6 zmn1=?UB4sGlihI2Fvh(I+O!iKFKJ7#qHXsGlA1kx8NY_Tm(c-cO!q0hfdt!;oH~Qx z01u5K-@D%@W)R;tH&y^+D2Wp~hf9;7_)jjuIl>SgH-+Bo^f28d3~-cT;WZd73?=)B zI7zuUheIf9A1u8Cj2&M!X7(Hyk5X|%8QrH!$tjJ^_M+a?)iypXBGB{BE1eIKWlJt| z=lv^dE8%MH`Lem8j7rtkh6D`}e@!JR zjB=4H`>gAt?fiH5Rait>S5c#Ng*jQ3I5WP5+Ra2+QV=RQak!#!xHxW_=F%U_3b&ti zQm&hvRy_f>AsK^I^V+Ze(_$V8VF(GBRK0`~z+n;7BbEI^qNrL!r@G5hk`RA>1WZbuyMl+IFpAcNm4?jYe808-v)zBnygpyHS7oa z)AYma_gghFWTI?0?KtCz~uWUV8-HjZzb6 z{IA|MSfn}JKRZ9DaZauL>qDCqWL{~CkMAA z0vK%#w;X9#*$qc-nRR-&`YF+VEgqib*7iK+3^fVt{dh6vZ-C}55FUqGH!Trg1tBXH zz$tsQ)U`^I>g&T*H1X9oR1TQvRgwKgIO%8V$3S&r&V-+BZx2$Q23`eiZknW3_bN|X zxteRjl<^_grCvEoaBlB235e_OV%gS%KKz!ckff4O^p<<1OUQ9Ihj6LJ0H;Z7gev8sR@p>4IMXCP^>i#(( zK_$kB^ar1XOs}g3sq1jJHYntYg0i$!M4n~$69+tb7Ry7EJfo`JnEtQE;Z;8Vxq6&8 zebo^OQ^6reocLnlPk7<3ybmSl*3};J>p}B?q{2CoNKR-mn>TUM$<@4lK{Cyqd(XGF6iDPp5Tz9Ebu6=QCgD~q@r3iQ?BzN!Pu@ktEMxj*zV%VhQ zE?KE9X8_LJJGLh?#e=Aa_{Qmb&g54~34!crA!m!M64{eAo(AFtCf*%17(68})s)dC zx|^+$MF%RCU!7If8pWF@zuRLqMjL&)V17P)l$E@_XDUbiAMcn3uUh`Er!ff!85AF} zn*0V5T|oCelemw1=(|TtzI>iXr4GZXpX z@*om)iMOSF0ZS&xldV=FDMCZt(g@Bw2Pp?es-n+CVf@^d++gK5adw# z0r3n1TjiF{U52eXkHDsO{wMBO=~!r%*f7T^Tvde@a}w#4FFO^dz_EGvtN}>v9uiUC zrhhWj7Ftl*n&cP1k4(TRuNVIySDM_CC5j2ZSRY*^o}x9k|CEFk;ym&A7xEP;kYQNDH z)w9^9rNb3kbl3CpUa|Go;$*S5fhd{QZ0I{?0J$;JXomXcJ0riMqY zIZsUm(YMGmKBs8LbA!Kokw-ZUy##|iK{xE~IjdBt;q%{;iCSTPu)71(I;BDN;O>vo8 zO&^7Egx#6TDPLGNZ=SL!)NUouJodc?D(P@u@mXb;-LHvUcK6NJo-alWKW&U7RO$IG zI(g?Pc2405acASk)5A!AgHCyv@fS(JM#K0iFmEA^j-P6{cL_vk0Iv~hp(?K%n|+^$ z2hX_wDPq*zKhym~k7Yk5u=9f@T4LBU*v|B{^SdiamH%0t^F6%QZw}ALcT`R<8g{fd z5zR3MjF;}C4eo<~)U@|%45E=nFSA3Ob@kNlb zmL^xAJKqc(C#avd4^cnDMCJPspPpoh2_DLo6dPd*$vrK<4LmbbXrQ&EWE(s)2tM$~ zvJ#agM~awW0lYFh5>zl#8UR5Shc28RC-o4nPWR)CkoIC5IcQ2V~IIq(*$3iEj1NPBytS^SzS;{^_CW5`1UG#6uXn-HdWg(G-l z2ghi|BU~!fQU4EnZxIz|z;0^-K?-+=1a}MW!9BPK2o~JkLZEPWD_jby3U>)11Pd;~ z-CcqO3sB8}emUpf9(4EY4ti3|3?VX9P&TBWY4hA? z#3NDNpAl4wF`LX@dBa{RK)CVCoPWUl)n>^x*cYV#CGN&xq}bxQ+lZ6R>}?0`JOZ0A z%vIjLSRtUS8h}B@;(3Ng!paOU&8>4H*rJJM21gs1zS6ddu?43afJ4#br^;#NqMRM^ zofCdCFYdXw>K9Zblg{>&)5er}h*m(tir-t{i>dhbzpQ$gDCgYiT0%?Zw_Ls$+sk-3 z-DJ9^))DsoEJa}Ec#rOC;^&|tV~HzNtag#_)d9^@;ZH*&w%Q|>%wj!3wUMa+dun0+ zm?PmJ^-A0*L%4(y&%G`zaoaT{t0JOCKEl8J^l>heoNNx-BU<&lCy@NX{`toHD%tJ5 zr9Gzu@%Kq*2y|ZG~QAdsUo|R@K z2P++HjKt~hl;Z%C2U-K%om9B;d!&B{Dyc2ZDQhw^x7BaH+}X?B459*l6~{7mOLQ4Q z`)#;}=tDi@uZTbcBAlQ0mh<)HTER&$g62(I;hy>`+6MJj7cpbkzk<5U<-v4b0wvZB4xXexSt2dn`)(?0 zZJ9e%n)>mWxzRq&(|U%axSNP*CuU&T8-5uu;Qm>pr?veVHw-FG+3G<_{KgKunm3VO zhn?F{b4^foA=Dg)zNDYFrVXECb}DAZ@}qa9T~dyz+}{s6cPRiX;f1#?hZrV^(~#aM z0bsA7bH9A@L8YcAT-ez9{T#o=XR%h;*AAs0<@R=&>zdBJmy&)AyqD8isU_O)e!RWi zcjEqA*vq3jiidf-)OLPUh;fMZWkvx*yZKQ&dxX8?!HtI%jLVp&{z+Cgtl`5*NnuNl z0Zp2LZ#q*KQAJf?*8wGVmB(v!KE>G5a5*9)8DaI0{2gAs@0E55-jJECrZXBfe*SL7 z=dmr|9oOcX+cw|Mwl0J%01;}x`_BH$P%d3qozMvHgbJREdEF!{#24Ss15CRrnn&il zb8L+!F2z89zfhL(4mqSLyNida_o6NmM~{d=mg`A5tK~|3$y@lApJxJGn8aceA{TG)oXt++;Lh7vH3@wMDL z*%|r#S6ZmO;v{8qv9ezXNODoofHESQ{k(=Fmz|4*p%X^kixtg*d85PP+@Q?Pv;dOis$ra{+m@a7$%%>CjN!u|R7T2|jWEK0X!lfrspDnq4J2 zMqy2Bv0a%jKhlInpwd(E5@FXIq5bh^%dAWEqm5sMq{Kxyb$Kh$2A~XCfqY3bU*9?O zI^JW!vBe2<&p7OE{B-VpeWJk?z`_!$jTcTRi$?4O7!aW}{UWxm3%dqLjC##v_r2fPHpH_QNa#zWUs#Ebmu#le zcJC8;|0(Vy{l_-{>>|HhDQpB>=fF$L05g%0kClv`k-HUBGM?cRi4D6>E2)#%7*Z3& z4f`^w=}hH8lfytZ@X18_#8l~tH=IoTQB$ISwl@+giDC^vO*71wD)c52ULcmpC78l( zq^38M6<=ANkH+?C-y$9tSB7l%q$ST05=4^<4Hc(C}q!{?qiN>JJca4wLtqGy9JP-B0u+`CRxGFImif`*?dY1 zfIQ$@;>#KmIQpiXK2%6IN`)YQ*OF(>mgbHOI#6#E91Y^ukwO{hPr2RW%E zc9)7ezqrkVwu~yRjJkmbT{yj#gE)K9jGT0Y`2&?y#rwtvgd(y0!YX#sz@v;nd&C|* ze~>*5n_c1_`>!s%z*zL$W^>aYcEiQq^kNQ+O9YqS>d50dn>NkA{KYp#C*VtgHy)9+ ze=qngMcEuy%mo$l8=!1Ah56~l`Hg*`{l*0A@$o~abiEpI%KYguYetizXjUKPoFH@& z?NEFB_Pjp!?Tk$k>hG_AUg6npv1V0c#nt@gz%BTwOw=z&@J3qmX`sRy4WHK$@`{;C zSH0}_z;V1HqvUVb-v#?RTNUNcfl4KifCu~$qwAp9jme@0GZyazP=afhFWo~A=lY+m zq3nzcQjG7`)LAgqW47vHQU-%#30%*bL=vKP4}bTjQqtQug~V?Syof)3@O)ABh?YCN z`=j{!&TgX8_870a^f=+{Py7UQ7Sy6>k?4Qs?lSTh6^o>yL71P@@@;!LP&UjVq zc_3q4O$t*rqaz`Cp!eE%{i9%;VTlN({z*`#vzbVV-etvFjTuC+8*=k7*Oh*F)0*({ zkyyp?#}O7raIEEnQ&QG>YT&u?5jdswmMZDSr~|CU=3(aJqh;*>TJfnXl4p^d9(&tw zc|avk=3Ej7!vVmHt)~hCGF`4$HncVm6v8*!yY_fQ=^`q~udhKAzN*mE^1>@s5lT-& z&UbCTh&TI-wg*75t6)q<_-mg&*9;qK&BU;!`<=hRU$uls^VCcwdz7#-)o>+sg}R7N z9%|uK3&FX-ORy17Q|h!@%*aL&w|4D-kLV#qm^{LAH0hXv!Sb0+H5?yHPpBYVJ{{6O z$Yc29^{=0nNrNF`S=)lDG!dVXb*0<~`t?*Q$Qv?q;6o;*6UbEx;`jjY7R503ZpQ}0TD%}k?+*@rSV0dl z!b&P6$qH3Q@g2w`emSTa@7a|GlO@#R2m9%KxunvAo70h-_<2hDVu;v{JEPy04kS>C z1v~zR6*h~yx`#sjt?C{=zQg)#z%5`M}kvSO8J9uQ6mTb(eyU|G7|wL3Ue}6 z57iaj63eE8GVRC`IeFs<7EPgiL&KJXHVpoFo; zT*#r^fuvH#2Aw?AepP*oO_#zPYzt-NR9bL0_s~B?ACJ-hz*ThBCPR6k>_*R~t|}WL zI$O-nlK-oy09~x*WU@NcaYV zKczRuyqj4q#1)7`ol~y3Tf!~Bhw(=&L}B|q!I2ZGOmtMytO3dAho*<4%t$eLB431* z5;=C?oYVmZy=0*mD;Y6Ev`8sYZ?weVRKrw#}w=0^hIRTYwsIASv(?-QmXKn%e`&tu&8KAEtuOR2Mk{%{QjCQGCDSf}V57 z@2+b-Yh;@x|@y_q7apC zTB2`(x=L}3&>XxV{xaKt6OeIci9cX1#mtwz`SvAABlv!=Fe&(fc+(EI^kiZg|ItIk zGQr4oqc7u^rH`1cQ0>L<4F1oD;tJ@Rx|-Xs8!|YEtu0X7_86pEc+OwEfXNbl;aDxn zr;aM@3S)_u@p7T(WO!;Szj(`tO?j-G*~AB62PDEqD;iD%ImH4%&)(YnNxQFXs6-~R zEjyfa8b|CU_({=y3~cy1myYNjoj(}lgCg+eCXumsy?f1&Q-TG8)~=Kk-^r~wxwza} zGAE;Ps0_U~bV@Il5f?7z9maQWfEO)Vq-)`F&rQW$rCO258aD2X*2nvbt8Lb_VK#hz z8$QHdMuCkkuf)Zh97dG4!N4RdFoPGncBk1{G#?itLXcjjYsIQ)Kdf8gkx?xEiCynr zMZd!%v#xBHz%^#Xd=LDncz$cr zZ%`_}niMaQZ&o^i6bk%Yvyhl7*i{V-t2njVO!~>p!1+jrdoJ1STa~iSGlyd0fq{4gK=t=spXQc;x&JBqN4T-#w@d!IU`7seWYdG%lS{{RI{%=a<7&vM4TY%xLrDVe@~oVt&4oWpW?|^dfJm{NB=MzdDBSLsK7Vv zzs#QMoSW60YdhAyG{fbYUw+Cg6XqB{@hh#F)994sLx2MHERy@Gp9@Fr@$j&N@7I;n z8ee6Uk)@`T6z8BrY|>MxjzrIFyvi;^Am*V?0i7!(L5`c2P;px2DC?VHgyq!|`})oE zMd0CXnSs6jS~G>;kNt#W?h*EnS-eTfH+oe}0`4C>OKyQBTk7Gb6c&Iu;Xa1f^)agP zTZEOu{TycXaUVvveh>Q%@ar!aGXLs}>*OdSZrob{vTL|LU$qzYS4k#N?#wR{f+U~a zg}(YvIFgX(YK>R8zHks;yy^Ck&s-XrL%Ym@`YnClrQDt)+nh;F#e|5?&Q`O^Q16nw zV{u#U@q>5M2mVE67M|+DOWOjk0w(Rbnu@Tg59A0RCgb(_OGr--6apFW5NPj_QBV*N z5I9g@K48ccuMp7SAqc>*@QBE$=$CO`d_rPUa!P7idPZheb`B^vFTddb1&Qf@Yl)|R zP0!5E%`Yr2{i7vb-`L#R-r49bh<*bR3%@f88%yK*yyl2+*~qh%5|hTYs;2hhe@^?`*r+9!z8ZINsTQwLe*^TB!c5 z-lF(e1CN?UHNgM>)YSk^X-ws|Df|HzJG_q5l~q+LjDJc>7TMwmd$V? zeY?#FG8@QdB$dy9x5Rc^F)ZbftyqqhgRMB80haA}{yDqt1mQi%cB1&r!FG}qD(g}TXa4*+wfOS95>wm~j z5BCd0P}vTO!buzsildoe2PN@>M+c?J3T%gE>G}?b;A|V%VR@d<(P2eVB-_7R;{TAC z>IT@3tD5E3G(DP+ocVuaWaU5_`%zrQ1(ah!J}lRBOM05Dgb_h1Q~ocH1> za$NKg88}_Q`^jvpE(WOn@m>#26vyQtLzdI!5KBeXj*oPmZfm{&}aXG2#8H zt8ww$ldFHV#H7yG{~|F7onB9CDsujr(KT@XGizvD{b$a^_w>)ac@*c(f_0Yj&7xgJ z_05t~>*>w1+fUBh6|Z^c+f~2)>f5!T+tb_i5Hzm4jc`(zyUl3kn!Bxdp|k%2G2QQ^ z8@Sx>X4}@>@8$WP-R~DgaXlRTvn8&mc{r+SJ$pE=`^ohm+37#g`G0?Rqj5i;_mR3j zUJNqVK3I zH~jH>-WC3Iw_gi?e!Ttt@)yJ^LO@gMMxyv3m&g;zS5|DQW(atFq{*#!SBw{>)JI2g z5&aJkQ`TS~ljuc^W_$@zg;GD8@kOlRXbDN{U_V#DMV$FRTH*#b9}A~h`tD;N~m^M5LnC-$UO zdPT5#4yitFG-Rc+eY)|1qFW+)veY|a(k%;Tntw`$TJUB*oP2C5W&au99#tAfP0TC)O{o&a934o+dp4^%V7ep~ZBrBpsxn z+mBAdY84~U|-fc{i}Mr7xk!_#f; z(WCMCR%v!SOxt|S&H|&Ss6-z|Vt~fIp!1D@ zi)u8zR<^Tz@`$nTT}%7qw+N4Smn#R6^wE0eHQxSTO+T-+eEk8acB0Asen?V^ zr9J6FO>AM7*m6rSi{i?>09*4;ZzfOhu5|lixNdqvN?H4<-^Eed46&>Dd%aX0$5v$` z+-IuxcfTB*ct+DaWMw`WV^98b+M&6(wIA?JSEQ*d$4BR6!#+m!`dEX}Es`(Jh4?-f zyfbyi$OM8tQo~}2uTzBQ*NiW-C?vt z;JdRlQ!OinzBXE{f{m<3;4Y`H*Qky9Rl;!9p5WJZTG=@_wi(I@9$cA`vS`o4le|PO zt1avxoXE_n$9Yyb-K6WD9C^tjM!&wuRGmqGC?USAglq?%rE@rV%;ck>B&hDanc^2& zXB&lDf!JJy@M7+KO1Zd{u_~W41#RkM6BwBkPj?qkPhaeXsgW^zRO&aoP=LKeUX zTn(!569)tak?3HBa7$%uGgA$ug_yb)+9qlzb&jvg!`Ymzl=wUD6+BBA(yfTCAX(cp zW8#6=ik-I>E@RMKVy{ji2GHqj!16CVMaHo2gcf#%4xctQ%p$sSZdHAS!qx>t{dy!= ze#UA%ZZ$B;f5qK&Q&Gy}0{j$aCgJihCd=%fiL@K!2JVUW8soqF+zu}Zu`QVqCw%j! ze{8jRyTzu&4qe|wkoC1&u& z{friYe3&5pv!UV{Zxom18eJoPwl|Ap?p9tn%JutJugrb?!n64I!_VeYcje6Qu6?LeaS?0I^417E6lJHQ%GFRF*tT);6)X{i+3tBEjVI@59 z&^oZcvtXtpDWJW1?vFnYV)pzI>!gvG7aiqF&E8W1FkT`6)2*KUx#5VLU zz{@+Ydi{2o*4@aAgX($0fDRUbK3QZop4|`tVJuq00na>--^F1gM3pCQa|5pZ?oa$A zVM4WLqy-?Nmkd{HGBMaQ0ZgNOZiit*Bpc3F~_PXS~srZ&gRzhjnQt1Gsbn29}qLB1ih>Thc zK1mLyL=HV+CY^>dx`7C;Q4W@CUB-w-I)pI8WhwpT?H`~IyerHTs}Mp5DU%x z>yuVO$>v8a(%dP0YQo@REQ-sJ9STn^Pk+H|#sNAEk&kfa52cBhFvk~hTsF)Jhvlg| z3TcM1vDnZL$V-?4N?1T8Y#k*WOC?$7#azbNna~{0ncTYMZ^Z>X#f%^s>Ln*03T%1| z1Zf9pK@9wqeOb}~XV=|S)&3%3kR?6_x@ zE1x zjIU#%<(lGQ#@!FZB1UA9stX}2N35?VqufHw<>c;x)yY4z4MzQaxt-I6aiw0dv^{)1 z5~iiUbZ3_}pDZB=(_D#eW6wf&rhoSa7}}qx=1@l?|F_{Pr=@OraH>w2+PnHAw>CEu zrC62fEQoCB57YR#^ZQ!F?lJVmgaZl$VvIu0w$v-*lI|vI49;r0pyR5x7JD>vpR^CS zbYL2{pH;1NDLUJc>&_aI!hK|~U7`=7>K3o)AZeD3Y_B`V+i08#8#?G@yX^sMl}=`Z z+ZpB(7(+41w2uda>!}mc-+cX2lWsZZA5dkYUGHKp~`-;4QnI2fN z*oQZaICozWG~M1`56eF!(n=#(mW1J874tkYmOU{d`Rxi%-6e*~NVkoI)5UM{mXYEj z3T^>;M`LsDF+VsU*m05I7GFxWuk=%N=_1kZ zUnaMQi=+^&G8#1HK^cn6s`W_eG9YYuqTX3c0)fcy^eeq7#vo0JaOy4C0L zl8vm}LMB$ZS#k0xTXj_TtpRzRJ!fdn77#?E+dV36r(--$VcA3RRw%6b++&-g5AXHO zur}BdZN`ZrDZ31a{I1Vh*FL^U_qBn^H`0tf6f!vu=0#a}f#~&?yK)%DJqFb7i=>~` zunAaxbs;E7XkP$YyYj28MTik_P4sO)f9i`NWBf*9Hm6wk3EQ3Z?X)b1g@y_6z!LL9 z-20D)PFnP|@7KWPN8S<-(^(@1uTN_T-4n%_Iu3q1BN$*~u=_|eGCYkX?mPT2wp3nc z9_PGIqls5Yuhw{u{S9iXfe#GN`oPFpgH8RI=c+v8{ESU-VNUJb&cm25+?LS27B}bI zgVSLOh#lGkwMTbByI!4ft3=;slYgHMCbPE<<1dQ2Jm)RR}O+;k_DVal~{c;wU2#~;) z0E%MT3t;8MS2w%7Pl2&*gQS+9U|lzz(_JElP1v8PHs7Jf9$vpG8W({a*)jko4Q2b9 zvbb2*=1o-cnSviz)=(hPHkWsk=}U@KaTix#eV9MGoUi?=Xz#D`HN4}48A_7x*05Gu z#IeuEsU(L25nn}e^k$#Gk~~W?SmTbLM`qD&;=GKOnbj|1v(VQ_CfWvs%!uPXAGc|6 zMU=(4E>m+KcU0}Cj7y^~GkwE$xRTmOZMjkcryutQLPW-W$FB;qdQ~T9+9#s^VRCyJ zEo*fA${N2e7kl~#7x*$nGWI9BX%=3VXdSt`(AWe_`{N6Qmc+C62?bBqen*vfbaTG`%bmOAP#BP3b7HXX~6yrgFa}m0UAHWiTZYKo@^}`HP0>orTBNj!( z*#RP00LE@npZ%zV$k6S0P}7TeP1?ef?a%|V0Zw)p836PG;TT{tvc^JmPByi3a%_TI zY}jOYf>a!^J2qQ07O@*e2M|{Z!7!mgjyOkdHU!l90Sw#1>)WW>48!cR0iFO1cDwkL zy7-9hD3j=fJ^*@!Qk(+|9GPtzrGx|__bz_?ZM;)5pz|NOIeM1GFEu&30+JAUo>&+X zbGHB(&rVEN0yrGR9g$J-1CSA=l8Yu2^W2c5Hj*QF;%g^S#t#6YgOSKaG1-R6L$oPL zZjq^OF=xVn*N1?Cg(To4D)AzQiBe?BMob(}64m{iC?#}_x@e1{F$N&O8fM&~sI^CH*+)zJXBg&W7`JDbEM}P9zaVcjUq;K; zMwvGLnRYpu4(*vvinQr7+FD5rHqbwi)EWezrfcC7Q#jN1_tPt{Sph$MOQFf$% z_RCX@ZO@Khgl8xI7Z^BIBq!Y{C(}PCJ0}O!o|Ctj^N-asIjBSgRAvM!_Xk08K(KaD z)gq|o9#lu3+aQwLWR%ozh+I{}ZXn9MdV8^In59&n=NpC1B*l(wBaHDC`wu+=F zMD+%=UZMULaX8JPppABhCKpVWVllN9zS~6AtOHIMcx!fJy;3QXND+=YSJU2=KG84E zwJj#3KycYWrQQlHpDe-w5#HO2-5BXiHTn+-7PB5nvk$v)f(k!1ix4qLc9xe^*E+%#;i9F{c4Jb1mCN+Kl2wRMv4DIK zs&W3 z7RFrdd4Q?u?INRakgsZpW+fkLuViz`3QU)dBUi!l*=)wF)xONvld=9co-iWz147vsfJ~FB7sVOl>3( z3$w<#!H<8L$N`}tKDj_svvQsdK!sUtf(c#gfqj31KF9I<3F@oxi`16 z)w-Oco(wmq57(R>HBM5fS;aN?T66aDve19zs;oH@dG>0wuG{n z@UaY+qlbXX8Vl?DQ*JlW3GwtLMDnQ2L8_bevB#_go<0sKmm%zVrRL3u^}`UuOSH4+ zXLAwXa5=7;D^t+-_pu_kHa~Pd1F1)UFr9P{pnf+>?UU&wMgn$Ip7dhJ{~$U7)GwFv zap*~?^_V{POXv0LZT1cS?RD$y!^wlZ!Rj$&>rtfYXEy24A0057>iOx|jX*^c9NQBX zU+jg%J@mH^5omCBU1|X&sNXrD-;dPC3`YBcMhEgn2RlcH zS4KywhLxrUR~?|6Vz8Mi)cL%zxz4fKm97PGIBXhgd?61ysYWISqRV-N9L5_Ta||L7 z1^AInbgCqst4%=kCjOjg?N$-#D?**+ev~v=4xj=nNUB_*gc6sO%@YB zC?(oNuF^#0Fl7#!5`Uba$S-yU6)RMA;yM$VH2l!x0JBp3V5J5#kNwhJ`lZ?NOVp%~ z7Wa^5TuV>zr*tf~o`IE`wQ;xbf-!VH%BAwqiQAy1OQ$7h!qQFWQqAg8UG-A^*iwV(a?|JKTIc2VgyqI> z%a?gW9jnVd)GK{s%l%Kw0pG$KK7aWsu`=+q(sjBrwYoC%4LyI%iX$w5!DL?cx^9Xbt_?Qw|Z^Ay71&_?TmVTo^yR^74z!z`k!xW*Vt<}QR|P`>(BXX ze>pc0Yt~WL$OVIN(OnixM3%}<$pnm7r?FRmnQjueY!U@;QWR`bNp22u4h~~4g^6w@ zvu!bs2T><(zOLE2Zrx-*+v334<_(6Uqv34audWG5ZlDHl3qG%lCT@#AZ%e3eNEd9Q zaWP1Q?fl8vd4TRny6nim-j(e7qWQd|t-hmQuxr@0`@l&){Cw-z*Qd6Zm^*4? z#%7I!xpOk^&FDTF z)EtgXL}%U-i`CgR!Pzlh+l2(}iWMBUac%f}9sRCb1YXd^b&Us69MaSr6pZqv;k0rF ze#?CZ{#a81NS_>s1t}=!po{x~d^kbF{YFBJ--fRdOeuo#7KR z%UQp^ykvta?*J;e%xMb*t>TzyNs43Krvf{4JPoEBGD+KlZl1?^$Xw zPqNwWpiBMIR%qbr*!(9C?p6>CnBv;w{6G(gGwVbfU0AN=S#S9j{P*jTjO%gjYpV{x zv5bu6-<1G{Nv_Jb8UQUG2{m71U-fe9Tyg)u^oE+ZYKGN^80sHGmP;97l5d#i-_nF8L1cbVseXj z&~ivZRMoOh%uYxJrKCmW=A>%Lz^IFX%3aJ)=ucRmaiR< z`po8>e^MskFPMq{+kj|VwY*}WKfCH@TV6&> zO{tDW{=ooH{7(7#5j#{5Z=?LnU-h?Tx`)LgA=b#Ke!`gawZ&+e0z&uw<*I z9fmT?geeYmL?9Ll2PYaab;OK{%1eraFT6r zn^Qkimcqd|)u$tM2_QdkeLf)4DU6qc|9InsIOne++E^Slk+Vc&OJy1N#Aw9I(yoJ# z{){q^;w@7khYCULF2hsAB!?}35oc_kdgpqAQe2z>ktzyh`ccBnc$M*Lno=gUAqAs> zieXciMtrcjs zKb5rdmCz;=3wd)4Zu_mZ-F+z!DlHPm z>ay=orS$4+y4Xx&&&6Tp?Fs0A?}r4`w7z8`?1@`i;$U>G2iIg z>m;27-o;-cVI7|q6tbx0jnsv+{K9YvOQco-$TVNJ2)QZK(tbZV3Sm5&g*5cCT%7NV z!hx_s7|RbxZ}U$4{$U#TXIWXDy!xD~r3@ruy|GYeYy?JeZRDJg-hSr;kPSdZN><58hS5X$;828 zfFG>pmIL;>-Y>=_NnWV-4rC|$%0$t{G)!UMUk)VOB*~O*kaFC#)oR}#N3co3+1Z;6 zwfdQ2Kh@FoOWyUj%+Z!sulKzF?ovVA-bw54VsDn**Ec~Z!EI7M8B4Jm+Z-*&Q!})H zwwfw~Hz{bDIg(xd9T}-h)()5A@p1%{8>LGE$v3{)@5D1LrTJ(pvu3XU%p2Td3iFC==2`g@7(DKHi3| zp=jNUoS7W9IRfr#^W;qdo7d&c=)tEW@o==?F5oQt0fSE|~fe?uPk zCbEVv0dr+vAnapAz;GCIM(lQt7r9OPr7IuRC&EgP5>|c29KMuI!I3HK=~CXv{VGfT zN|DjL(Z>HWx&6(d^sc(%rzGhJ=MW_C6=vr33Z0YIvdW~4WOb(Iuw17z?(lqFPsL|y zB{JMIMw#dp^^WT3O{wspG-@;KcOHbmacSJ9tI<{d(!ttGW{yGd50vpVI{2PqdOXNU zO-VfCaExSZG3`Q1>4XpXmcBg_%4d=m*tDBS-s^P3Q#}D0z;^x6U!EyAvBb8m$80d& zwZ`9=_O1Ed*T1XhiLITRWoIU?Vyi{X{LnFRo64ePii|lSZ%1_a?BQC|cu`lKt7?#y zM&&(GEowuPki@oed;7fgvrs>Nu*syL{^;g_Fbhx6=Eu6bK%wXAy}?&IQH3V!a)NSs zwgtz_`DFNyyj#}DLqT|gV4fEi)5AC>U%eIwqoqLE~g zxZgkD`}sP4BsY_C#0W>(cbNPh|`BsY{aB%6^wm`+WYH>-Vo~SI;xK zNdf(DYTaTnTigvxAD2pSL<^AAr-2sR4qeZ$T|eG%TUaf{|5W$c4Rb!pRrPHl+~d59 zK0fmN>we|-R-g{Ly7wc_xGj(ATO|GW4VUHDSGBHZbw9Blrr=futHu3y4evh&uGdm- zMuDIDvhoAC&WyZtc3Yil=sCF={#i543}G;Cmn$6OS;1(UT= zYtS4iP`wREhGzdVCDQ`{KQ4sE2)Ww$McQRYIxJudvcOd_e%d45hHDJ^{bI%@H}rC4 zr1%8EqG+{ZjSl0pbAi}|b&y0GN4qQx0GR;xtYp!YEYbM;QO;zJkiE}M$XcpOd};^U z#6VKMhVb88;%eD|i7QQgl30V5X!nJ1lWc!c9n0ZeS867w;w;C7M#~02!vTxXK7(ii z{g@*k*X7p8KEwC{zj%A4=yXHJ!e+O7{fKuy*uG@EQ+2+LUT&Ps4oqZWBo#J{2A|su zKVy@+aYuU7NBLP1MQ}vh&{rg!Ex2cRJACzV2>nU_8xrh(AQ9acCJp#xp>55~%%s<2 z_{2hwgswH79Qt9;NO9f~4+TTh*Bce;fiGe$0D*fGqT2u#lWp}=$QF}Z=)K-m&|60N z`uy}|&PiozPYtX{%c=Ok+?f7XX&Xe&GG7-oDe$HrWI_sQS6dk>3d z+omQueq%EI+BU=^Cd94%i>J{?Z}bpf@+`0RET6@!keIAsXjbTu*&efhu25)(VusT| zVEe6wGkM71q`7B9>>^&|7oSW7q0ATfmzkdpTei1}Fi4*@wH%sb84WTp@^kt3!8Z6N z=c8W=%qZ6)C&;NG-PJN@p)M-W2cA2PkQemA?qvcc@><=PR@EIoGtVs&AR7HLl+7p&v&;h zU9_i|_*AM8TdD{v6=BO(0hOselm(%Kzp#SEDE@J-Z9wJb9q@k*h}#zuwwK81LFtWQoFM7U09YOj#<~P6 z?|{|4n2;Y}4HT74N5vctl?80NWhzh=|D0cjr5}#K(GS>R#-N{|+=|1}_e(DQOI4F9 z;Lup`j4^mtw0aIyz0gtpC7}Anm9jou-OE8LRv^?bWunF*@Q zF|N&D@=F30#!nX%=DIGD%N9t*g>#1|K=`pbwC>xmB8(170I4J8_|2A+s*0Ucsy!~6x7TCz6 z+9=B2DEy(3@oS@CZlgGRlk{;TpIDQ^@(XpMQR!=w;)f>n%BJ_2b)Cai_x~4R_xv4M zze<3WOB1 zh^{VMdaqs8t2e9$8Vj^r7_?j2SN0tU-Mcs2`X@fiH@v*JzwviG$aiGoclaQ5_)OR5C2;%32KLPA!K<$G) z?UjWISJ6e^43b;AyYgeYeEmEBy!V=CbY{);g5cZ0avHBNnznlycVik+clzM3`%q?D z*A4nl{QFg6`ql0GFL(N{Yx*S-2IL3^ek%<8F&KEr8Bnty_^cVwnCX8Z82sBa@DE`S z@^j!`VGv%i4gL!8*Zbgacn_AbZcIVv?vL&kgCYFhA;R8>XnA!@@orpxFH%LX!fnKp zC6Q%?*iZc~_ljNy!FHzIPFBT{jO$_EfDwMdKEBxz0mZ(Ing%PMrX^KD^xAs)FTA}F z_hWEWZwcon5l=|h13yQ^pvxw59QW4>=gTk~ts<`VD(+-sIE;STn@2ERU@-D!0}COJ3V%-56VB8L z&eR&tG%C(?ILtKPbQIMfqJGYxDN-fu5*mCWir%qJv**hW!73v{6PC{EJ&Nih0y14@ zCsSsp4y2qK(GqH5u9k;t#U_qE#}5qW&t}Vy1A5%>Qx+wlr9*JJQ%P4z7uEuJqBUvA zh7k=+l~WaYb|noSYf*ZExWHYae*yEbd5iFUi-<(!i-1uyhv?!=P<`$#{C@Kswj=iR zRJc03SY|IGNuX|`11pdUmI{e!h9BL-91dY6Go6hsJ2+R5b_IuTg*&brDQ|_pZ$(gP zi11fJ$#jQ18r#>Wl5(&mUIfa9U|F-##!FPzSSyc$LYVAaw6q|V+LR<1ErBaY6t;>f zIyD4vGmH|$_1kSkYoVcn?G=Z;b*I1UjeHgAM6-j0?hSw?I5q4B)max!49O)}9S=6& zyV+b+!*AdV+?wpcd5AH6SVo4pY(#LXjT;6C8|i^tnR#1QA47%ynru8gN{&*fEr@6u z!r5F@HI$Z}E(=e&IGv6142}Ym!Bg1eD0YtYuV^Y2ocJtdp!Mv(JA*{K!;W%pb!#3> zONF}AIg0{tp*WQ>Dl4H>1h}4pP)4KTo93A4@8i09xqM?0=>8US^OSpZ>EyrKqgkMGyPGa@+ zP)e*ALDjU{u3#W3)}@^qVYOvFED%E9x0fojUoXDdA!DYNRXTbJJfh7%qVGrYRND9* zEr?%|vu8BwTb5Et#ZyoOnEi|cj02Qi7Yf^;MoP$=ERPYUSZ61682V2X_fM4P<>I0> zgm}W4BdQnD5gpsGY@WO=dk?YYw81ZZc0To9L_LL^K(MCdW>rf&D*IKMBk|p zgB3o#4Ep=L4era61QS+jD9Z_R5mMjKUlxxg?};yL$s33-LwH31xAEH-QLT|KByZ<}UUA5vpQ1hAvQdKK6)v@9%o}cz$9!_OB)5tGWH-YT4Yu{9oJs zztZNJwy8K-{}U1;zcRnz^VW~83b`eo!5N-T4VU;R5A;l6pJcAjea4f3KR10(-oGL-|Sk zbKh$Bjuf;{LGttX-`xXDsc-qc$@D!q%1z~)hkiyqHXnsa{pi4Jl#W-0+hkP1rDyHd zRaI5R##v&Lc4V+}4$s`3;=+U7_Vw`Iqk_tcy2*<6!W~P&lfH9=PD84FBVGXsO z3m!T=uNnidIwr4i3$M|j*JP5n)IYCjC~xU1E0!PIfP`W3AH$PtnUE|uPM|Y?&>NN~ z%dqNJMgnU{*_-y1tjOMWR`m)r? zBnFsklzaBcg`XT&BbiSUt-k<*8L7D1ciNU`=$3rJT3fm*`{Q|o z&1r`JtMX}w{%VU=s;Xo9^;wsJfzld0{q25jHk;cH;A?sO;#S*loZQHK{Wgoo(V4=! zkHtV&pl^UWqJA^_lxC_Oi68^->85F}o`5$J9UH|?HUrLTpFg|hYCTy%*5shTGCYRp5#bMg6nhuiFpQUL zOO%M5*wtF@qJW8Jn*!073;fmiB-ZnD?Q|m@Z(O8tBLFeZ>X-ewsw<0t z$3$|ApW1Mwl)P+f>fQFQ;gn!c-3ewZ+ zt0^MCWn6wWKHm?jGE32k-F53nU@4wmk7E-cli)GgK_OrU8&p-d_u~Y}OhEIjK3v>i zVvEc=ve_F$Hd;DSwQRsn*F0ONt_x%SdZIK?SoWkekwE7rH}{`-+A=OdYTq_%QEK0@s$hEAcJDBH`HDxCUiQ5n z;yQl%y)eDf9KL0_gK6Qar*pd`Euxf3w9;!fgHjPN>-L-}nPIg=A}ggAA7DMS1)&4m zHd|;h#378C|5N#-JWY0OcvNM~0E|UUDk=TX;FR?bv1>2FjP{?Zj|le%{uRYL(Qp$S4FUG5B+gT_YR12xj~xy^ac?1` zg0EFH^ZB=_3PfOZV*QR(3;{a6KeHy!dsgYnzN3CMJ{6h=AmLGPhIizSIB`TZNE;2o zB=bP9x~(YB^FQW7Nm8jNhq2Y+bZ`+e9Ni?~+@J>|uBOn)93~|K*lNvq#qqx4dqynu zMXUh%L$n+oTxxVAY`FXMfs=3vJQzd};BDuyvz-=>si-u@gc9a|rrgdFW34$$kk@P| zx%+A2Tnvu~vU$h2zogdgtV7#IYbt^&1)zm!-VO35EXwz5tOYj`v?{9}d0JG%7x%C8 zbKiVfw-CQz4y;|&j2Z~HM4M~^Qr^jlp#!=ld+1UN&FS%}-ZpfX20}w>YrL6(spj-n zi!x@OE(M)u^z?yRb#RXJo;f_1bX|8!+MZ!iLvrS{>ezBlqlhW_M^$vKRdVh+pAqq> zYrUTV61*e6Q4aIu;j|j*x5L)5;^Pk4d%59!b8riO)5X~oriJ`)bn=4ZwOJ(MG@a|V z3nBO((yXJBtd3KvGV|tnAE3|+5h1dreKz(mh}C^blq(>8?NAA+)UHgP95DPWIfM4e zQa-qA*&us0izV)on&na@=>nq=UyIt*C40#XA1xa}ky<8FUNb&w2K(W}3gwH-GBtb< z;`X@0kIRrHg=Z%&=FBH;jqa_Ac0&?X#MTwsjIU`&i^Ph#(%jgVu9ttRb@*jxa6GLg zOR1G}H+!wI7QIm}UxcWxDgo%3((I#uY7CUBHN}|O9OL(Dhz@r!r~25O)c0!6sAII^ zso9!W#}2zM25@M=sop(uV3b*&(*b?t(XpS}8h^oBTG!|*T`>tGom#+s3lDqS2zjOg@-TZA47fkm|rF`_X9$^QW1r{^ejx@nY9Y>bkePY#iA zqkftEq2cOl18@5sfUW0<80B>W6S+A$SkQv3;&qZ%xj8DS*MiEwbBa9PEvbU>6jNtw zN~fGA{P}Hw#yBaY@^3@t#7%rshzC-z5Qo>o?3vDySFZ^;qVhf&G~w^+J_XuP>f*vv zT~-NJI4qPB3s4h`UK$kjXCeg6Kf-YK#*>8wQb>%GI}$EdG4JHpYxF3vSbh#M!|spm zoW0KuwVdwN%6`XcxwH)^DK=&wTLTU?6x!(WO+B;egws%?A%Wl}SZdMkIEITeR43a|L82t!${x(W$XK9sUB1SwA zb<@k=D=wgp)%beO)yXU1m>7j`Ql|bc+4}PYSB1||2U#nvBlnbqr)%c=uO>d8gY+PF z`#h*e2gJ|e$!veng4I!@l$Bgh#tg1062jc-c5Kbu5Ynyw?-My_wnTFMyPqQSo=t`> z{>p0?KUeY#U2ZE><7*dLLCyE{MKXPoRqD_Pmp9LTPW}a!s)&5cX2gmI^JSe5P8D4A z%9`u0BKQnovB+3hA4%wnhXSTBEg3Yqq8~}bk(}?8fN@Dp#zVnNfS*jl{&K^{qZ<6< zyv~9j9^C6-E|LXrPz9>5C_L69Y<5lAg*3lk(7=5~qZg5y2heE+{%_ z6IcQpT}B(7ViR4(t3mq(1HYQ^t~f>&hPTMt(k?lUK0EAcq8NEgfT(SQOtJrb!$H9e zn;!iBb43Z)RASZ|M{$vaa>VncF~F6Z$Zg%pWr=D0f+TX&X{4Ff`wkI$GZs8CKpLHW zaxu*M0`cpoH)Dfs?(k|RvE)q*QgK8OXG}b>)luh;gc_@`<;7n)b*c0)>7G#-YRKM@B!NON$ z+{x%J$mJ?V_%oEp9QnE)%Nd+z>7B{tUuEUr0P{7ZGjH(#4SPC#k^T*OT(4+h-= z{|9wAO+Gkr$Nd+M-4_lS&!|*xPXtrX#72){aiaSm^7nHN+Tt`?b6*`@oAD;ptwGNf zu@s3Y&w?}Om>0+AB*H+ARH*5cRBt1<)mXp`wO zbNX^!`U0Ema2^jlPL{t0?~8$l)H5?OZqv+CVUl`tYHw+KJVQ*QNr| zoT*Y-4tJ`&2Su`urVggT6=tWR&Em-jddcKfHJ$iL)KfKs*)_vgH9h>bxO%mt+cgu_ zwS;)J$JjD<_;t%y{Omw1VkwhAZux%m0>5%=w$1`_EzVb|Y9bHu@h9VbHtjMGr$`;n zQh8g#RfF-e`txk%F-md3TFK}M(S^L|Xc{90WkXJALrzIOn0-USt?D{H4!pf6YFR0j zXZ|#kl&X!v4KkIr7n`k8lk{&jhi+pMXq%Xl zsM2UYn938r&Oo$K4x!QwI;~?w(2+i~IQMP8zRb7T$+xd*x7}%XAu8(zWa3|Gxui9C%rXkJHY2?)jPtf@loKm&o7Oe`>hIIwy zi0P(?ou_t&ZEK@%k)v$c zD{TQni9ApDg$lG)$>*tSX_e5copiSbpfgKegEC2X`nPI?D)8Iht}|JCl$xUQAN=99 z8TuL9D@;F{7CvfhT6-T|k`X@#QF}4jS1d@Hjrb~%@4F2aI2#Fj8#F_^%OoX>IETRq zLdd7hUs=2EB*Q5*W`BehbvD)wy*+hxJLnW-Drpg zI02*b0U@>!eY(^#%DMT|TaIhjnPLJ1I);7fHDfzHV~jat^f_Y^3VCW?t*riIw4vi) z8++mcElT#|FnR-`F=JMU6H+xTTDc7gZLP%Vj`E)JrPX5q=yr7g3@^<11W;*@uUTPt zGX8V2&8@>br+z!BBUMoS($l`xLv1@0q!zX_tkq&D_-=gr)QH7pG9oE|@GzXqP@jUJ zDW)}z)M`-m*Z^Cj@e@obzh-D7+$w@l8N$=xxy>%oaCSUk7R3#}t1`17sEsBa!+tb9 zE{*vXHD7LB=iwSlRzTo%l>re3r9DcIxkm}eHQzx?3^*Un1`CeUlTr#WWmcFpt+B!O z@56TI-?EdNG-P;PCI0waSk{!`kq&)WVHJtpGJYkcLu#1e#z*E8KxA7{i(m zfXW6x$7@3aQ6VTuyQNGWMvx5X>&Pvsxdo$^Ulu)(V)PclOkwQL5MkAp=?#s943Tpe zzWL!gK?KA)PZ9YuT6`=coSRwvIyucgMJZ(*wze$*^3pl|MJN3QMowv7Eq9Q2Z{z#l zjr3`_lv#;BMOg9KCI1+|S_`B^95nk0#F_IVI~m7#Y|kOqsKh!itY{Ld#;qSZTUy_~ zGrBI3uL;ePZtK5nPHp(;e52jw!jO2Q3tjY7@TU4fllJE&ZTc|9a+5Ag2vtJWHPOu} zkUJ{B1zSna)t46u0E-zX8@?%;C0qsJiSHK_xVM?NH;XR8=eih{Sg}MfarAfAyvciw zI|AxFLHKf0@mAOgy*hWip|5oxjZ!5 zWqQdtICz=9=q~Kpi5UN2rEF=(^lvC~wa4`s7hU8R5zJIN;(!UO%~2T-;kk`@`)1!V z9G2`XA$2EL@2n62$Oe4S_GK>OCpRKE4d54(ErMA%{$axA1dC&F7!73yN_+%uSmwe6dL{nu%d8)Q@R~;#IHREQ-~uJ3E!iG;qLn9C8a6!QkMgiZ=T;G zk-lmL6E*fZz4qCP_T(?iv2vGZozL2tQ0C%lWh&eGu!ih$7G<3DHk3+^ZvlG&hPBZNiu79fU9b zh=bh#cC|1_St8~TQjBQNp2(62(XD{P1a%Ge*JomFWIQ(lqq>TL(M$-@sE}-^>6*_ zrE1F#0|=H}8Q#BIsm~|zjsa0>m89c2r$|6Inq9(GeBBrWXt-vm+IHxBkdn$G; zd46s-#xgPL>q%NhC&j8~rSE|csN3(HR4GVkHBZk^%uHw8rQbX}^*;Z8eEE2LegFIU z5AIQh6(AM_`wibP^R{p}0%m6@Mdts<=)*Mib?^q}mMbUN5sz%C8&#^dX^Z)l#Oqhm z+tXuF`8i%Js;VTYlf$RsD^sWER@bnv*?`u{6^Gd4xKb$t5s! zPTB=pck6jlP3T6A76lP=cnKG~_WY2s*VLygT)kQ_cSc*!s;5&9A9m^9T@Pooxgy8t zj#KUzD}@>f(tY}#w#(_JBf0-iqmRLIT%Rw=Sg8o zbO8CBzCDwBf=d+`?CzJ*$4e0Qg(CcKqi-_DG=0u<%SnMuYr~weep1fV96AC=GptNE z9Vk;J14^@mo>$k7aViVPPNaK2*U**Srd?N&AAi+wWO&9kO1)1lhA}ToRnv1N|Bunv z6#SnlxBoHv^gEFx(E2IWgrTSE#qtD+;Jy6Ii#op8`Vx_e~>UzV#Iq*~y5bX4kX0U6< zaAm^pcUsq}5P`7Yd9%Ec8gP+({ky}7OghPLU7NwNPwg;gBe9t@eAO&E%?xBo^QxZX z+=@ll`)3$M=H_8K_DN&- zuhGYRX6(C-kP8s(z9=~rhk;*<<uI2UI4WhIRLNB8iLtN3dOV8k1V|S9~9xMF)NOk?Gmc4kpCZ}58Dsuh-6-v zHjdUYP8uG9W*>*l)*PmS86w1sCjE<<98pi|P%n2XU5dXbqEJ)P4PqpUHU1*YA&Z*0 zHgj@Ia9L7?u?S58m)u}_Fibiz7^u+-#gH8($%QS>u8$|rlPL;AU1^7;>l?~!I4QF& zTmN4ap^IR&(Gl~e6CDw9bP|(Z6&)nOdxTzlm+uf@hWf-o_>3GprR4ApJHtWo;9td{ z%6MJWQOqI(qbvMH9lN+v?-)H!72Ur(71Ut=GNun4A&Fjccu80=yr_fe3oW=z@XS*D zZ>F%y&Ba;Sxuw3pY-;OywzW!81eNx%P%?aOTNfd=XIe~v{nZi_I zNVswMU=pW7(1m@S(?&iFUkVy(rObG;aWwbS9YdCW5Bv@*uk0= zI**pIUuDB4U9FMV6Zh}sQFrGt2p}CDrs%` z0)j3jkcBGNuMEYTmz&4qSf78S7IP(Wges8L?_)>e@dPTz%R-zoMLjp^vc~ihiG4EQ zE3u84os<-IOUkG{p{9B3QH^g-RpRdW*!Mt!UmoSzm_F4{m#MK;v8-7!WTtmb5xxDr0jnGQ=m3G5>UOlmOEu4(K z_JCiW#(Z17HxsBFlOq%TALEHjGM+lxu^X1@Cmk6of|BySZ<$>u1ID-aXjD++P)NQF zgEu~=n8-z?C2cUhzALiAZ(poqWQt}fK{%}5r-FOvi`?+z74+P@!iRY&yz{_@zBRW^ z>?JRA+V_o$e@8-1VCC@5OJt>B@X+GhG*pX{f zFAK+m6kfVE#(KP3V)lBRz5jea@7ez_-WwEAufta9J;E5UG;Brrxi^HWB>Q|kZE_+D z-V+|`-E*Lu?OVkz#6RF^XL$xIftjMz7s5^-RXX3cXmETRWQ%59lm*d0SxzApCuS03 zmHJ$O5*Kc8iL(NFcmg2a8CWO9`A0o6@rr$;SN;kBjgGLh&rd2?*yJD=$lDu{&PWTL z;fQAZ@BRlxDCiYQ=2*kJ^btxJiFY*(@v{@`}Kw1mx$#274*Suy`%hY4aR zVyA&X$AY*JVJsuXtANcA)e?CL_G`8Yco6I;w`+b5ZLzW!(+z8bu@}z_Gavh|xEXeN zZpqV1Ay7>4W5`rWQppvQATgA3Gl`$hiXwjn+e;FgJ{ewKP1w^FgPNS)P>h*AgxKnk z{1z4Zw>72LFx(SQIAAhdMhv&n9kWdmOLHaMmX^vC_s8dhfXE?M%4yg*g>*bFFu7AD zVpVh_+`j+F70(0dg@cnMgZQk3Cgg}CNu0#x5izk@WC2xFE|jhgmg0SgEAyW7&_y&g znPZzHW}kwFQyozSg7b`m1|^B%MuQGHH5xID-agopw^bx(Io6BD=ZB1sFApd#)H`lH zGj6#vE<(p=pBH$58?Sn1uLXg}bjEn$DJ3WEtKNo5Unwbc#H8g)bCpcOP08YVhq*z{ zryeeF1;P9vi^JoAD}v5y?HZLff^+A_4DE*Z4+6MG&WuBraCa*Z4dsegWL(e`FT0+k zIG#junWRjeOv;x`Mi>8;*nXVA{5g?54&!Cz`_Z;c;%-I*$%$z|!5!>IRG!8mejJis z9HIh|sDZ(y)WUg@L>kIVv<`_RErwDaMw=g&a)idIorTtKX=ljIA7Vy(0!go@CShHH z+dU?PQ-LNC6(%hkCgWr02p8s@mF_f^?opNQ3iP4>LJ_ujpV2wa?m-c#dIU;ebG+)A z4x3C8Q*hcv6cJrGp*AdcugnFIcvTV<62lakmlV->>0-kPxh+{49E9pNnHfB(92Ts> zd@Pn?0@)G~y!VnCof-c{5!m0NNhUMm#k$cc-^Y2Aitc`4|AiKjG> z)6ME3N0R5}h2)L{Y2`7ZyI&FIT8K9mX)B|Mz$LwBNIAJu={YS7w}R;kD?RsZW22ix zbIhVVt${ybeV*jvp7r8(eF`=^3*NWlKzw2#s~jl(LV{(VvLUJv*vPTTkkm+HRCp$D zaS~g##JsAIOz%WrGUng3ED&3ao5_4^AxyzPYsjwi&B#;(%Yk4ljt(oX+eA23{Soc zpY963?FyHyiXYPz!ThCPC$>f*W%eaec4WrAXb!NlXk?RU#gS1AWjWraQBt1YNs^>8 zP^%sQNL!J1ALUMno^aA->TGCc=40@Te4Z(m01FH0@0w9V6lD>9HJ#Zt#%m&$Yc;nS zz`ic?X-b3&R5$cakt|B#L@(POPPn9@*w}P=nNE;q%wX-*rtPmqxti4Ur{x{Xl&1>N0^h&u#FZ%CYE@^J7Lm1@aY5ka`^j@XtO7gjQ29;&+@&XujwtvV2y zVVGQH^o!Jz+M818MH<>Ra;*S4BsbLo9kuBRRk-2+HZT33LDg&b8k$^zB9bz@C%$$m z>e=1uxc(l4nh^SCngFOIDn2IcZV8$!d@Y4!!<;sQ>oz0%WV4U9%%w&v|8|C8T*j_2 zXLC!I5pJ1k?HDYR2B}~xY``X|ep#a}k{d`}I$S4|RJ{gO?vqCU4%;9>ixam-KO;;( zA{@}srr$AUozJD(pKAT%8o7W{wE@~!s@0aioGw|^3>*i{2@E=$r(W&WT|gTe`J&3Y z%N0t@!p|m66Sym^A?|8odk)0KBC-6|_>aOGhax zo;OvKR*->Xo5K{hD&_hRK>&OMD=31)6Vc`zEjFq} z-pi|2X~LQ9(!=>c&pW(>kXGUazE%ytaP6Yu)3;IOrT{#rQBiPA)o=_ZW)!M-49TG4 z>`Bz(&1@IUVG*}TCpGDA&<~T^q;&(=&d~ZNxIsmsI+tai=q#2qq4Ok+*^ZYOok#b#6iOw znG(b4(U{8flPN3O9@lHxBRE?0D1AlXEgV-DL2J!-@7#pX zIy7I@Sl)YJkx{Rj2d z!o{mUs?oNDJP@@V>IT7piV3ihst9JzqPn&qO0+6&#NIn!O9|`lu1e` z2aSlk@k9Mm0i0ErPYY=-BB=Q)dh~O(SJe)@6>9MfA>fqA&?t986@iSCZt0D)E2y?F zTu6rpE%OnEo`5KI7&Hq*UAQtvts5JsOt`7zFs7b625YbxMKlIh>bk#6zCYv5H+NBU{pCoFZLTaBCYM*9GP9)ls;4gGhuwUCN_0ba& z7-9UO#o;uRDh0`n9LfEIlE_FRAZ#d)Qmj3XLn;VAW+K7vA zg96nI@uZNxGFf<;qdU8`hlz~v!MEp0apCpmpc~=vfpOuvcVRj3@bI-UF*F8?J##RS zFU6Op8-qd|9TXwq>UX1lL}^T#G8JK?hW`T!5A}Hg)Qct;GA;BjS$`k^4VfjNc1&(| zh%0;|*`FajC%fF8)8l5S#h$zRwq8)2YCI==l{fOd!Tt*Va|}2CyUo5<^{Oxj)j-t< zW$S2%ddj^J?UwmZB{uj!LgL>YtM1P?uWL`N>Ib8h#v=AM1LilsD{n?Bokn(FD@(g z^NvYbLC+=V(QWkLQoW71v*H)+->)OC=2QK0p8dpaG!8@|5zprG66Cl}@cczMZnQLR zEa`9leEEM?-(3E^>{Ydp+*0txQ1T3YPnA0cvO*9gyG>?=RL#SZc2o9{-BCjzBcC%L zzI`5G{{DSW{O3OQ`Z+#1xU&q4J*vU+D1W4kuKqIhl01NRdz0)k_aS3B|F-3CWUV`T zja+K+Tw#}cCfjtj=zcj>(p`Ns?rB>ZuJZ1S@V#2Zyin#~pPM0GFOiPMTzzcqGknFb z@3xDJDL}b777hhCLXiiwSOmfp{gN^cce-n$^v%nddsA&D4q=p0+j1y)^JkX;zr#qrW!a4;~fBSXz)z=61LJ)a@d zxjvw;J)tS$p{57eYZ}v+bQ;xV#9Q>Y=43DFum$`EGD+qf%-42xdM&)M@@XDi$2~D= zkqjgs%(kTN-TA%ULXr0cruQz*-a)FAf;Uou- zx$8v!r;aq&Z~G|UXwEr8KeqB`#xT?7Tt14Z{)pF2s1`-~dq(fm< zi8xu7qpOSTmIrRQH%uWN>yb&(a1<`t_L5 zfH|H$z7n5H^+8*YJfXfZbF_Cn7lgNRb0h(ZlARK8S{FBNY#Tr0*5|(1F10>x&-?JY z*&p;psHppzN!c3>+)yUYeRDmowJFa1KUP&fMi<~~2C*BSu;$hv98uicg^4~Q81VMw zH5GF8zY5fMM}$EE3o7-g=q3tnjTt{_>>VQHFsql2()g+}%?n6xvN&+nv=D>kIP6ky zW$B5diw}RiK_5r?yn#b!_z+eX&Y{C)eo!7O)u3iSEk$Gt(2%bF7h5P%I#*+i^1^7RLX3 zRfWg%4Uwa5(12m$+@7s(E{5@F@+n=1(S&@1pfseKyo#<*Y6_ph{Khiurjb5d!FbDX zUa7bbNt=2T`-4Mi0;Vy(&fye~uPR1>d<->%%5KL%(q-P4FFh?{?xu;Q=If7YV)R?r z^JMR?jSGnS-E2~HKFu~}Fwf&hV|SjAaHKo8(~fgbomJU4tQSF#$+m@mAJ-EN|30Ue ze&&AMZ!~;~iaVg)2Y-bBj;bKLsMM&9Go6G2HQ&xbc$QZvl!%-|@Wl}5r~VQ&r2rhc zn+Y18WULy^!y0AMVjE%p>~siHpdFx0Hd{AxO45hC#5NJlk52V@un4c-Jo04s%Ym6$bMMbpY~ z)(Y&XcTSf!Mq*kDZnH^((bY;sagsta-C9_d&{ht$wGfI$c2W#KIV4>2gbccBQkwlf z2^e-lP9ZlXr@x$(HhMxuWjpxA*nTOtmR`G!s0KIoMpyWT2ijTj?rDF}EfR zKPCC0n_a|eSz#5NLb|5oLc#%`ONYIB!hgi3naf8HcC5gqrFw%y<5Bk*u%!red(|@w z;G+2d4uz4Uy61y55dvq65~)vMxZuPUus`pyAqc4!1o)<)+mpuW|xQ}c_c zsdZ(YK;Rl0Z+5mRy5Ke741`z=h^u4VoqnNM3P|_Om2z~4lQ%K+2XcEEVWnor$57r{MoUBsM z^{4D+Ie*)5+1#K6ruBCukQpc0-& zaOx*QsFKHFXL!d!Tm?SFAZloig9)K$q~^p(#S?OZtfDk%O{wD^BWj5oz`>|$qgo+*7lFd%7Gy7*_1KveU$Jzas^DK>!T^1H+KO^A>Ws_Yq;dMrw2EzKx zI6cj^`gL@3bq=?^GGY7zzS-^}fQv_qsNrC-LH(hPkg%T`rF-gfMtiFaSKOu2%ghSi zug4@Ar7QJ>ZIysr0AG$GQI4`!oO4{;&S>Ug=N9j{3jNHw5ixO>9^$(+QMX)T}l zq^2kTra(h~MfX~BlStTHId1NdU_QIPHGi^YNM4H**96-Ar^262Kvu`vU!H7ha-*Nd+}-t( z9>Xa+yNAkp1+LOuqXgf#u7=Z(hznkFMtZp-OE3@m+nTemz9R$=ADuFHoRZJS5u~8h zcU?jM@!hF$f4LVt7qxcw3GaU>8u|EQZ2k6$JC~ufP^B(>0P%U0a!Tq%qjb!TF-^+W z^^hNyReSWXWIdy-w}ujq*L^3^<*E|asAK^ zgi61IH`{K)d;Q|Mre3QBgi{iJQl8MTyiN1u-)D4`F7qtB%})SRU-BwbR*GCzyP!rq zyenRCcht_*Kwmq3Q#V`FvuAi8Gb;N1F&3j2U_Ep52ZoUP5n>3|$@o@$>2Llj0MH3svpBBY|6{yM-fP5E3 z=sU_uZ)8R`q`(wGB~6-a7fLEr)W3j`gmzfpR@x2HVE?o4thk}xyTnc`(lIkuJ8myiTT-#f4yNQ>Xy1kux|pPmABM4riY^XPP*l!6Al66f%np zR1mB?3k@@PlFUpoS>~Z&eWXagjz|23yY!2@a>JheESY7+dc4Ll?* z=D=XA&=GtstgMBL)1M;vzlG!vgK%8UcvHM$Oy8n}ht2;EB7v!eY!*U{y~0&p0AaI0 ziSS*C&dbc4!J)}8fu%f2FZUD~X0-LPnCpcC)Ww*}u5|t4eA{7C%HUc}q};s2BtoX~ zlHf`;=txl)Och6@BNEA>LD;MAC@8o{$`1j58|i*NG9Z)VyO1HjM}&7r84=1FuP`5}HWwq1#T0SI>$L}|C;Kw3^Ua4FVhIxuuB2U|`ES$2ve3C?8k zwL;43L%4a?$hc+_0(Q(ZyZ&N>~T>L!W0xv zt$M07Mozvc>|>%j87|tWI>PW9-YN$5Khne)08C`c^#ESD zeB(px=R2jQDSQ?)zo#i&404V*4QxI$>?O4{CLpe$9$ue|6!w*2su@8cIsw6Sc=#>K zjVgET0gAa^x;hvZ#3{coCoY&+q~m75T8MjE6fijxnBoKcLJ^WWOE~!Rgx^af$34Cl z9fvVJFVMxZDr8%9NOEu4FhjDqirF|yG9wmwBz0pKZ^@|5u(a?&+3}(fK+!l^c02_D zyCo#qXA&8~Fp32wnI(K#*(LokYL@&G;@=|j$^%aN5OQvIDWDv?F?e};VuF)Lj+Pk) zyE&?gA1pDoLb|ng)GAq(JECn7cMxqD<0{k@STPkC<{M4i%@?VVQJN%EovBk@=~rEC z=UX*hod#9fh+h*7?e|trGkU^8R^}=np-Vr(t9QufzKw%e95U1bo05`-Q=MMPfx`xw z-`~uv+=5%+_Tv+bk5LVF)2!CruTDOS(c&;7f{mY-t!$p1vrHWBDkS`Hns?kif9}0@ zVH#-!A1>aNY-~d&w<@gxXu7;nCzyouc!20@f!PQa#pnSlwtJvDYtEkuszsB_NP}r| z@y$R}Xu`gS@qVWQnfbnHtfaeV2oM&=L~b^4R=3H?&|a4k|n3Bb6Q#ecJt#!MN*FapDnVRZay3#FPb- z^!_il&cZDUuwT=F)R4mr3>`zabfa`P4Bg$`AYDV}(A}kUgLI>Gqm-1Wgn%GA>$iKZ zv%A-I{)o4p_xatovXy*%J;A=78i5MQv8RJGvCi^O?2dijzD|78I%N20rxXt_0AhsC zWmO&8rfw5Q;39<^p>I+H>;B#6fzIMNjyBLGrJkmrfb|u(A+q9St-&8m{NOV~J{9MBkuTu=%UA?vs$AJ=8=E`ow=EGmToBir{pm3!3mrW4?a3&|H6pF1>aH zo!=x4_B73oqxEc{6&Kt^<<1VnE4*P+hfav;f!&Ir>-`SG75ARz-Tg4Dzl*n`Zp!uu zK;IhdtMK!;or{T$9SPR~CV2K-nBeSvXm!Wg4iTlum{920ztMo7_8xuPA+M-Jh#rKg z6-`{aOzJR<7wuQa5 zZH;YeO)P3no_7{`o=&G?DQ&RXIdDehbaSYVWGi}a&obgSj#wR02BcG$Jo zUpsMb@6z*`)N^GSDWNiG-krq|!h05UHf~rp+6U$+Za{v{~*!E;0pTq=< zP{86R%AsA{A<2S8{DsBC z!pb>`C1NvBeQaW*qD!G8kqW!N89Gen*Y#Ec0Ben!nMyHC~N1*W|Ku>|A%~ z@|eEMcGj>X)@b9YKkpkWdi#(;q4!zwt_%LATJa`Y^wL_ju<`^0j>}c8KW;xvmh{Zl z^zlJsA;yuHMCZnuN=+t{Od;d}<5(MZleqR|SQFTGo})P-P<#j5fn^)l1#LrPbA3Ih zKeJLPTH>)Ao%cHaH4ZMO6E?-6cbHBzVlP9vH1QOT^(Vckx;xH@fLy{`h(VCuRlnD~ z{VhGU>&(`};nvRG)&%bMGW~Yl-c|$kcH^7vW;f(^i`jM?m$QM{PS+oh)%dtiC-t(u zKHL0kN$9($f4e^)tZQK8oy4dF3?{$eChq65x~_uaILFs4NJJHjlJI^cE|^OpqDEp5 zD^Ewt-&II|wmkIH^RQdmtQzWEt*Aq4sDGvxa;e2sF>LAym*#DGll9j>W8 zacG25!pl>eRmFrA4zg0zCEVu4Bg!QL^&_5C_|M`au~eKe?7$ik_nMw1a$bM|16YkZ znec!CSY!i>sd6W6=t=na8)yke0M2yA@FI~c=TeLp9IJ*6iCE=yxV-NL^=LgB0{A$L zJbdT{V0c9+%c-r>LuHCC;5DWOMel8VicOVJn%Fnr=fY^tB3_-fvz*0hoQ=nyZ4{ry ze>zM4dzLhGl*#>hbpPXNJwk$w7K!z7w;kWTQZruov?@LdibY8a@{yLVfw@hs^ zr*#KkjY{R~WpKSh``jc&I#kBkHJzth$Y3m1AbUozd5Ues2RTd!{EB7F0SXaEF z<|20K5iwC>Dq@5~9z>ey&2B_o_IsK;wNGgnwoW0=uIt(VT#%{vK;dRQ;45WY@Zq=H^s7K1CS4wbGux|4t zpmof1cjMRbBEER8b475uQ&|w#rE2a8w*= zkO<>!K`@pe6OIV`@E9KveAQ;zeLtNk7jIPD(aADDG9gZnF=nxnSW~V_LQNEGDWj$% zqoCI=$57!v3;+>jL4|+I1Ax~$4KY`0)``pM?0WJsfn~=HY>AqY zcM=--i&qEp!Qi8?(Fd>&bB5{BtE%ppnF3+Es%$F|e~Z<~O%H@o{9Ch)RyvU+f@;h9 z%M60e<-%6lU}gC{?O{=wan=AHB%O& z@oL=hR;NNZMrO9X2bOND(Xr~eI?i^g*XFEZIxE2c&(;)=B*O}f9<-RN-vs#03hUTK z9Q(21c68^ksJdX|u9V{VAAUzy|nsadoC#67{nK7*IfD&8S;i|q6{mFS43ot&z;2{@Dq?=28 zM#8|9+%Xe}#bjU1AD#Wh6h&vJsdM`Kpo$QEHOUT~OVvD)+g$ePA8|D5+3Pg5P3Ea zvnM223Mh2atRY3de6%QWt!OtFHELValvW01oOD>4v%pH?!~ECZ4>p&TAat5p6i|o$ z&=xm@#se#nj}&>fE3r}i(ktN?wGm`F($6IxR;zF2Q3GT+uE%};irkgBW;H;=I$4m0Bj_5yX~DLBsu5YURF)Ae7rbVE!)+Hq{iJRJq_bI-Z9| z{ik(S=_H~k*BI-atW6~{coR$x!hM->*Z~om803Mz>^<0IZk1L+(Ufu#LB}@{k#o%R z)IMQwQ6~?D3$NInOxc+Cixwe>FjfCD; zynh+0SmbINn_`;{KHYpqc^@}KX2ovMkyyp9o3E_hwwzV{Ow7K4t-PG^JtU8;m~&=I zRnxgOJmZq_QXs$4TE{yZK~zo8menjrkm=3akW}fPBdR+si!(V|BiIh7Qx^K3-eYVa zvM2w+tKio7i`=Q`dnOIPzO3T&7#6xy<&VMJwl;q8Iqu^J1v20bWisk2X ziEXRZ41eroz=?89?6PDo=w~mD3v{Cj99iA)hABV)0*lfo8I$8X!u3%5YW`ttOFfO^?`QPx-h^vcK_tr&t=r ztL`3BY?ud`nUP-qDtKv66xS$g{8&N=_qXGB4P7eQ3{QM5u;eS7q#;w+AR z+?~lfZz&m#v#Ro&yK=SaGRtVkAlp1;unW;=SH}&aHnc#vfj!^ZS_M zzeet;W;=G*^qtC1isze}d1eCU)jljEvdC%@f~x2Ab#B^?LR?^)mulfbo9*d)SIP!> z+aH44GKEj}pTtZWKMElxMt*Xzy0OjUXL04N3g9R3hgyLg{4>C!%#@`oN9?7WW-xP> z-fcEbywapOlT?Si&SyK`-28d_xR!#A`4cRxYZVzeMq9>Uq^>>Z*n5U5$B?W9Ic5ZD z)vDQnw8N}!=}k~9$Fn2RovZRYA-$T+HR}?a0)@nH6+@8k7EmmafO)h=(oToAdXrObRbBPnV&Uv+H9=yP<5reS_!5eSVZbjX6tr$cUF*mMFsoo zb9u+Vcs`XP>|5Maub~DyZz$ryEB5b-}Mi z@IHZ;e@PYvz}$O31s4tu-_t^k)nQuYIrsiomZ^&%Rs5*O3R24SDa^TS`nA z>?%z9U?V*0e9j~DWEmqtU-Ul55$+fmF*Z;i9$6#hyK&z8^)S4-sy&rhj+c|pvkc1+ zeEoDz6f<+g()MHLy~KPu8tMTl{h{~PB&$1!=G!nf&nW?YD?0Gd^AiiM@2RK3Eg!=O zCt4+y_>}HMjV{E&73)`IszMaANEE9Qm!)46yI~ZYBaD0VpMlJ$-5^>hFuH5fC7&N` z*FrdmaPiEwua6d}x#ERJ3eCUpQ+z?ATfuLyLoz1|A!6VG5#j9K(m=grQR&6sXM+C5 zKyJLiOzqP7TK=X=tS~-e$yV^{2xs154E7Qe!4kU;ulgeji1C_*`~{I%B}NjuQba&BKFO&_mUM5P6CGsO!B5Cg08x|`q`5`8T1%3aRdxWuI>xxH0Df;KN=Ny#!FA?z-xd8`uKoNRvyqDC5{5c zSUM!R;-!((_2B}GCAN4C#beKG5(j2dlhMBT5(Ur)nGyjn8wD(`^V7Y{xrxAb=_NYx>!z^O>6%i1Gqd!7!=a4+Z%N_}WW3~#E zd$Zuk5;~G)8IC}%w6aRdKq|aQVBk_bAz6Gq5XW5xhw*?UKtxhFT99Z&61kt%1OjE9 z(t3>mr`vfHH>uDweY7%E{8^$8{rxlPLAz(-k07qbQ>X!|iB@G4@2J^cmq%%B@N_o9 zFDTb%E4ZmU*FQfu6rNjNo7;$$@lQO~{L4O(M{%Doo}4)n73K`7O*L#ys_^6&(vA!i z;knO|msn8N3rEM$_ zm1ODK*A?~kBpdGw)HmS=%wM4FgNk&*=+Ip9;*D~m`@rlJvMha(f0X%<%p=yM2POis z3cqExRVaJFCLB8>B8WFCNXJU!SgQaEB#usfk;RY2%9-T}|LH@!VyH?QUjef=K0_ob z%z88?R4FDV?nRYoJXLkpyeuJCRD@FcRmqU4kbKW5)T-oJT;Vx04Et+~W3W}t?wnI@ z!}AS6TzDLEphRt40Le{H#*z;I2IXUFAg1b?Ge4T{MWS==r`mluyJUUP2D|U z`l}tm=$)EtiF{SQ1$qMo#`?+X1sN~e>M&c^61DTSG6@Ifu}_UC*p{Kq^F_$f2}7~W zzGrxAQ8c!RHOi2N*T?$G8EcH%`45akEoBK$6d7;=X!3OpN`@r{!ScTWxFZEPgQ7%# zQ4-cOEKTJ7)h0{dwNY72?-;;*&d=>xliaie_- zyF*RdA}D)8(HnoDH~Kpzxsxj^)xIi~HGJAO4~p&Lh*AEdi*azI#4c2|ng7at%fR?1 z<#t51inwp<9x<+G9l#`1e8p0^*~_lb!A@*bDENwE!pehOqvDvm37uUQVeL1;)hq7a z5ka>h>qZmcF6*43JXf@YL0!PoP$zb$~wC%CrI|BmNRrx?41zBQe#T{+N zFGv}b^$zW}`U{xMt@O}=orO5Wd>_|LqFYSjr(6}fg?>l7W6TiiooS?B$IaLG4{H{c zC-|3w@|c_orF4p??SpG5BkSTL#HJ(iHw+bLT-{xb^PS`p_wVu=wcAv!-x8~^D%kNb zt4=!GQee6@$k}qOtJcZeh6!;>fW|!UAhR(>#Z_t+q_%9AqelL=8+^8;jT-Qr-qK(F z{pgTEA8bNiCWq5tmF-q4+&0RjR;bH_anS_i>x8L9-ih^yt?A@fsh-QxqE?jXOu4T9 zhsl+FU5Pa(jAUn%B(rbwjzSZfD2PEJJ9C@L&h$9P&7CM762q-K=`4F^st2>Qb!2CD zTxaUQREw(D?@s&(duP9FJ;Kfz>`84lldjxkJm=p+H4-KEMY9b9v*(tCYP%z##u0}a zV~GKqH)}or+=%R)wsl{}2cG8w1$AcbR2%2yLYagzDBqt6j8*g-c5)a2PHbL=%%AHn zFt01|s_+)gd6_!kW6!-3F47(wrEQ=_5k->}uV7mj0lplXk4Ps@xG=+Zr zg+~eF82Zi0hz*ms*PCXI(JSh*_^m46Xm6q6JA^a&@#}BZjftqM-$Sd_vJ*m!DbIr_ zyACHLi{*%PO0%v5Bm5G79YtI77ZO9bRKqI~WFESnk5nssk}G>!?qaMORIW%0dFX_c zLhC0!*E(VO*KauZ(vOiA^otg52NZ%)`@3w{Y!WMDv`mBfbOWvhwlhC%2?F#hC|+{!;F!X>#s zfh!SZeVa#9i2wa0K&*-8baF*XV7cX`d6t3zi&tf}R|(rP%S7H`?;?+EiB_)6`$*iU zI_!WB{D*k_3Y6`XauQ}herRPlSA1wGnop53Ufx0>rmk@mZ}^xYWG+BAyuaIDe0S1p zclulR%wQA_VDrPD-5n{?S9y)US=-1&Gd*s^w0>0#+?RWzX8E&d+$3huUdBceC3xGb%aEU@d zb7SUlY}t`WD~?xJUE+4>(7HGK25HGHPcj`KYJ@I3|M)lkajXtCp-S26k|!(zK`%p< zJA)35E?7FEP1|Q%q{H}K4$n`IKr=jyF+XcHcDK>-C8_rv_CxM@{VI&%2~zN(QV5C( z_;iG?4P2g$vr_`)d_M<<)sO{yQdJ0a1p_fNN|q{vZ}BAkZ3!0WAw2MduR!5I`U0g>?PZFo@5os)o}rR9D}w z^(N(}_7lsSkd-nK1}(XKzUp3JN$(MqLTvOGFMo_Un=ch$PEw|m-F1|`$i}gJ4Gu3TsJsD6oYSoFvAi6Cbh2XD zW9OKTN=~+BO26~p90H&{v1eO7EMk}aLGcPw0+~8;St?Oy1h-(1fG0OA(Am<~$;&2D zIkJ~4xNs25K`V~!PgX`{vDvH}d}L%a0_I&KP>qknKMqDLC@d|W!>RSK<2^g2ad$@e ztfGXdl&*r>fMQVcJgIQXQtDfgH2vG7Z=4dl1xOlZ{$HclG=`|u=TkQt3_RfL(!pQU zFq>_R@Y*~;)BBCm*gn8_qM{>0Y!}pMIqm3ZXKgs#MO!Xf+x8!u7H+E=rJ5Iys;!z4 zhnj1bsGXv!Q(T(u;$fGA>QU6*(b?55*J4|q;#}n6($bziXJwvHfSleRov!Z`41?P86Kr7o5ZYVkSIp@{U>E z968knbdxieMWB4av$~GqQdvPF!5R-apRz8A`Vh{ev6GgzbabZvM6j%FN|`RI`D33_ z+Htv{?8UjjbQJ@OMHF|lDN@ItVLbB{`Q)Ko3>_mqAwSJwPWm!~3TFm)2Ju2Yt65?L z@KI#hA{$C8JYG`xZScb5>;+tARDunS+)9eE3<;a=9=oK!UAP> zqAZ5xO#;AyEnM)EZ`F@l#}#31#aCROei!R4u&I-P0)OY7w=V671!ax`4vP;GJ8m}4ReD8;5cNUXjdwoFse|i&jrt5;V*Y@a)v9@H%~3*K$Fn-N7c<*T zN`uTDKzw=Ywj`QOLqDgA?9j4R;dPVxz={N%AA>R$jDX40J^BnOUHPD?BH>IP0#Sw> zPNR&GacS59G&bQu4#~I9Opt1KAiZ`RWx&&)7f=>c!6Ubx@+4n{6C%q=WXq3EGNT#N zP3E+wrg|}L!EW>zh)vEUfta?hG*q&)!Q=Y5$?uWfa~#V=CXXKwUG|t9;JRFKQt0*p zt+vq1-M}Hh_hf6gB=r9Lv@`;f{j4n9-}Xd%W5Aw*Yj^=|Q!SoTJD-0jU1lAM(D+x@ zpmAS?Gk(xR9MSbFJ*w50ORO2n#QL$(xOoh)C(^H+k!JHh`K=}j_BX+u8lsEHCmIOy3eYGWJ{bU?MX{t7!G8(fjoKBbXq{lD(%p7HRU;D30&|>lmSUk>@zErt7tH z2bId%BvruW^%Pw^=grtBd`9i80d#u^9 zPWhY;K&2+wPJ-4p#iknRr~|AaSB51L5i$7>4|)P&X<@~a4JwK6o)gf#a7 zS8W>{n9-*=7SX+PH7AZt%o&IvleSy5+Y9ZT`I-@jpQ9dvbKBcxk;21U^D#=dz)gwi zDo}80%u#&I3};b^L*e6-a$#jpSC_+7 z;!TzmS6F&vvM!ixN@;5s?9q38wMJyoc{g(d!c{I>-AoN{$Ezii&!{>vW4l^Gs? zbfokw?&JNG)-3jTTux-!-iuFd6<~ztwFbWBpl2X zxijD}P>SSGI*RJ8c-*TwT^yndb_WuKscu?g#iNPECNOJ8(zQG! z2et~dAOZ!E>h?c6W7+opM7um*EW#9_#AmJ zwiHeE^Sr(lVX=l#j1C3gS-J%4p91DH z+fIh+#;f}Wn7nS!rH>6`>swc85$+_GCLy=_79Rw|alh7?Kg@0nVd98@wwQ5Qri(94 zdE)(Nv%oHAa%@7^v5|A9Hm!Biu3?_;pHVBYRHrwYnDygXe{N%T9%;3a@Wv$L7qT>a z$uib-mHC=%VVgBF2b;?#Ediw1y#dU)UhQrHCQvXD<&wwzOjPhwllS}S-7)Ax@0_2z zFPvlVL}%mO=E7V5?!rmA`nB4DRq7vl0X*a<_K3$8XXAxA#;fy`kjJuQ4rQd1^7tlc zezydIG+G(Ap*7XmS%O2G*4}*GDCd1%yMC0cagv!(pasxg`Au72L1JodPFG8_X1LRQ zW^iB?-FMG~4D!QduZM9~jn+iv0^ak$wFxdToA@+1k#?87$-gUoIH ztHa_0b49Xkk?+2ZL^d>qqoWcEZGLWE<>)z3j{L74JfWzSn)xyTV^3}S0vSChBR%t8107njD-_GJ}8%sZ`2%l0gfs`SAUXP z;HcHLQdL0i=Us9fF^cjI-iivRe(pX2lrY1Wr#MbFa&jwO*D=4*yC{)>tjUxBl9x6j zH;7LMn?%U(oqcmt1VBhVn*1y4Ky!*!miQOrVR{DASzsuEfbCz?T4E9}?KH}z(mpLG z$>*-_6(wx}9IN=xi|MJ|B9X^YCJ8chaNf}IUCYo7S;ohkn;atTY@XLI=x0T66K{M= zIz*a3vuKiV8ruUFSfc=xAMcd#tbUY>d`sziJFYBKPOLRtg=43Mn(BT4JXDbeqhqC* zRfU>c(MZu3Gy{(j}1juFjwgSjjD#hHss&0<^#%KvXYJ21gep8UEU(b?y$7Wl%;H#>$IBvC?1!2 zMzXCywOuZ#D3ILs0A*@BoZ)XvLawIu3CH_KtjWHgwVHpVmB(1Q%~O+RBkVJUO(pvS6mZ-lgScgNgr1;xW>}8sH!B-O6Bt3TI0Mq5|aX?B94vQGv*C4{iH)Pry<xYxFnSoB{f4B4hgh~HIMM(Vi=a^wa*XRH7H=iI#sWk?&^7|`aH4Q#;(}(@ zBh$+1hP2p`H+VWq#QnF##btQ(-ofzw$fVY|ADG;Q%#0Z__9tj@+SB%`M4_+jLW^x6 zZCb9*fli9$B#c@1+C22J2&?^0{1tmQmuk1E05?Ba;+}tERZrpzp(v4Gi7jMFt0+{b zLNv~{)E}#D*I_0!yqtzY^nc^*Sojj!J?ZMCkle4ZA~jwR<&@F+N;4GJ;%K$ubybj$ zmO*BLv2k>G_)}0UR4DMl4t^DIxkQWN2q-MW#2fXL4S*Idav)D>ITZb03Zwr8`NS#O z7W81_K_TK^0(J?e(MR?$A6&HrCc<117Zmc%5p2H<+`ej(DzIPhvM)(h64|&rIy_@z zB7-K#khVU9nk|XZD3jR<|IIyILL}+)nhM*k48E5?OSuo$bzt}=Q$B=j$I$U{ov1v( zH$<8Dla>}(yi17C^z+aRz?+W-`J(}udHMOv4+=c%F#s&V@GgQj81%RTD0D+s z3-GCrh)8e6mOt=4si4^lh%ETUhD9HjegokcWonhdxr6%tQ!}wbU!I}TjdO4+j8fs3^qMM1cr1K{ zkXAryYoU`26ZJuqt-S_KMMyz!>GM|U-x=F1;Ol8Yt7NBqb~ef|-j|70s`nVux|!@1 zqXH>b{I1&WqUGdKTX01-Bu!T(4c#X8Gl-kJ8_0M7)!XF!*USMc;M_1_a~gAr3n~Al zBR-9?NT^?U*qUtrl!t~)GSvg_=V zPj*Sxj*xO}`DH)mHB7tJk<~OMxLM=W*gX9QV@uW<{bX5Q?^D^FAqx#Mxk1O}g(ah?Am@j!3DDNyt`Z(Zi17;;li%0URmp~D3$p0SO2n|3+Gvhc8%Z$= z7IhkOFvwcV$!W4^2ty6Mw>9>7)$jQ5dG+MY?u|59{G3(N+l?E$g0)>$teTJYQ_eHd z{ki*?036z4EjLKe3_m#3R_sTWh4+?xdag?II8*a-MNqb~dSc0(kFKvk^ydy`S+Fv< zoH87m>$6(+&QG#sMg6miMWvofOFxJ(;dQ##8GfqA z^%<3k8C_S&rEOc(y7XmS)E@uTYzooZ(`pU6;O|tG<7C8tc}v|>h!2SxD1&xj!0~;= z%vKtE>5T>{0h!z4y}LVu>CrH!oW|*ZGR8nyWQ;gFi3$mk3U8;-N`vxwp;%H(Q`lk zeuc_S3RB=J<)F@~S2+Q^W}UJX5KI#u=RZX?H&-p>gMl z0t(-v4&WsE?`4P5xXi?z+4`Oyi`kwws=1D)*Dj%RJ$M8$bF7fQhNUtsuReF;Af#E> zmHU-wr7mjF4o~qaoxSzXIv;N)!n@-5OrtNWHuHrGdyBTyL6h#IID8`5bM%etEm7&7 zcDt_lP9qhsvR=yR(h=g?=eNIPs3@Ro@GOn4F)o^W-u>9;O1RrfYV6vR(v}y=j7{oA zzH5zlV$IDQ$I96no9picIkRNPGoKG7?mU4uO)f>+FOCL;i9R}&2+`mciz+))R8B^j z)$&RL%D~ObiNaGE%U(r@EDzU^J_X{Jtihw}^Y@z+yNa>B5wkBqBy7o}%tfnegRAkE zAGN60E-g6+_aXhR1Sa~~&u4{q65iW<#bqrFh^2R|hTPo%0D&anMV^UsG1AM?jMN{C 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