From 86e1fbde1d73d1c10fac5271c42a8b9097ca01e1 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 1 Dec 2021 14:17:49 +0100 Subject: [PATCH 001/116] Add runMigrations argument to DatabaseManager Signed-off-by: Marcus Eide --- .../backend-common/src/database/DatabaseManager.ts | 12 +++++++++++- packages/backend-common/src/database/types.ts | 8 ++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index cf5e801d66..495dcfd303 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -47,19 +47,25 @@ export class DatabaseManager { * names if config is not provided. * * @param config - The loaded application configuration. + * @param runMigrations - Controls whether or not to perform database migrations. */ - static fromConfig(config: Config): DatabaseManager { + static fromConfig( + config: Config, + runMigrations?: boolean | (() => boolean), + ): DatabaseManager { const databaseConfig = config.getConfig('backend.database'); return new DatabaseManager( databaseConfig, databaseConfig.getOptionalString('prefix'), + runMigrations, ); } private constructor( private readonly config: Config, private readonly prefix: string = 'backstage_plugin_', + private readonly runMigrations: boolean | (() => boolean) = true, ) {} /** @@ -76,6 +82,10 @@ export class DatabaseManager { getClient(): Promise { return _this.getDatabase(pluginId); }, + runMigrations: + typeof _this.runMigrations === 'function' + ? _this.runMigrations() + : _this.runMigrations, }; } diff --git a/packages/backend-common/src/database/types.ts b/packages/backend-common/src/database/types.ts index e96f86980b..3dad57ff62 100644 --- a/packages/backend-common/src/database/types.ts +++ b/packages/backend-common/src/database/types.ts @@ -30,6 +30,14 @@ export interface PluginDatabaseManager { * stores so that plugins are discouraged from database integration. */ getClient(): Promise; + + /** + * runMigrations can be used to determine if database migrations + * should be performed. + * + * Useful if connecting to a read-only database. + */ + runMigrations: boolean; } /** From b4588ffdb1db84762ea0d748931962ca23ae45a5 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 1 Dec 2021 14:18:53 +0100 Subject: [PATCH 002/116] Conditionally run db migrations in catalog backend Signed-off-by: Marcus Eide --- plugins/catalog-backend/src/service/NextCatalogBuilder.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts index c9ebc8f723..d85fd741a5 100644 --- a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts @@ -336,7 +336,10 @@ export class NextCatalogBuilder { const parser = this.parser || defaultEntityDataParser; const dbClient = await database.getClient(); - await applyDatabaseMigrations(dbClient); + if (database.runMigrations) { + logger.info('Performing database migration'); + await applyDatabaseMigrations(dbClient); + } const db = new CommonDatabase(dbClient, logger); From 5cb156ef3b0b6c23467d3980e58520a3d06b181e Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 1 Dec 2021 14:19:21 +0100 Subject: [PATCH 003/116] Add tests for runMigrations argument when creating a database manager Signed-off-by: Marcus Eide --- .../src/database/DatabaseManager.test.ts | 52 ++++++++++++++----- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/packages/backend-common/src/database/DatabaseManager.test.ts b/packages/backend-common/src/database/DatabaseManager.test.ts index f2fe855234..56fb8ddc3e 100644 --- a/packages/backend-common/src/database/DatabaseManager.test.ts +++ b/packages/backend-common/src/database/DatabaseManager.test.ts @@ -36,25 +36,53 @@ describe('DatabaseManager', () => { afterEach(() => jest.resetAllMocks()); describe('DatabaseManager.fromConfig', () => { - it('accesses the backend.database key', () => { - const config = new ConfigReader({ - backend: { - database: { - client: 'pg', - connection: { - host: 'localhost', - user: 'foo', - password: 'bar', - database: 'foodb', - }, + const backendConfig = { + backend: { + database: { + client: 'pg', + connection: { + host: 'localhost', + user: 'foo', + password: 'bar', + database: 'foodb', }, }, - }); + }, + }; + + it('accesses the backend.database key', () => { + const config = new ConfigReader(backendConfig); const getConfigSpy = jest.spyOn(config, 'getConfig'); DatabaseManager.fromConfig(config); expect(getConfigSpy).toHaveBeenCalledWith('backend.database'); }); + + it('runMigrate default value', () => { + const config = new ConfigReader(backendConfig); + const database = DatabaseManager.fromConfig(config); + const client = database.forPlugin('test'); + + expect(client.runMigrations).toBe(true); + }); + + it('runMigrate as a function', () => { + const config = new ConfigReader(backendConfig); + const runMigrate = jest.fn().mockReturnValue(false); + const database = DatabaseManager.fromConfig(config, runMigrate); + const client = database.forPlugin('test'); + + expect(runMigrate).toHaveBeenCalledTimes(1); + expect(client.runMigrations).toBe(false); + }); + + it('runMigrate as a boolean', () => { + const config = new ConfigReader(backendConfig); + const database = DatabaseManager.fromConfig(config, false); + const client = database.forPlugin('test'); + + expect(client.runMigrations).toBe(false); + }); }); describe('DatabaseManager.forPlugin', () => { From 2f45b2987573abb267c4bdfbbcdb5f7846cbe7f4 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Wed, 1 Dec 2021 16:21:40 +0100 Subject: [PATCH 004/116] Change runMigrations to only accept a boolean Signed-off-by: Marcus Eide --- .../src/database/DatabaseManager.test.ts | 14 ++------------ .../backend-common/src/database/DatabaseManager.ts | 12 +++--------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/packages/backend-common/src/database/DatabaseManager.test.ts b/packages/backend-common/src/database/DatabaseManager.test.ts index 56fb8ddc3e..895afb4af4 100644 --- a/packages/backend-common/src/database/DatabaseManager.test.ts +++ b/packages/backend-common/src/database/DatabaseManager.test.ts @@ -58,7 +58,7 @@ describe('DatabaseManager', () => { expect(getConfigSpy).toHaveBeenCalledWith('backend.database'); }); - it('runMigrate default value', () => { + it('runMigrations defaults to true', () => { const config = new ConfigReader(backendConfig); const database = DatabaseManager.fromConfig(config); const client = database.forPlugin('test'); @@ -66,17 +66,7 @@ describe('DatabaseManager', () => { expect(client.runMigrations).toBe(true); }); - it('runMigrate as a function', () => { - const config = new ConfigReader(backendConfig); - const runMigrate = jest.fn().mockReturnValue(false); - const database = DatabaseManager.fromConfig(config, runMigrate); - const client = database.forPlugin('test'); - - expect(runMigrate).toHaveBeenCalledTimes(1); - expect(client.runMigrations).toBe(false); - }); - - it('runMigrate as a boolean', () => { + it('runMigrations can be set', () => { const config = new ConfigReader(backendConfig); const database = DatabaseManager.fromConfig(config, false); const client = database.forPlugin('test'); diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index 495dcfd303..d5dbd90b79 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -49,10 +49,7 @@ export class DatabaseManager { * @param config - The loaded application configuration. * @param runMigrations - Controls whether or not to perform database migrations. */ - static fromConfig( - config: Config, - runMigrations?: boolean | (() => boolean), - ): DatabaseManager { + static fromConfig(config: Config, runMigrations?: boolean): DatabaseManager { const databaseConfig = config.getConfig('backend.database'); return new DatabaseManager( @@ -65,7 +62,7 @@ export class DatabaseManager { private constructor( private readonly config: Config, private readonly prefix: string = 'backstage_plugin_', - private readonly runMigrations: boolean | (() => boolean) = true, + private readonly runMigrations: boolean = true, ) {} /** @@ -82,10 +79,7 @@ export class DatabaseManager { getClient(): Promise { return _this.getDatabase(pluginId); }, - runMigrations: - typeof _this.runMigrations === 'function' - ? _this.runMigrations() - : _this.runMigrations, + runMigrations: _this.runMigrations, }; } From 70c46a708b626eb6b81a44280a59c22ed1a92fbc Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 2 Dec 2021 13:05:31 +0100 Subject: [PATCH 005/116] Add options with migrations category Signed-off-by: Marcus Eide --- .../src/database/DatabaseManager.test.ts | 12 +++++++----- .../src/database/DatabaseManager.ts | 17 ++++++++++++----- packages/backend-common/src/database/types.ts | 17 ++++++++++++----- .../src/service/NextCatalogBuilder.ts | 2 +- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/packages/backend-common/src/database/DatabaseManager.test.ts b/packages/backend-common/src/database/DatabaseManager.test.ts index 895afb4af4..2905775494 100644 --- a/packages/backend-common/src/database/DatabaseManager.test.ts +++ b/packages/backend-common/src/database/DatabaseManager.test.ts @@ -58,20 +58,22 @@ describe('DatabaseManager', () => { expect(getConfigSpy).toHaveBeenCalledWith('backend.database'); }); - it('runMigrations defaults to true', () => { + it('handles default options', () => { const config = new ConfigReader(backendConfig); const database = DatabaseManager.fromConfig(config); const client = database.forPlugin('test'); - expect(client.runMigrations).toBe(true); + expect(client.migrations?.apply).toBe(true); }); - it('runMigrations can be set', () => { + it('handles migrations options', () => { const config = new ConfigReader(backendConfig); - const database = DatabaseManager.fromConfig(config, false); + const database = DatabaseManager.fromConfig(config, { + migrations: { apply: false }, + }); const client = database.forPlugin('test'); - expect(client.runMigrations).toBe(false); + expect(client.migrations?.apply).toBe(false); }); }); diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index d5dbd90b79..2c76c0f163 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -36,6 +36,10 @@ function pluginPath(pluginId: string): string { return `plugin.${pluginId}`; } +type Options = { + migrations?: PluginDatabaseManager['migrations']; +}; + /** @public */ export class DatabaseManager { /** @@ -47,22 +51,22 @@ export class DatabaseManager { * names if config is not provided. * * @param config - The loaded application configuration. - * @param runMigrations - Controls whether or not to perform database migrations. + * @param options - An optional configuration object. */ - static fromConfig(config: Config, runMigrations?: boolean): DatabaseManager { + static fromConfig(config: Config, options?: Options): DatabaseManager { const databaseConfig = config.getConfig('backend.database'); return new DatabaseManager( databaseConfig, databaseConfig.getOptionalString('prefix'), - runMigrations, + options, ); } private constructor( private readonly config: Config, private readonly prefix: string = 'backstage_plugin_', - private readonly runMigrations: boolean = true, + private readonly options?: Options, ) {} /** @@ -74,12 +78,15 @@ export class DatabaseManager { */ forPlugin(pluginId: string): PluginDatabaseManager { const _this = this; + const defaultMigrationOptions = { + apply: true, + }; return { getClient(): Promise { return _this.getDatabase(pluginId); }, - runMigrations: _this.runMigrations, + migrations: _this.options?.migrations ?? defaultMigrationOptions, }; } diff --git a/packages/backend-common/src/database/types.ts b/packages/backend-common/src/database/types.ts index 3dad57ff62..3c5bbf19bb 100644 --- a/packages/backend-common/src/database/types.ts +++ b/packages/backend-common/src/database/types.ts @@ -32,12 +32,19 @@ export interface PluginDatabaseManager { getClient(): Promise; /** - * runMigrations can be used to determine if database migrations - * should be performed. - * - * Useful if connecting to a read-only database. + * This optional property is used to control the behavior of database migrations. */ - runMigrations: boolean; + migrations?: { + /** + * apply can be used to determine if database migrations + * should be performed. + * + * Useful if connecting to a read-only database. + * + * @default true + */ + apply: boolean; + }; } /** diff --git a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts index d85fd741a5..712ab0ae44 100644 --- a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts @@ -336,7 +336,7 @@ export class NextCatalogBuilder { const parser = this.parser || defaultEntityDataParser; const dbClient = await database.getClient(); - if (database.runMigrations) { + if (database.migrations?.apply) { logger.info('Performing database migration'); await applyDatabaseMigrations(dbClient); } From 6304c8f94714b6fecb6759d831d5c1d2214b5aa2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 24 Nov 2021 11:59:59 +0100 Subject: [PATCH 006/116] core-plugin-api: Refactor IdentityApi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Johan Haals Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- .../IdentityApi/AppIdentityProxy.ts | 90 ++++++++++++++++++ .../IdentityApi/GuestUserIdentity.ts | 60 ++++++++++++ .../IdentityApi/LegacyUserIdentity.ts | 76 ++++++++++++++++ packages/core-app-api/src/app/AppIdentity.ts | 85 ----------------- packages/core-app-api/src/app/AppManager.tsx | 89 +++++++++++++++--- packages/core-app-api/src/app/types.ts | 8 ++ .../src/layout/SignInPage/SignInPage.tsx | 38 ++++---- .../src/layout/SignInPage/UserIdentity.ts | 91 +++++++++++++++++++ .../src/apis/definitions/IdentityApi.ts | 64 +++++++++++-- .../src/apis/definitions/auth.ts | 46 ++++++++-- plugins/auth-backend/src/providers/types.ts | 45 ++++++--- 11 files changed, 543 insertions(+), 149 deletions(-) create mode 100644 packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts create mode 100644 packages/core-app-api/src/apis/implementations/IdentityApi/GuestUserIdentity.ts create mode 100644 packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts delete mode 100644 packages/core-app-api/src/app/AppIdentity.ts create mode 100644 packages/core-components/src/layout/SignInPage/UserIdentity.ts diff --git a/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts b/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts new file mode 100644 index 0000000000..8a2f12594c --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts @@ -0,0 +1,90 @@ +/* + * 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 { + IdentityApi, + ProfileInfo, + BackstageUserIdentity, +} from '@backstage/core-plugin-api'; + +function mkError(thing: string) { + return new Error( + `Tried to access IdentityApi ${thing} before app was loaded`, + ); +} + +/** + * Implementation of the connection between the App-wide IdentityApi + * and sign-in page. + */ +export class AppIdentityProxy implements IdentityApi { + private target?: IdentityApi; + + // This is called by the app manager once the sign-in page provides us with an implementation + setTarget(identityApi: IdentityApi) { + this.target = identityApi; + } + + getUserId(): string { + if (!this.target) { + throw mkError('getUserId'); + } + return this.target.getUserId(); + } + + getProfile(): ProfileInfo { + if (!this.target) { + throw mkError('getProfile'); + } + return this.target.getProfile(); + } + + async getProfileInfo(): Promise { + if (!this.target) { + throw mkError('getProfileInfo'); + } + return this.target.getProfileInfo(); + } + + async getBackstageIdentity(): Promise { + if (!this.target) { + throw mkError('getBackstageIdentity'); + } + return this.target.getBackstageIdentity(); + } + + async getCredentials(): Promise<{ token?: string | undefined }> { + if (!this.target) { + throw mkError('getCredentials'); + } + return this.target.getCredentials(); + } + + async getIdToken(): Promise { + if (!this.target) { + throw mkError('getIdToken'); + } + return this.target.getIdToken(); + } + + async signOut(): Promise { + if (!this.target) { + throw mkError('signOut'); + } + await this.target.signOut(); + location.reload(); + } +} diff --git a/packages/core-app-api/src/apis/implementations/IdentityApi/GuestUserIdentity.ts b/packages/core-app-api/src/apis/implementations/IdentityApi/GuestUserIdentity.ts new file mode 100644 index 0000000000..db4731704c --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/IdentityApi/GuestUserIdentity.ts @@ -0,0 +1,60 @@ +/* + * 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 { + IdentityApi, + ProfileInfo, + BackstageUserIdentity, +} from '@backstage/core-plugin-api'; + +export class GuestUserIdentity implements IdentityApi { + getUserId(): string { + return 'guest'; + } + + async getIdToken(): Promise { + return undefined; + } + + getProfile(): ProfileInfo { + return { + email: 'guest@example.com', + displayName: 'Guest', + }; + } + + async getProfileInfo(): Promise { + return { + email: 'guest@example.com', + displayName: 'Guest', + }; + } + + async getBackstageIdentity(): Promise { + const userEntityRef = `user:default/guest`; + return { + type: 'user', + userEntityRef, + ownershipEntityRefs: [userEntityRef], + }; + } + + async getCredentials(): Promise<{ token?: string | undefined }> { + return {}; + } + + async signOut(): Promise {} +} diff --git a/packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts b/packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts new file mode 100644 index 0000000000..5c74472cdb --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts @@ -0,0 +1,76 @@ +/* + * 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 { + IdentityApi, + ProfileInfo, + BackstageUserIdentity, + SignInResult, +} from '@backstage/core-plugin-api'; + +function parseJwtPayload(token: string) { + const [_header, payload, _signature] = token.split('.'); + return JSON.parse(atob(payload)); +} + +export class LegacyUserIdentity implements IdentityApi { + constructor(private readonly result: SignInResult) {} + + getUserId(): string { + return this.result.userId; + } + + async getIdToken(): Promise { + return this.result.getIdToken?.(); + } + + getProfile(): ProfileInfo { + return this.result.profile; + } + + async getProfileInfo(): Promise { + return this.result.profile; + } + + async getBackstageIdentity(): Promise { + const token = await this.getIdToken(); + + if (!token) { + const userEntityRef = `user:default/${this.getUserId()}`; + return { + type: 'user', + userEntityRef, + ownershipEntityRefs: [userEntityRef], + }; + } + + const { sub, ent } = parseJwtPayload(token); + return { + type: 'user', + userEntityRef: sub, + ownershipEntityRefs: ent ?? [sub], + }; + } + + async getCredentials(): Promise<{ token?: string | undefined }> { + const token = await this.result.getIdToken?.(); + return { token }; + } + + async signOut(): Promise { + return this.result.signOut?.(); + } +} diff --git a/packages/core-app-api/src/app/AppIdentity.ts b/packages/core-app-api/src/app/AppIdentity.ts deleted file mode 100644 index 64e698102f..0000000000 --- a/packages/core-app-api/src/app/AppIdentity.ts +++ /dev/null @@ -1,85 +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 { IdentityApi, ProfileInfo } from '@backstage/core-plugin-api'; -import { SignInResult } from './types'; - -/** - * Implementation of the connection between the App-wide IdentityApi - * and sign-in page. - */ -export class AppIdentity implements IdentityApi { - private hasIdentity = false; - private userId?: string; - private profile?: ProfileInfo; - private idTokenFunc?: () => Promise; - private signOutFunc?: () => Promise; - - getUserId(): string { - if (!this.hasIdentity) { - throw new Error( - 'Tried to access IdentityApi userId before app was loaded', - ); - } - return this.userId!; - } - - getProfile(): ProfileInfo { - if (!this.hasIdentity) { - throw new Error( - 'Tried to access IdentityApi profile before app was loaded', - ); - } - return this.profile!; - } - - async getIdToken(): Promise { - if (!this.hasIdentity) { - throw new Error( - 'Tried to access IdentityApi idToken before app was loaded', - ); - } - return this.idTokenFunc?.(); - } - - async signOut(): Promise { - if (!this.hasIdentity) { - throw new Error( - 'Tried to access IdentityApi signOutFunc before app was loaded', - ); - } - await this.signOutFunc?.(); - location.reload(); - } - - // This is indirectly called by the sign-in page to continue into the app. - setSignInResult(result: SignInResult) { - if (this.hasIdentity) { - return; - } - if (!result.userId) { - throw new Error('Invalid sign-in result, userId not set'); - } - if (!result.profile) { - throw new Error('Invalid sign-in result, profile not set'); - } - this.hasIdentity = true; - this.userId = result.userId; - this.profile = result.profile; - this.idTokenFunc = result.getIdToken; - this.signOutFunc = result.signOut; - } -} diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 5903a8c874..13ac8b4177 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -43,12 +43,15 @@ import { AppThemeApi, ConfigApi, featureFlagsApiRef, + IdentityApi, identityApiRef, BackstagePlugin, RouteRef, SubRouteRef, ExternalRouteRef, } from '@backstage/core-plugin-api'; +import { GuestUserIdentity } from '../apis/implementations/IdentityApi/GuestUserIdentity'; +import { LegacyUserIdentity } from '../apis/implementations/IdentityApi/LegacyUserIdentity'; import { ApiFactoryRegistry, ApiResolver } from '../apis/system'; import { childDiscoverer, @@ -66,7 +69,7 @@ import { RoutingProvider } from '../routing/RoutingProvider'; import { RouteTracker } from '../routing/RouteTracker'; import { validateRoutes } from '../routing/validation'; import { AppContextProvider } from './AppContext'; -import { AppIdentity } from './AppIdentity'; +import { AppIdentityProxy } from '../apis/implementations/IdentityApi/AppIdentityProxy'; import { AppComponents, AppConfigLoader, @@ -189,7 +192,7 @@ export class AppManager implements BackstageApp { private readonly defaultApis: Iterable; private readonly bindRoutes: AppOptions['bindRoutes']; - private readonly identityApi = new AppIdentity(); + private readonly appIdentityProxy = new AppIdentityProxy(); private readonly apiFactoryRegistry: ApiFactoryRegistry; constructor(options: AppOptions) { @@ -344,14 +347,23 @@ export class AppManager implements BackstageApp { component: ComponentType; children: ReactElement; }) => { - const [result, setResult] = useState(); + const [identityApi, setIdentityApi] = useState(); - if (result) { - this.identityApi.setSignInResult(result); - return children; + const setLegacyResult = (result: SignInResult) => { + setIdentityApi(new LegacyUserIdentity(result)); + }; + + if (!identityApi) { + return ( + + ); } - return ; + this.appIdentityProxy.setTarget(identityApi); + return children; }; const AppRouter = ({ children }: PropsWithChildren<{}>) => { @@ -360,13 +372,7 @@ export class AppManager implements BackstageApp { // If the app hasn't configured a sign-in page, we just continue as guest. if (!SignInPageComponent) { - this.identityApi.setSignInResult({ - userId: 'guest', - profile: { - email: 'guest@example.com', - displayName: 'Guest', - }, - }); + this.appIdentityProxy.setTarget(new GuestUserIdentity()); return ( @@ -430,7 +436,7 @@ export class AppManager implements BackstageApp { this.apiFactoryRegistry.register('static', { api: identityApiRef, deps: {}, - factory: () => this.identityApi, + factory: () => this.appIdentityProxy, }); // It's possible to replace the feature flag API, but since we must have at least @@ -485,3 +491,56 @@ export class AppManager implements BackstageApp { } } } + +interface FooPropsV1 { + foo: () => undefined; +} + +interface FooPropsV2 { + foo: () => undefined; + bar: () => undefined; +} + +// type FooProps = { +// foo: () => undefined +// } | { +// foo: () => undefined +// bar: () => undefined +// } + +interface CreateDerpOptions { + components: { + Foo: (props: FooPropsV1 | FooPropsV2) => JSX.Element; + }; +} + +interface Derp { + getComponents(): { + Foo: (props: FooPropsV1) => JSX.Element; + }; +} + +function createDerp(options: CreateDerpOptions): Derp { + return { getComponents: () => options.components }; +} + +function CustomFoo(props: FooPropsV1) { + return
{props.foo()}
; +} + +function NewCustomFoo(props: FooPropsV2) { + return ( +
+ {props.foo()} {props.bar()} +
+ ); +} + +const derp = createDerp({ + components: { + Foo: NewCustomFoo, + }, +}); + +const { Foo } = derp.getComponents(); +const _foo = undefined} />; diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index f16c8ac656..9e89bc0b56 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -25,6 +25,7 @@ import { SubRouteRef, ExternalRouteRef, PluginOutput, + IdentityApi, } from '@backstage/core-plugin-api'; import { AppConfig } from '@backstage/config'; @@ -42,6 +43,7 @@ export type BootErrorPageProps = { * The outcome of signing in on the sign-in page. * * @public + * @deprecated replaced by passing the {@link IdentityApi} to the {@link SignInPageProps.onSignInSuccess} instead. */ export type SignInResult = { /** @@ -70,8 +72,14 @@ export type SignInResult = { export type SignInPageProps = { /** * Set the sign-in result for the app. This should only be called once. + * @deprecated use {@link SignInPageProps.onSignInSuccess} instead. */ onResult(result: SignInResult): void; + + /** + * Set the IdentityApi on successful sign in. This should only be called once. + */ + onSignInSuccess(identityApi: IdentityApi): void; }; /** diff --git a/packages/core-components/src/layout/SignInPage/SignInPage.tsx b/packages/core-components/src/layout/SignInPage/SignInPage.tsx index bd4fe37480..be2287f705 100644 --- a/packages/core-components/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core-components/src/layout/SignInPage/SignInPage.tsx @@ -15,11 +15,12 @@ */ import { - BackstageIdentity, + BackstageIdentityResponse, configApiRef, SignInPageProps, useApi, } from '@backstage/core-plugin-api'; +import { UserIdentity } from './UserIdentity'; import Button from '@material-ui/core/Button'; import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; @@ -87,7 +88,7 @@ export const MultiSignInPage = ({ }; export const SingleSignInPage = ({ - onResult, + onSignInSuccess, provider, auto, }: SingleSignInPageProps) => { @@ -105,54 +106,47 @@ export const SingleSignInPage = ({ type LoginOpts = { checkExisting?: boolean; showPopup?: boolean }; const login = async ({ checkExisting, showPopup }: LoginOpts) => { try { - let identity: BackstageIdentity | undefined; + let identityResponse: BackstageIdentityResponse | undefined; if (checkExisting) { // Do an initial check if any logged-in session exists - identity = await authApi.getBackstageIdentity({ + identityResponse = await authApi.getBackstageIdentity({ optional: true, }); } // If no session exists, show the sign-in page - if (!identity && (showPopup || auto)) { + if (!identityResponse && (showPopup || auto)) { // Unless auto is set to true, this step should not happen. // When user intentionally clicks the Sign In button, autoShowPopup is set to true setShowLoginPage(true); - identity = await authApi.getBackstageIdentity({ + identityResponse = await authApi.getBackstageIdentity({ instantPopup: true, }); - if (!identity) { + if (!identityResponse) { throw new Error( `The ${provider.title} provider is not configured to support sign-in`, ); } } - if (!identity) { + if (!identityResponse) { setShowLoginPage(true); return; } - const profile = await authApi.getProfile(); - onResult({ - userId: identity!.id, - profile: profile!, - getIdToken: () => { - return authApi - .getBackstageIdentity() - .then(i => i!.token ?? i!.idToken); - }, - signOut: async () => { - await authApi.signOut(); - }, - }); + onSignInSuccess( + UserIdentity.from({ + identity: identityResponse.identity, + authApi, + profile, + }), + ); } catch (err: any) { // User closed the sign-in modal setError(err); setShowLoginPage(true); } }; - useMount(() => login({ checkExisting: true })); return showLoginPage ? ( diff --git a/packages/core-components/src/layout/SignInPage/UserIdentity.ts b/packages/core-components/src/layout/SignInPage/UserIdentity.ts new file mode 100644 index 0000000000..daf9111d84 --- /dev/null +++ b/packages/core-components/src/layout/SignInPage/UserIdentity.ts @@ -0,0 +1,91 @@ +/* + * 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 { + IdentityApi, + ProfileInfo, + ProfileInfoApi, + BackstageUserIdentity, + BackstageIdentityApi, + SessionApi, +} from '@backstage/core-plugin-api'; + +export class UserIdentity implements IdentityApi { + static from(options: { + identity: BackstageUserIdentity; + authApi: ProfileInfoApi & BackstageIdentityApi & SessionApi; + /** + * Passing a profile synchronously allows the deprecated `getProfile` method to be + * called by consumers of the {@link IdentityApi}. If you do not have any consumers + * of that method than this is safe to leave out. + * + * @deprecated Only provide this if you have plugins that call the synchronous `getProfile` method, which is also deprecated. + */ + profile?: ProfileInfo; + }) { + return new UserIdentity(options.identity, options.authApi, options.profile); + } + + private constructor( + private readonly identity: BackstageUserIdentity, + private readonly authApi: ProfileInfoApi & + BackstageIdentityApi & + SessionApi, + private readonly profile?: ProfileInfo, + ) {} + + getUserId(): string { + const ref = this.identity.userEntityRef; + const match = /^([^:/]+:)?([^:/]+\/)?([^:/]+)$/.exec(ref); + if (!match) { + throw new TypeError(`Invalid user entity reference "${ref}"`); + } + + return match[3]; + } + + async getIdToken(): Promise { + const identity = await this.authApi.getBackstageIdentity(); + return identity!.token; + } + + getProfile(): ProfileInfo { + if (!this.profile) { + throw new Error( + 'The identity API does not implement synchronous profile fetching, use getProfileInfo() instead', + ); + } + return this.profile; + } + + async getProfileInfo(): Promise { + const profile = await this.authApi.getProfile(); + return profile!; + } + + async getBackstageIdentity(): Promise { + return this.identity; + } + + async getCredentials(): Promise<{ token?: string | undefined }> { + const identity = await this.authApi.getBackstageIdentity(); + return { token: identity!.token }; + } + + async signOut(): Promise { + return this.authApi.signOut(); + } +} diff --git a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts index 9f68a8b2cc..38376491ca 100644 --- a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts @@ -16,6 +16,34 @@ import { ApiRef, createApiRef } from '../system'; import { ProfileInfo } from './auth'; +/* + +- [ ] IdentityApi getProfile, make async +- [ ] BackstageIdentity (settle or remove) +- [ ] Evolution plan for utility APIs + +*/ + +/** + * User identity information within Backstage. + * + * @public + */ +export type BackstageUserIdentity = { + type: 'user'; + + /** + * The entityRef of the user in the catalog. + * For example User:default/sandra + */ + userEntityRef: string; + + /** + * The user and group entities that the user claims ownership through + */ + ownershipEntityRefs: string[]; +}; + /** * The Identity API used to identify and get information about the signed in user. * @@ -26,25 +54,45 @@ export type IdentityApi = { * The ID of the signed in user. This ID is not meant to be presented to the user, but used * as an opaque string to pass on to backends or use in frontend logic. * - * TODO: The intention of the user ID is to be able to tie the user to an identity - * that is known by the catalog and/or identity backend. It should for example - * be possible to fetch all owned components using this ID. + * @deprecated use {@link IdentityApi.getIdentity} instead. */ getUserId(): string; - /** - * The profile of the signed in user. - */ - getProfile(): ProfileInfo; - /** * An OpenID Connect ID Token which proves the identity of the signed in user. * * The ID token will be undefined if the signed in user does not have a verified * identity, such as a demo user or mocked user for e2e tests. + * + * @deprecated use {@link IdentityApi.getCredentials} instead. */ getIdToken(): Promise; + /** + * The profile of the signed in user. + * + * @deprecated use {@link IdentityApi.getProfileInfo} instead. + */ + getProfile(): ProfileInfo; + + /** + * The profile of the signed in user. + */ + getProfileInfo(): Promise; + + /** + * User identity information within Backstage. + */ + getBackstageIdentity(): Promise; + + /** + * Provides credentials in the form of a token which proves the identity of the signed in user. + * + * The token will be undefined if the signed in user does not have a verified + * identity, such as a demo user or mocked user for e2e tests. + */ + getCredentials(): Promise<{ token?: string }>; + /** * Sign out the current user */ diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index 8fe532f3a6..8da7fbe6fd 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -160,7 +160,31 @@ export type BackstageIdentityApi = { */ getBackstageIdentity( options?: AuthRequestOptions, - ): Promise; + ): Promise; +}; + +/** + * User identity information within Backstage. + * + * @public + */ +export type BackstageUserIdentity = { + /** + * The type of identity that this structure represents. In the frontend app + * this will currently always be 'user'. + */ + type: 'user'; + + /** + * The entityRef of the user in the catalog. + * For example User:default/sandra + */ + userEntityRef: string; + + /** + * The user and group entities that the user claims ownership through + */ + ownershipEntityRefs: string[]; }; /** @@ -168,23 +192,31 @@ export type BackstageIdentityApi = { * * @public */ -export type BackstageIdentity = { +export type BackstageIdentityResponse = { /** * The backstage user ID. + * + * @deprecated The identity is now provided via the `identity` field instead. */ id: string; - /** - * @deprecated This is deprecated, use `token` instead. - */ - idToken: string; - /** * The token used to authenticate the user within Backstage. */ token: string; + + /** + * Identity information derived from the token. + */ + identity: BackstageUserIdentity; }; +/** + * @public + * @deprecated use {@link BackstageIdentityResponse} instead. + */ +export type BackstageIdentity = BackstageIdentityResponse; + /** * Profile information of the user. * diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 089c82b293..04da05a314 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -140,32 +140,51 @@ export type AuthResponse = { backstageIdentity?: BackstageIdentity; }; -export type BackstageIdentity = { +/** + * @public + */ +export type BackstageIdentityResponse = { /** * An opaque ID that uniquely identifies the user within Backstage. * * This is typically the same as the user entity `metadata.name`. + * + * @deprecated Use the `identity` field instead */ id: string; - /** - * This is deprecated, use `token` instead. - * @deprecated - */ - idToken?: string; - - /** - * The token used to authenticate the user within Backstage. - */ - token?: string; - /** * The entity that the user is represented by within Backstage. * * This entity may or may not exist within the Catalog, and it can be used * to read and store additional metadata about the user. + * + * @deprecated Use the `identity` field instead. */ entity?: Entity; + + /** + * The token used to authenticate the user within Backstage. + */ + token: string; + + /** + * A plaintext description of the identity that is encapsulated within the token. + */ + identity?: { + type: 'user'; + + /** + * The entityRef of the user in the catalog. + * For example User:default/sandra + */ + userEntityRef: string; + + /** + * The user and group entities that the user claims ownership through + */ + ownershipEntityRefs: string[]; + }; }; /** @@ -173,6 +192,8 @@ export type BackstageIdentity = { * * It is also temporarily used as the profile of the signed-in user's Backstage * identity, but we want to replace that with data from identity and/org catalog service + * + * @public */ export type ProfileInfo = { /** From 32b04436608362c868a6fc9736f9124de5ec9b39 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 26 Nov 2021 16:05:32 +0100 Subject: [PATCH 007/116] core-plugin-api: Use LegacyUserIdentity helper Co-authored-by: blam Signed-off-by: Johan Haals --- .../IdentityApi/LegacyUserIdentity.ts | 6 +- packages/core-app-api/src/app/AppManager.tsx | 101 ++++++++---------- packages/core-app-api/src/app/types.ts | 6 -- .../src/layout/SignInPage/SignInPage.tsx | 8 +- .../src/layout/SignInPage/auth0Provider.tsx | 42 ++++---- .../src/layout/SignInPage/commonProvider.tsx | 45 ++++---- .../src/layout/SignInPage/customProvider.tsx | 19 ++-- .../src/layout/SignInPage/guestProvider.tsx | 15 +-- .../src/layout/SignInPage/providers.tsx | 27 +++-- .../src/layout/SignInPage/types.ts | 4 +- plugins/auth-backend/src/index.ts | 2 +- plugins/auth-backend/src/providers/types.ts | 4 +- 12 files changed, 129 insertions(+), 150 deletions(-) diff --git a/packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts b/packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts index 5c74472cdb..ff6d38c860 100644 --- a/packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts +++ b/packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts @@ -27,12 +27,16 @@ function parseJwtPayload(token: string) { } export class LegacyUserIdentity implements IdentityApi { - constructor(private readonly result: SignInResult) {} + private constructor(private readonly result: SignInResult) {} getUserId(): string { return this.result.userId; } + static fromResult(result: SignInResult): LegacyUserIdentity { + return new LegacyUserIdentity(result); + } + async getIdToken(): Promise { return this.result.getIdToken?.(); } diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 13ac8b4177..31e093a145 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -51,7 +51,6 @@ import { ExternalRouteRef, } from '@backstage/core-plugin-api'; import { GuestUserIdentity } from '../apis/implementations/IdentityApi/GuestUserIdentity'; -import { LegacyUserIdentity } from '../apis/implementations/IdentityApi/LegacyUserIdentity'; import { ApiFactoryRegistry, ApiResolver } from '../apis/system'; import { childDiscoverer, @@ -78,7 +77,6 @@ import { AppRouteBinder, BackstageApp, SignInPageProps, - SignInResult, } from './types'; import { AppThemeProvider } from './AppThemeProvider'; import { defaultConfigLoader } from './defaultConfigLoader'; @@ -349,17 +347,8 @@ export class AppManager implements BackstageApp { }) => { const [identityApi, setIdentityApi] = useState(); - const setLegacyResult = (result: SignInResult) => { - setIdentityApi(new LegacyUserIdentity(result)); - }; - if (!identityApi) { - return ( - - ); + return ; } this.appIdentityProxy.setTarget(identityApi); @@ -492,55 +481,55 @@ export class AppManager implements BackstageApp { } } -interface FooPropsV1 { - foo: () => undefined; -} - -interface FooPropsV2 { - foo: () => undefined; - bar: () => undefined; -} - -// type FooProps = { -// foo: () => undefined -// } | { -// foo: () => undefined -// bar: () => undefined +// interface FooPropsV1 { +// foo: () => undefined; // } -interface CreateDerpOptions { - components: { - Foo: (props: FooPropsV1 | FooPropsV2) => JSX.Element; - }; -} +// interface FooPropsV2 { +// foo: () => undefined; +// bar: () => undefined; +// } -interface Derp { - getComponents(): { - Foo: (props: FooPropsV1) => JSX.Element; - }; -} +// // type FooProps = { +// // foo: () => undefined +// // } | { +// // foo: () => undefined +// // bar: () => undefined +// // } -function createDerp(options: CreateDerpOptions): Derp { - return { getComponents: () => options.components }; -} +// interface CreateDerpOptions { +// components: { +// Foo: (props: FooPropsV1 | FooPropsV2) => JSX.Element; +// }; +// } -function CustomFoo(props: FooPropsV1) { - return
{props.foo()}
; -} +// interface Derp { +// getComponents(): { +// Foo: (props: FooPropsV1) => JSX.Element; +// }; +// } -function NewCustomFoo(props: FooPropsV2) { - return ( -
- {props.foo()} {props.bar()} -
- ); -} +// function createDerp(options: CreateDerpOptions): Derp { +// return { getComponents: () => options.components }; +// } -const derp = createDerp({ - components: { - Foo: NewCustomFoo, - }, -}); +// function CustomFoo(props: FooPropsV1) { +// return
{props.foo()}
; +// } -const { Foo } = derp.getComponents(); -const _foo = undefined} />; +// function NewCustomFoo(props: FooPropsV2) { +// return ( +//
+// {props.foo()} {props.bar()} +//
+// ); +// } + +// const derp = createDerp({ +// components: { +// Foo: NewCustomFoo, +// }, +// }); + +// const { Foo } = derp.getComponents(); +// const _foo = undefined} />; diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 9e89bc0b56..0921bd2e17 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -70,12 +70,6 @@ export type SignInResult = { * @public */ export type SignInPageProps = { - /** - * Set the sign-in result for the app. This should only be called once. - * @deprecated use {@link SignInPageProps.onSignInSuccess} instead. - */ - onResult(result: SignInResult): void; - /** * Set the IdentityApi on successful sign in. This should only be called once. */ diff --git a/packages/core-components/src/layout/SignInPage/SignInPage.tsx b/packages/core-components/src/layout/SignInPage/SignInPage.tsx index be2287f705..d0704c2b85 100644 --- a/packages/core-components/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core-components/src/layout/SignInPage/SignInPage.tsx @@ -50,7 +50,7 @@ type SingleSignInPageProps = SignInPageProps & { export type Props = MultiSignInPageProps | SingleSignInPageProps; export const MultiSignInPage = ({ - onResult, + onSignInSuccess, providers = [], title, align = 'left', @@ -61,7 +61,7 @@ export const MultiSignInPage = ({ const signInProviders = getSignInProviders(providers); const [loading, providerElements] = useSignInProviders( signInProviders, - onResult, + onSignInSuccess, ); if (loading) { @@ -88,9 +88,9 @@ export const MultiSignInPage = ({ }; export const SingleSignInPage = ({ - onSignInSuccess, provider, auto, + onSignInSuccess, }: SingleSignInPageProps) => { const classes = useStyles(); const authApi = useApi(provider.apiRef); @@ -133,7 +133,9 @@ export const SingleSignInPage = ({ setShowLoginPage(true); return; } + const profile = await authApi.getProfile(); + onSignInSuccess( UserIdentity.from({ identity: identityResponse.identity, diff --git a/packages/core-components/src/layout/SignInPage/auth0Provider.tsx b/packages/core-components/src/layout/SignInPage/auth0Provider.tsx index e1372a2362..8e2728a95e 100644 --- a/packages/core-components/src/layout/SignInPage/auth0Provider.tsx +++ b/packages/core-components/src/layout/SignInPage/auth0Provider.tsx @@ -26,17 +26,18 @@ import { errorApiRef, } from '@backstage/core-plugin-api'; import { ForwardedError } from '@backstage/errors'; +import { UserIdentity } from './UserIdentity'; -const Component: ProviderComponent = ({ onResult }) => { +const Component: ProviderComponent = ({ onSignInSuccess }) => { const auth0AuthApi = useApi(auth0AuthApiRef); const errorApi = useApi(errorApiRef); const handleLogin = async () => { try { - const identity = await auth0AuthApi.getBackstageIdentity({ + const identityResponse = await auth0AuthApi.getBackstageIdentity({ instantPopup: true, }); - if (!identity) { + if (!identityResponse) { throw new Error( 'The Auth0 provider is not configured to support sign-in', ); @@ -44,15 +45,13 @@ const Component: ProviderComponent = ({ onResult }) => { const profile = await auth0AuthApi.getProfile(); - onResult({ - userId: identity!.id, - profile: profile!, - getIdToken: () => - auth0AuthApi.getBackstageIdentity().then(i => i!.token ?? i!.idToken), - signOut: async () => { - await auth0AuthApi.signOut(); - }, - }); + onSignInSuccess( + UserIdentity.from({ + identity: identityResponse.identity, + authApi: auth0AuthApi, + profile, + }), + ); } catch (error) { errorApi.post(new ForwardedError('Auth0 login failed', error)); } @@ -77,25 +76,20 @@ const Component: ProviderComponent = ({ onResult }) => { const loader: ProviderLoader = async apis => { const auth0AuthApi = apis.get(auth0AuthApiRef)!; - const identity = await auth0AuthApi.getBackstageIdentity({ + const identityResponse = await auth0AuthApi.getBackstageIdentity({ optional: true, }); - if (!identity) { + if (!identityResponse) { return undefined; } const profile = await auth0AuthApi.getProfile(); - - return { - userId: identity.id, - profile: profile!, - getIdToken: () => - auth0AuthApi.getBackstageIdentity().then(i => i!.token ?? i!.idToken), - signOut: async () => { - await auth0AuthApi.signOut(); - }, - }; + return UserIdentity.from({ + identity: identityResponse.identity, + authApi: auth0AuthApi, + profile, + }); }; export const auth0Provider: SignInProvider = { Component, loader }; diff --git a/packages/core-components/src/layout/SignInPage/commonProvider.tsx b/packages/core-components/src/layout/SignInPage/commonProvider.tsx index 515ae01e4d..48dd7df62c 100644 --- a/packages/core-components/src/layout/SignInPage/commonProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/commonProvider.tsx @@ -27,36 +27,33 @@ import { import { useApi, errorApiRef } from '@backstage/core-plugin-api'; import { GridItem } from './styles'; import { ForwardedError } from '@backstage/errors'; +import { UserIdentity } from './UserIdentity'; -const Component: ProviderComponent = ({ config, onResult }) => { +const Component: ProviderComponent = ({ config, onSignInSuccess }) => { const { apiRef, title, message } = config as SignInProviderConfig; const authApi = useApi(apiRef); const errorApi = useApi(errorApiRef); const handleLogin = async () => { try { - const identity = await authApi.getBackstageIdentity({ + const identityResponse = await authApi.getBackstageIdentity({ instantPopup: true, }); - if (!identity) { + if (!identityResponse) { throw new Error( `The ${title} provider is not configured to support sign-in`, ); } const profile = await authApi.getProfile(); - onResult({ - userId: identity!.id, - profile: profile!, - getIdToken: () => { - return authApi - .getBackstageIdentity() - .then(i => i!.token ?? i!.idToken); - }, - signOut: async () => { - await authApi.signOut(); - }, - }); + + onSignInSuccess( + UserIdentity.from({ + identity: identityResponse.identity, + profile, + authApi, + }), + ); } catch (error) { errorApi.post(new ForwardedError('Login failed', error)); } @@ -82,25 +79,21 @@ const Component: ProviderComponent = ({ config, onResult }) => { const loader: ProviderLoader = async (apis, apiRef) => { const authApi = apis.get(apiRef)!; - const identity = await authApi.getBackstageIdentity({ + const identityResponse = await authApi.getBackstageIdentity({ optional: true, }); - if (!identity) { + if (!identityResponse) { return undefined; } const profile = await authApi.getProfile(); - return { - userId: identity.id, - profile: profile!, - getIdToken: () => - authApi.getBackstageIdentity().then(i => i!.token ?? i!.idToken), - signOut: async () => { - await authApi.signOut(); - }, - }; + return UserIdentity.from({ + identity: identityResponse.identity, + profile, + authApi, + }); }; export const commonProvider: SignInProvider = { Component, loader }; diff --git a/packages/core-components/src/layout/SignInPage/customProvider.tsx b/packages/core-components/src/layout/SignInPage/customProvider.tsx index da1848ac05..77bde5223c 100644 --- a/packages/core-components/src/layout/SignInPage/customProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/customProvider.tsx @@ -26,6 +26,7 @@ import isEmpty from 'lodash/isEmpty'; import { InfoCard } from '../InfoCard/InfoCard'; import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; import { GridItem } from './styles'; +import { LegacyUserIdentity } from '@backstage/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity'; // accept base64url format according to RFC7515 (https://tools.ietf.org/html/rfc7515#section-3) const ID_TOKEN_REGEX = /^[a-z0-9_\-]+\.[a-z0-9_\-]+\.[a-z0-9_\-]+$/i; @@ -60,7 +61,7 @@ const asInputRef = (renderResult: UseFormRegisterReturn) => { }; }; -const Component: ProviderComponent = ({ onResult }) => { +const Component: ProviderComponent = ({ onSignInSuccess }) => { const classes = useFormStyles(); const { register, handleSubmit, formState } = useForm({ mode: 'onChange', @@ -69,13 +70,15 @@ const Component: ProviderComponent = ({ onResult }) => { const { errors } = formState; const handleResult = ({ userId, idToken }: Data) => { - onResult({ - userId, - profile: { - email: `${userId}@example.com`, - }, - getIdToken: idToken ? async () => idToken : undefined, - }); + onSignInSuccess( + LegacyUserIdentity.fromResult({ + userId, + profile: { + email: `${userId}@example.com`, + }, + getIdToken: idToken ? async () => idToken : undefined, + }), + ); }; return ( diff --git a/packages/core-components/src/layout/SignInPage/guestProvider.tsx b/packages/core-components/src/layout/SignInPage/guestProvider.tsx index e91d9cbc75..9e0326c7df 100644 --- a/packages/core-components/src/layout/SignInPage/guestProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/guestProvider.tsx @@ -20,16 +20,9 @@ import Button from '@material-ui/core/Button'; import { InfoCard } from '../InfoCard/InfoCard'; import { GridItem } from './styles'; import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; +import { GuestUserIdentity } from '@backstage/core-app-api/src/apis/implementations/IdentityApi/GuestUserIdentity'; -const result = { - userId: 'guest', - profile: { - email: 'guest@example.com', - displayName: 'Guest', - }, -}; - -const Component: ProviderComponent = ({ onResult }) => ( +const Component: ProviderComponent = ({ onSignInSuccess }) => ( ( @@ -56,7 +49,7 @@ const Component: ProviderComponent = ({ onResult }) => ( ); const loader: ProviderLoader = async () => { - return result; + return new GuestUserIdentity(); }; export const guestProvider: SignInProvider = { Component, loader }; diff --git a/packages/core-components/src/layout/SignInPage/providers.tsx b/packages/core-components/src/layout/SignInPage/providers.tsx index 19915e6aa9..1ab7369dab 100644 --- a/packages/core-components/src/layout/SignInPage/providers.tsx +++ b/packages/core-components/src/layout/SignInPage/providers.tsx @@ -17,10 +17,10 @@ import React, { useLayoutEffect, useState, useMemo, useCallback } from 'react'; import { SignInPageProps, - SignInResult, useApi, useApiHolder, errorApiRef, + IdentityApi, } from '@backstage/core-plugin-api'; import { IdentityProviders, @@ -81,7 +81,7 @@ export function getSignInProviders( export const useSignInProviders = ( providers: SignInProviderType, - onResult: SignInPageProps['onResult'], + onSignInSuccess: SignInPageProps['onSignInSuccess'], ) => { const errorApi = useApi(errorApiRef); const apiHolder = useApiHolder(); @@ -89,16 +89,16 @@ export const useSignInProviders = ( // This decorates the result with sign out logic from this hook const handleWrappedResult = useCallback( - (result: SignInResult) => { - onResult({ - ...result, + (identityApi: IdentityApi) => { + onSignInSuccess({ + ...identityApi, signOut: async () => { localStorage.removeItem(PROVIDER_STORAGE_KEY); - await result.signOut?.(); + await identityApi.signOut?.(); }, }); }, - [onResult], + [onSignInSuccess], ); // In this effect we check if the user has already selected an existing login @@ -151,7 +151,14 @@ export const useSignInProviders = ( return () => { didCancel = true; }; - }, [loading, errorApi, onResult, apiHolder, providers, handleWrappedResult]); + }, [ + loading, + errorApi, + onSignInSuccess, + apiHolder, + providers, + handleWrappedResult, + ]); // This renders all available sign-in providers const elements = useMemo( @@ -161,7 +168,7 @@ export const useSignInProviders = ( const { Component } = provider.components; - const handleResult = (result: SignInResult) => { + const handleSignInSuccess = (result: IdentityApi) => { localStorage.setItem(PROVIDER_STORAGE_KEY, provider.id); handleWrappedResult(result); @@ -171,7 +178,7 @@ export const useSignInProviders = ( ); }), diff --git a/packages/core-components/src/layout/SignInPage/types.ts b/packages/core-components/src/layout/SignInPage/types.ts index 1e4e4a2997..5b458448bf 100644 --- a/packages/core-components/src/layout/SignInPage/types.ts +++ b/packages/core-components/src/layout/SignInPage/types.ts @@ -17,12 +17,12 @@ import { ComponentType } from 'react'; import { SignInPageProps, - SignInResult, ApiHolder, ApiRef, ProfileInfoApi, BackstageIdentityApi, SessionApi, + IdentityApi, } from '@backstage/core-plugin-api'; export type SignInProviderConfig = { @@ -41,7 +41,7 @@ export type ProviderComponent = ComponentType< export type ProviderLoader = ( apis: ApiHolder, apiRef: ApiRef, -) => Promise; +) => Promise; export type SignInProvider = { Component: ProviderComponent; diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index 894f8c1e55..66c1b42dd9 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -29,7 +29,7 @@ export * from './providers'; // ensuresXRequestedWith and postMessageResponse to safely handle CORS requests for login. The WebMessageResponse type in flow is used to type the response from the login-popup export * from './lib/flow'; -// OAuth wrapper over a passport or a custom `startegy`. +// OAuth wrapper over a passport or a custom `strategy`. export * from './lib/oauth'; export * from './lib/catalog'; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 04da05a314..52cff0bdf9 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -137,7 +137,7 @@ export type AuthProviderFactory = ( export type AuthResponse = { providerInfo: ProviderInfo; profile: ProfileInfo; - backstageIdentity?: BackstageIdentity; + backstageIdentity?: BackstageIdentityResponse; }; /** @@ -230,7 +230,7 @@ export type SignInResolver = ( catalogIdentityClient: CatalogIdentityClient; logger: Logger; }, -) => Promise; +) => Promise; export type AuthHandlerResult = { profile: ProfileInfo }; From e9471d274c244c13daa34250e242fa7ac3d164fe Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 30 Nov 2021 13:14:48 +0100 Subject: [PATCH 008/116] Use BackstageUserIdentity, fix tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: blam Co-authored-by: Fredrik Adelöw Co-authored-by: Patrik Oldsberg Signed-off-by: Johan Haals --- .../src/apis/definitions/IdentityApi.ts | 22 +---------- .../src/identity/IdentityClient.test.ts | 20 +++++++++- .../src/identity/IdentityClient.ts | 26 +++++++++---- .../src/lib/flow/authFlowHelpers.test.ts | 6 +-- .../src/lib/oauth/OAuthAdapter.test.ts | 1 + .../src/lib/oauth/OAuthAdapter.ts | 10 ++--- .../src/providers/auth0/provider.ts | 2 +- plugins/auth-backend/src/providers/index.ts | 2 +- .../src/providers/oidc/provider.ts | 1 - .../src/providers/onelogin/provider.ts | 2 +- plugins/auth-backend/src/providers/types.ts | 39 ++++++++++++------- .../src/api/CatalogImportClient.test.ts | 3 ++ .../DefaultImportPage.test.tsx | 3 ++ .../components/ImportPage/ImportPage.test.tsx | 3 ++ .../catalog/src/CatalogClientWrapper.test.ts | 6 +++ .../cost-insights/src/testUtils/providers.tsx | 3 ++ plugins/fossa/src/api/FossaClient.test.ts | 13 +------ plugins/search/src/apis.test.ts | 3 ++ .../sonarqube/src/api/SonarQubeClient.test.ts | 6 +++ plugins/techdocs/src/client.test.ts | 3 ++ 20 files changed, 107 insertions(+), 67 deletions(-) diff --git a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts index 38376491ca..4b0672d745 100644 --- a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { ApiRef, createApiRef } from '../system'; -import { ProfileInfo } from './auth'; +import { BackstageUserIdentity, ProfileInfo } from './auth'; /* @@ -24,26 +24,6 @@ import { ProfileInfo } from './auth'; */ -/** - * User identity information within Backstage. - * - * @public - */ -export type BackstageUserIdentity = { - type: 'user'; - - /** - * The entityRef of the user in the catalog. - * For example User:default/sandra - */ - userEntityRef: string; - - /** - * The user and group entities that the user claims ownership through - */ - ownershipEntityRefs: string[]; -}; - /** * The Identity API used to identify and get information about the signed in user. * diff --git a/plugins/auth-backend/src/identity/IdentityClient.test.ts b/plugins/auth-backend/src/identity/IdentityClient.test.ts index 5eca6bf8cd..9f5e1ce489 100644 --- a/plugins/auth-backend/src/identity/IdentityClient.test.ts +++ b/plugins/auth-backend/src/identity/IdentityClient.test.ts @@ -96,7 +96,15 @@ describe('IdentityClient', () => { it('should accept fresh token', async () => { const token = await factory.issueToken({ claims: { sub: 'foo' } }); const response = await client.authenticate(token); - expect(response).toEqual({ id: 'foo', idToken: token }); + expect(response).toEqual({ + id: 'foo', + token: token, + identity: { + ownershipEntityRefs: [], + type: 'user', + userEntityRef: 'foo', + }, + }); }); it('should throw on incorrect issuer', async () => { @@ -159,7 +167,15 @@ describe('IdentityClient', () => { jest.spyOn(Date, 'now').mockImplementation(() => fixedTime); const token = await factory.issueToken({ claims: { sub: 'foo' } }); const response = await client.authenticate(token); - expect(response).toEqual({ id: 'foo', idToken: token }); + expect(response).toEqual({ + id: 'foo', + token: token, + identity: { + ownershipEntityRefs: [], + type: 'user', + userEntityRef: 'foo', + }, + }); }); it('should not be fooled by the none algorithm', async () => { diff --git a/plugins/auth-backend/src/identity/IdentityClient.ts b/plugins/auth-backend/src/identity/IdentityClient.ts index d60829bf59..552d231e1f 100644 --- a/plugins/auth-backend/src/identity/IdentityClient.ts +++ b/plugins/auth-backend/src/identity/IdentityClient.ts @@ -16,8 +16,9 @@ import fetch from 'node-fetch'; import { JWK, JWT, JWKS, JSONWebKey } from 'jose'; -import { BackstageIdentity } from '../providers'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { AuthenticationError } from '@backstage/errors'; +import { BackstageIdentityResponse } from '../providers/types'; const CLOCK_MARGIN_S = 10; @@ -45,15 +46,17 @@ export class IdentityClient { * Returns a BackstageIdentity (user) matching the token. * The method throws an error if verification fails. */ - async authenticate(token: string | undefined): Promise { + async authenticate( + token: string | undefined, + ): Promise { // Extract token from header if (!token) { - throw new Error('No token specified'); + throw new AuthenticationError('No token specified'); } // Get signing key matching token const key = await this.getKey(token); if (!key) { - throw new Error('No signing key matching token found'); + throw new AuthenticationError('No signing key matching token found'); } // Verify token claims and signature // Note: Claims must match those set by TokenFactory when issuing tokens @@ -62,12 +65,21 @@ export class IdentityClient { algorithms: ['ES256'], audience: 'backstage', issuer: this.issuer, - }) as { sub: string }; + }) as { sub: string; ent: string[] }; // Verified, return the matching user as BackstageIdentity // TODO: Settle internal user format/properties - const user: BackstageIdentity = { + if (!decoded.sub) { + throw new AuthenticationError('No user sub found in token'); + } + + const user: BackstageIdentityResponse = { id: decoded.sub, - idToken: token, + token, + identity: { + type: 'user', + userEntityRef: decoded.sub, + ownershipEntityRefs: decoded.ent ?? [], + }, }; return user; } diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts index 117f1b86e4..8b4b95d59b 100644 --- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts +++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts @@ -51,7 +51,7 @@ describe('oauth helpers', () => { }, backstageIdentity: { id: 'a', - idToken: 'a.b.c', + token: 'a.b.c', }, }, }; @@ -106,7 +106,7 @@ describe('oauth helpers', () => { }, backstageIdentity: { id: 'a', - idToken: 'a.b.c', + token: 'a.b.c', }, }, }; @@ -148,7 +148,7 @@ describe('oauth helpers', () => { }, backstageIdentity: { id: 'a', - idToken: 'a.b.c', + token: 'a.b.c', }, }, }; diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index fa61d69d80..5e52e60f46 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -31,6 +31,7 @@ const mockResponseData = { }, backstageIdentity: { id: 'foo', + token: '', }, }; diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index e1a6fdf2f9..2df67462cf 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -19,8 +19,8 @@ import crypto from 'crypto'; import { URL } from 'url'; import { AuthProviderRouteHandlers, - BackstageIdentity, AuthProviderConfig, + BackstageIdentityResponse, } from '../../providers/types'; import { AuthenticationError, @@ -228,17 +228,17 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { * If the response from the OAuth provider includes a Backstage identity, we * make sure it's populated with all the information we can derive from the user ID. */ - private async populateIdentity(identity?: BackstageIdentity) { + private async populateIdentity(identity?: BackstageIdentityResponse) { if (!identity) { return; } - if (!(identity.token || identity.idToken)) { + if (!(identity.token || identity.token)) { identity.token = await this.options.tokenIssuer.issueToken({ claims: { sub: identity.id }, }); - } else if (!identity.token && identity.idToken) { - identity.token = identity.idToken; + } else if (!identity.token && identity.token) { + identity.token = identity.token; } } diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 4cc3f33499..7aa98c3e65 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -151,7 +151,7 @@ export class Auth0AuthProvider implements OAuthHandlers { const id = profile.email.split('@')[0]; - return { ...response, backstageIdentity: { id } }; + return { ...response, backstageIdentity: { id, token: '' } }; } } diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 88c6ecb6fe..5ad0b84074 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -38,4 +38,4 @@ export type { // These types are needed for a postMessage from the login pop-up // to the frontend -export type { AuthResponse, BackstageIdentity, ProfileInfo } from './types'; +export type { AuthResponse, BackstageUserIdentity, ProfileInfo } from './types'; diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index eba9c72e41..2e58111868 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -205,7 +205,6 @@ export class OidcAuthProvider implements OAuthHandlers { }, ); } - return response; } } diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 092d22189d..66e8b0bfc5 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -148,7 +148,7 @@ export class OneLoginProvider implements OAuthHandlers { const id = profile.email.split('@')[0]; - return { ...response, backstageIdentity: { id } }; + return { ...response, backstageIdentity: { id, token: '' } }; } } diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 52cff0bdf9..17f7002d41 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -140,6 +140,30 @@ export type AuthResponse = { backstageIdentity?: BackstageIdentityResponse; }; +/** + * User identity information within Backstage. + * + * @public + */ +export type BackstageUserIdentity = { + /** + * The type of identity that this structure represents. In the frontend app + * this will currently always be 'user'. + */ + type: 'user'; + + /** + * The entityRef of the user in the catalog. + * For example User:default/sandra + */ + userEntityRef: string; + + /** + * The user and group entities that the user claims ownership through + */ + ownershipEntityRefs: string[]; +}; + /** * @public */ @@ -171,20 +195,7 @@ export type BackstageIdentityResponse = { /** * A plaintext description of the identity that is encapsulated within the token. */ - identity?: { - type: 'user'; - - /** - * The entityRef of the user in the catalog. - * For example User:default/sandra - */ - userEntityRef: string; - - /** - * The user and group entities that the user claims ownership through - */ - ownershipEntityRefs: string[]; - }; + identity?: BackstageUserIdentity; }; /** diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index 276b6d351c..6154eec757 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -79,6 +79,9 @@ describe('CatalogImportClient', () => { signOut: () => { return Promise.resolve(); }, + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; const scmIntegrationsApi = ScmIntegrations.fromConfig( diff --git a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx index 784cbfbe9e..d6132b249e 100644 --- a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx +++ b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx @@ -37,6 +37,9 @@ describe('', () => { signOut: () => { return Promise.resolve(); }, + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; let apis: TestApiRegistry; diff --git a/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx b/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx index 1778af73dc..498e786eb3 100644 --- a/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx +++ b/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx @@ -43,6 +43,9 @@ describe('', () => { signOut: () => { return Promise.resolve(); }, + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; let apis: TestApiRegistry; diff --git a/plugins/catalog/src/CatalogClientWrapper.test.ts b/plugins/catalog/src/CatalogClientWrapper.test.ts index 0e9da8d8c8..6917b5abed 100644 --- a/plugins/catalog/src/CatalogClientWrapper.test.ts +++ b/plugins/catalog/src/CatalogClientWrapper.test.ts @@ -40,6 +40,9 @@ const identityApi: IdentityApi = { async signOut() { return Promise.resolve(); }, + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; const guestIdentityApi: IdentityApi = { getUserId() { @@ -54,6 +57,9 @@ const guestIdentityApi: IdentityApi = { async signOut() { return Promise.resolve(); }, + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; describe('CatalogClientWrapper', () => { diff --git a/plugins/cost-insights/src/testUtils/providers.tsx b/plugins/cost-insights/src/testUtils/providers.tsx index 09d2feb71b..fb18c1a68c 100644 --- a/plugins/cost-insights/src/testUtils/providers.tsx +++ b/plugins/cost-insights/src/testUtils/providers.tsx @@ -187,6 +187,9 @@ export const MockCostInsightsApiProvider = ({ getIdToken: jest.fn(), getUserId: jest.fn(), signOut: jest.fn(), + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; const defaultCostInsightsApi: CostInsightsApi = { diff --git a/plugins/fossa/src/api/FossaClient.test.ts b/plugins/fossa/src/api/FossaClient.test.ts index 4debb37ad6..e723a4b00a 100644 --- a/plugins/fossa/src/api/FossaClient.test.ts +++ b/plugins/fossa/src/api/FossaClient.test.ts @@ -24,20 +24,11 @@ import { UrlPatternDiscovery } from '@backstage/core-app-api'; const server = setupServer(); -const identityApi: IdentityApi = { - getUserId() { - return 'jane-fonda'; - }, - getProfile() { - return { email: 'jane-fonda@spotify.com' }; - }, +const identityApi = { async getIdToken() { return Promise.resolve('fake-id-token'); }, - async signOut() { - return Promise.resolve(); - }, -}; +} as IdentityApi; describe('FossaClient', () => { setupRequestMockHandlers(server); diff --git a/plugins/search/src/apis.test.ts b/plugins/search/src/apis.test.ts index 91395d1f0b..5721812059 100644 --- a/plugins/search/src/apis.test.ts +++ b/plugins/search/src/apis.test.ts @@ -34,6 +34,9 @@ describe('apis', () => { getUserId: jest.fn(), getProfile: jest.fn(), signOut: jest.fn(), + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }); const client = new SearchClient({ diff --git a/plugins/sonarqube/src/api/SonarQubeClient.test.ts b/plugins/sonarqube/src/api/SonarQubeClient.test.ts index f8fd7c2264..a3a301d77a 100644 --- a/plugins/sonarqube/src/api/SonarQubeClient.test.ts +++ b/plugins/sonarqube/src/api/SonarQubeClient.test.ts @@ -37,6 +37,9 @@ const identityApiAuthenticated: IdentityApi = { async signOut() { return Promise.resolve(); }, + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; const identityApiGuest: IdentityApi = { getUserId() { @@ -51,6 +54,9 @@ const identityApiGuest: IdentityApi = { async signOut() { return Promise.resolve(); }, + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; describe('SonarQubeClient', () => { diff --git a/plugins/techdocs/src/client.test.ts b/plugins/techdocs/src/client.test.ts index 7624f250d9..bdb2f74772 100644 --- a/plugins/techdocs/src/client.test.ts +++ b/plugins/techdocs/src/client.test.ts @@ -44,6 +44,9 @@ describe('TechDocsStorageClient', () => { getProfile: jest.fn(), getUserId: jest.fn(), signOut: jest.fn(), + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), }; beforeEach(() => { From 15b98232d089f3b8b88e73bb86c7786aff8a34dd Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 30 Nov 2021 13:19:21 +0100 Subject: [PATCH 009/116] chore: drop wip comments Signed-off-by: Johan Haals --- packages/core-app-api/src/app/AppManager.tsx | 53 -------------------- 1 file changed, 53 deletions(-) diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 31e093a145..a923b4131e 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -480,56 +480,3 @@ export class AppManager implements BackstageApp { } } } - -// interface FooPropsV1 { -// foo: () => undefined; -// } - -// interface FooPropsV2 { -// foo: () => undefined; -// bar: () => undefined; -// } - -// // type FooProps = { -// // foo: () => undefined -// // } | { -// // foo: () => undefined -// // bar: () => undefined -// // } - -// interface CreateDerpOptions { -// components: { -// Foo: (props: FooPropsV1 | FooPropsV2) => JSX.Element; -// }; -// } - -// interface Derp { -// getComponents(): { -// Foo: (props: FooPropsV1) => JSX.Element; -// }; -// } - -// function createDerp(options: CreateDerpOptions): Derp { -// return { getComponents: () => options.components }; -// } - -// function CustomFoo(props: FooPropsV1) { -// return
{props.foo()}
; -// } - -// function NewCustomFoo(props: FooPropsV2) { -// return ( -//
-// {props.foo()} {props.bar()} -//
-// ); -// } - -// const derp = createDerp({ -// components: { -// Foo: NewCustomFoo, -// }, -// }); - -// const { Foo } = derp.getComponents(); -// const _foo = undefined} />; From 48e1d3bfca17449f202799df64ffb6ce75ff17fb Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 30 Nov 2021 13:24:52 +0100 Subject: [PATCH 010/116] chore: remove wip comments Signed-off-by: Johan Haals --- .../core-plugin-api/src/apis/definitions/IdentityApi.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts index 4b0672d745..36d43f93ff 100644 --- a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts @@ -16,14 +16,6 @@ import { ApiRef, createApiRef } from '../system'; import { BackstageUserIdentity, ProfileInfo } from './auth'; -/* - -- [ ] IdentityApi getProfile, make async -- [ ] BackstageIdentity (settle or remove) -- [ ] Evolution plan for utility APIs - -*/ - /** * The Identity API used to identify and get information about the signed in user. * From 8c337a480f28b4eec6e991bb6c1d0575ef035655 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 30 Nov 2021 16:25:29 +0100 Subject: [PATCH 011/116] chore: Update types and API reports Signed-off-by: Johan Haals --- packages/core-app-api/api-report.md | 5 +-- packages/core-app-api/src/app/types.ts | 2 +- packages/core-plugin-api/api-report.md | 36 +++++++++++++------ .../src/apis/definitions/IdentityApi.ts | 2 +- plugins/auth-backend/api-report.md | 24 +++++++------ plugins/auth-backend/src/providers/index.ts | 7 +++- plugins/auth-backend/src/providers/types.ts | 1 + .../permission-backend/src/service/router.ts | 4 +-- plugins/permission-node/src/policy/types.ts | 4 +-- 9 files changed, 55 insertions(+), 30 deletions(-) diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index e69ee4f325..252531f7ad 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -39,6 +39,7 @@ import { FeatureFlagsSaveOptions } from '@backstage/core-plugin-api'; import { gitlabAuthApiRef } from '@backstage/core-plugin-api'; import { googleAuthApiRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; +import { IdentityApi } from '@backstage/core-plugin-api'; import { microsoftAuthApiRef } from '@backstage/core-plugin-api'; import { OAuthApi } from '@backstage/core-plugin-api'; import { OAuthRequestApi } from '@backstage/core-plugin-api'; @@ -635,10 +636,10 @@ export type SamlSession = { // @public export type SignInPageProps = { - onResult(result: SignInResult): void; + onSignInSuccess(identityApi: IdentityApi): void; }; -// @public +// @public @deprecated export type SignInResult = { userId: string; profile: ProfileInfo; diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts index 0921bd2e17..a538ccd366 100644 --- a/packages/core-app-api/src/app/types.ts +++ b/packages/core-app-api/src/app/types.ts @@ -43,7 +43,7 @@ export type BootErrorPageProps = { * The outcome of signing in on the sign-in page. * * @public - * @deprecated replaced by passing the {@link IdentityApi} to the {@link SignInPageProps.onSignInSuccess} instead. + * @deprecated replaced by passing the {@link @backstage/core-plugin-api#IdentityApi} to the {@link SignInPageProps.onSignInSuccess} instead. */ export type SignInResult = { /** diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 560071e1fa..9245f039df 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -10,6 +10,7 @@ import { BackstageTheme } from '@backstage/theme'; import { ComponentType } from 'react'; import { Config } from '@backstage/config'; import { IconComponent as IconComponent_2 } from '@backstage/core-plugin-api'; +import { IdentityApi as IdentityApi_2 } from '@backstage/core-plugin-api'; import { Observable as Observable_2 } from '@backstage/types'; import { Observer as Observer_2 } from '@backstage/types'; import { ProfileInfo as ProfileInfo_2 } from '@backstage/core-plugin-api'; @@ -236,18 +237,21 @@ export type AuthRequestOptions = { instantPopup?: boolean; }; -// @public -export type BackstageIdentity = { - id: string; - idToken: string; - token: string; -}; +// @public @deprecated (undocumented) +export type BackstageIdentity = BackstageIdentityResponse; // @public export type BackstageIdentityApi = { getBackstageIdentity( options?: AuthRequestOptions, - ): Promise; + ): Promise; +}; + +// @public +export type BackstageIdentityResponse = { + id: string; + token: string; + identity: BackstageUserIdentity; }; // @public @@ -264,6 +268,13 @@ export type BackstagePlugin< externalRoutes: ExternalRoutes; }; +// @public +export type BackstageUserIdentity = { + type: 'user'; + userEntityRef: string; + ownershipEntityRefs: string[]; +}; + // @public export const bitbucketAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi @@ -531,8 +542,13 @@ export type IconComponent = ComponentType<{ // @public export type IdentityApi = { getUserId(): string; - getProfile(): ProfileInfo; getIdToken(): Promise; + getProfile(): ProfileInfo; + getProfileInfo(): Promise; + getBackstageIdentity(): Promise; + getCredentials(): Promise<{ + token?: string; + }>; signOut(): Promise; }; @@ -745,10 +761,10 @@ export enum SessionState { // @public export type SignInPageProps = { - onResult(result: SignInResult): void; + onSignInSuccess(identityApi: IdentityApi_2): void; }; -// @public +// @public @deprecated export type SignInResult = { userId: string; profile: ProfileInfo_2; diff --git a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts index 36d43f93ff..fe4de89106 100644 --- a/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/IdentityApi.ts @@ -26,7 +26,7 @@ export type IdentityApi = { * The ID of the signed in user. This ID is not meant to be presented to the user, but used * as an opaque string to pass on to backends or use in frontend logic. * - * @deprecated use {@link IdentityApi.getIdentity} instead. + * @deprecated use {@link IdentityApi.getBackstageIdentity} instead. */ getUserId(): string; diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 46d8b03cbb..2004490261 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -103,7 +103,7 @@ export interface AuthProviderRouteHandlers { export type AuthResponse = { providerInfo: ProviderInfo; profile: ProfileInfo; - backstageIdentity?: BackstageIdentity; + backstageIdentity?: BackstageIdentityResponse; }; // Warning: (ae-missing-release-tag) "AwsAlbProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -116,14 +116,19 @@ export type AwsAlbProviderOptions = { }; }; -// Warning: (ae-missing-release-tag) "BackstageIdentity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type BackstageIdentity = { +// @public +export type BackstageIdentityResponse = { id: string; - idToken?: string; - token?: string; entity?: Entity; + token: string; + identity?: BackstageUserIdentity; +}; + +// @public +export type BackstageUserIdentity = { + type: 'user'; + userEntityRef: string; + ownershipEntityRefs: string[]; }; // Warning: (ae-missing-release-tag) "BitbucketOAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -361,7 +366,7 @@ export type GoogleProviderOptions = { // @public export class IdentityClient { constructor(options: { discovery: PluginEndpointDiscovery; issuer: string }); - authenticate(token: string | undefined): Promise; + authenticate(token: string | undefined): Promise; static getBearerToken( authorizationHeader: string | undefined, ): string | undefined; @@ -551,8 +556,6 @@ export const postMessageResponse: ( response: WebMessageResponse, ) => void; -// Warning: (ae-missing-release-tag) "ProfileInfo" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type ProfileInfo = { email?: string; @@ -637,5 +640,4 @@ export type WebMessageResponse = // src/providers/github/provider.d.ts:71:68 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" // src/providers/github/provider.d.ts:78:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts // src/providers/types.d.ts:100:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:122:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative ``` diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 5ad0b84074..5b4fbc6338 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -38,4 +38,9 @@ export type { // These types are needed for a postMessage from the login pop-up // to the frontend -export type { AuthResponse, BackstageUserIdentity, ProfileInfo } from './types'; +export type { + AuthResponse, + BackstageUserIdentity, + BackstageIdentityResponse, + ProfileInfo, +} from './types'; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 17f7002d41..1d2c6f5134 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -165,6 +165,7 @@ export type BackstageUserIdentity = { }; /** + * Response object containing the {@link BackstageUserIdentity} and the token from the authentication provider. * @public */ export type BackstageIdentityResponse = { diff --git a/plugins/permission-backend/src/service/router.ts b/plugins/permission-backend/src/service/router.ts index b85e7feb85..41791b8fc8 100644 --- a/plugins/permission-backend/src/service/router.ts +++ b/plugins/permission-backend/src/service/router.ts @@ -23,7 +23,7 @@ import { PluginEndpointDiscovery, } from '@backstage/backend-common'; import { - BackstageIdentity, + BackstageIdentityResponse, IdentityClient, } from '@backstage/plugin-auth-backend'; import { @@ -71,7 +71,7 @@ export interface RouterOptions { const handleRequest = async ( { id, resourceRef, ...request }: Identified, - user: BackstageIdentity | undefined, + user: BackstageIdentityResponse | undefined, policy: PermissionPolicy, permissionIntegrationClient: PermissionIntegrationClient, authHeader?: string, diff --git a/plugins/permission-node/src/policy/types.ts b/plugins/permission-node/src/policy/types.ts index 3548d051f6..d122ad8d8d 100644 --- a/plugins/permission-node/src/policy/types.ts +++ b/plugins/permission-node/src/policy/types.ts @@ -20,7 +20,7 @@ import { PermissionCondition, PermissionCriteria, } from '@backstage/plugin-permission-common'; -import { BackstageIdentity } from '@backstage/plugin-auth-backend'; +import { BackstageIdentityResponse } from '@backstage/plugin-auth-backend'; /** * An authorization request to be evaluated by the {@link PermissionPolicy}. @@ -83,6 +83,6 @@ export type PolicyDecision = export interface PermissionPolicy { handle( request: PolicyAuthorizeRequest, - user?: BackstageIdentity, + user?: BackstageIdentityResponse, ): Promise; } From 0bb10226b8f618e9a932a283aeedace421ba9ebd Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 1 Dec 2021 10:33:55 +0100 Subject: [PATCH 012/116] chore: move some packges around so that we don't have bad imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johan Haals Co-authored-by: Fredrik Adelöw Signed-off-by: blam --- packages/core-app-api/src/app/AppManager.tsx | 2 +- .../src/layout/SignInPage}/GuestUserIdentity.ts | 0 .../src/layout/SignInPage}/LegacyUserIdentity.ts | 0 .../core-components/src/layout/SignInPage/customProvider.tsx | 2 +- .../core-components/src/layout/SignInPage/guestProvider.tsx | 2 +- packages/core-components/src/layout/SignInPage/index.ts | 2 ++ 6 files changed, 5 insertions(+), 3 deletions(-) rename packages/{core-app-api/src/apis/implementations/IdentityApi => core-components/src/layout/SignInPage}/GuestUserIdentity.ts (100%) rename packages/{core-app-api/src/apis/implementations/IdentityApi => core-components/src/layout/SignInPage}/LegacyUserIdentity.ts (100%) diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index a923b4131e..141de0aab2 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -50,7 +50,7 @@ import { SubRouteRef, ExternalRouteRef, } from '@backstage/core-plugin-api'; -import { GuestUserIdentity } from '../apis/implementations/IdentityApi/GuestUserIdentity'; +import { GuestUserIdentity } from '@backstage/core-components'; import { ApiFactoryRegistry, ApiResolver } from '../apis/system'; import { childDiscoverer, diff --git a/packages/core-app-api/src/apis/implementations/IdentityApi/GuestUserIdentity.ts b/packages/core-components/src/layout/SignInPage/GuestUserIdentity.ts similarity index 100% rename from packages/core-app-api/src/apis/implementations/IdentityApi/GuestUserIdentity.ts rename to packages/core-components/src/layout/SignInPage/GuestUserIdentity.ts diff --git a/packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts b/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts similarity index 100% rename from packages/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity.ts rename to packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts diff --git a/packages/core-components/src/layout/SignInPage/customProvider.tsx b/packages/core-components/src/layout/SignInPage/customProvider.tsx index 77bde5223c..a6e46aee6a 100644 --- a/packages/core-components/src/layout/SignInPage/customProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/customProvider.tsx @@ -26,7 +26,7 @@ import isEmpty from 'lodash/isEmpty'; import { InfoCard } from '../InfoCard/InfoCard'; import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; import { GridItem } from './styles'; -import { LegacyUserIdentity } from '@backstage/core-app-api/src/apis/implementations/IdentityApi/LegacyUserIdentity'; +import { LegacyUserIdentity } from './LegacyUserIdentity'; // accept base64url format according to RFC7515 (https://tools.ietf.org/html/rfc7515#section-3) const ID_TOKEN_REGEX = /^[a-z0-9_\-]+\.[a-z0-9_\-]+\.[a-z0-9_\-]+$/i; diff --git a/packages/core-components/src/layout/SignInPage/guestProvider.tsx b/packages/core-components/src/layout/SignInPage/guestProvider.tsx index 9e0326c7df..b2370a98d5 100644 --- a/packages/core-components/src/layout/SignInPage/guestProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/guestProvider.tsx @@ -20,7 +20,7 @@ import Button from '@material-ui/core/Button'; import { InfoCard } from '../InfoCard/InfoCard'; import { GridItem } from './styles'; import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; -import { GuestUserIdentity } from '@backstage/core-app-api/src/apis/implementations/IdentityApi/GuestUserIdentity'; +import { GuestUserIdentity } from './GuestUserIdentity'; const Component: ProviderComponent = ({ onSignInSuccess }) => ( diff --git a/packages/core-components/src/layout/SignInPage/index.ts b/packages/core-components/src/layout/SignInPage/index.ts index caa8506399..176cba96a6 100644 --- a/packages/core-components/src/layout/SignInPage/index.ts +++ b/packages/core-components/src/layout/SignInPage/index.ts @@ -18,3 +18,5 @@ export type { SignInProviderConfig } from './types'; export { SignInPage } from './SignInPage'; export type { SignInPageClassKey } from './styles'; export type { CustomProviderClassKey } from './customProvider'; +export { GuestUserIdentity } from './GuestUserIdentity'; +export { LegacyUserIdentity } from './LegacyUserIdentity'; From 64c3fc492e1956396019f5e6a67cd4d2fb3f639e Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 1 Dec 2021 10:55:17 +0100 Subject: [PATCH 013/116] chore: fix up api-reports and fix the export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johan Haals Co-authored-by: Fredrik Adelöw Signed-off-by: blam --- packages/core-app-api/src/app/AppManager.tsx | 4 +- packages/core-components/api-report.md | 41 +++++++++++++++++++ .../src/layout/SignInPage/UserIdentity.ts | 14 ++++++- .../src/layout/SignInPage/index.ts | 3 +- .../src/lib/oauth/OAuthAdapter.ts | 2 +- plugins/permission-node/api-report.md | 4 +- 6 files changed, 60 insertions(+), 8 deletions(-) diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 141de0aab2..21940c08d5 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -50,7 +50,7 @@ import { SubRouteRef, ExternalRouteRef, } from '@backstage/core-plugin-api'; -import { GuestUserIdentity } from '@backstage/core-components'; +import { UserIdentity } from '@backstage/core-components'; import { ApiFactoryRegistry, ApiResolver } from '../apis/system'; import { childDiscoverer, @@ -361,7 +361,7 @@ export class AppManager implements BackstageApp { // If the app hasn't configured a sign-in page, we just continue as guest. if (!SignInPageComponent) { - this.appIdentityProxy.setTarget(new GuestUserIdentity()); + this.appIdentityProxy.setTarget(UserIdentity.createGuest()); return ( diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index c1e021391e..f237932baa 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -9,6 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { BackstagePalette } from '@backstage/theme'; import { BackstageTheme } from '@backstage/theme'; +import { BackstageUserIdentity } from '@backstage/core-plugin-api'; import { ButtonProps as ButtonProps_2 } from '@material-ui/core/Button'; import { CardHeaderProps } from '@material-ui/core/CardHeader'; import { Column } from '@material-table/core'; @@ -20,6 +21,7 @@ import { CSSProperties } from 'react'; import { ElementType } from 'react'; import { ErrorInfo } from 'react'; import { IconComponent } from '@backstage/core-plugin-api'; +import { IdentityApi } from '@backstage/core-plugin-api'; import { LinearProgressProps } from '@material-ui/core/LinearProgress'; import { LinkProps as LinkProps_2 } from '@material-ui/core/Link'; import { LinkProps as LinkProps_3 } from 'react-router-dom'; @@ -27,6 +29,7 @@ import MaterialBreadcrumbs from '@material-ui/core/Breadcrumbs'; import { MaterialTableProps } from '@material-table/core'; import { NavLinkProps } from 'react-router-dom'; import { Overrides } from '@material-ui/core/styles/overrides'; +import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; @@ -35,6 +38,7 @@ import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { SessionApi } from '@backstage/core-plugin-api'; import { SignInPageProps } from '@backstage/core-plugin-api'; +import { SignInResult } from '@backstage/core-plugin-api'; import { SparklinesLineProps } from 'react-sparklines'; import { SparklinesProps } from 'react-sparklines'; import { StyledComponentProps } from '@material-ui/core/styles'; @@ -2317,6 +2321,42 @@ export function useQueryParamState( // @public (undocumented) export function UserIcon(props: IconComponentProps): JSX.Element; +// Warning: (ae-missing-release-tag) "UserIdentity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class UserIdentity implements IdentityApi { + // Warning: (ae-forgotten-export) The symbol "GuestUserIdentity" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static createGuest(): GuestUserIdentity; + // (undocumented) + static from(options: { + identity: BackstageUserIdentity; + authApi: ProfileInfoApi & BackstageIdentityApi & SessionApi; + profile?: ProfileInfo; + }): UserIdentity; + // Warning: (ae-forgotten-export) The symbol "LegacyUserIdentity" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static fromLegacy({ result }: { result: SignInResult }): LegacyUserIdentity; + // (undocumented) + getBackstageIdentity(): Promise; + // (undocumented) + getCredentials(): Promise<{ + token?: string | undefined; + }>; + // (undocumented) + getIdToken(): Promise; + // (undocumented) + getProfile(): ProfileInfo; + // (undocumented) + getProfileInfo(): Promise; + // (undocumented) + getUserId(): string; + // (undocumented) + signOut(): Promise; +} + // Warning: (ae-missing-release-tag) "useSupportConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2360,4 +2400,5 @@ export type WarningPanelClassKey = // src/components/TabbedLayout/RoutedTabs.d.ts:9:5 - (ae-forgotten-export) The symbol "SubRoute" needs to be exported by the entry point index.d.ts // src/components/Table/Table.d.ts:20:5 - (ae-forgotten-export) The symbol "SelectedFilters" needs to be exported by the entry point index.d.ts // src/layout/ErrorBoundary/ErrorBoundary.d.ts:8:5 - (ae-forgotten-export) The symbol "SlackChannel" needs to be exported by the entry point index.d.ts +// src/layout/SignInPage/UserIdentity.d.ts:22:9 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "IdentityApi" ``` diff --git a/packages/core-components/src/layout/SignInPage/UserIdentity.ts b/packages/core-components/src/layout/SignInPage/UserIdentity.ts index daf9111d84..088f76e60e 100644 --- a/packages/core-components/src/layout/SignInPage/UserIdentity.ts +++ b/packages/core-components/src/layout/SignInPage/UserIdentity.ts @@ -21,16 +21,28 @@ import { BackstageUserIdentity, BackstageIdentityApi, SessionApi, + SignInResult, } from '@backstage/core-plugin-api'; +import { GuestUserIdentity } from './GuestUserIdentity'; +import { LegacyUserIdentity } from './LegacyUserIdentity'; + export class UserIdentity implements IdentityApi { + static createGuest() { + return new GuestUserIdentity(); + } + + static fromLegacy({ result }: { result: SignInResult }) { + return LegacyUserIdentity.fromResult(result); + } + static from(options: { identity: BackstageUserIdentity; authApi: ProfileInfoApi & BackstageIdentityApi & SessionApi; /** * Passing a profile synchronously allows the deprecated `getProfile` method to be * called by consumers of the {@link IdentityApi}. If you do not have any consumers - * of that method than this is safe to leave out. + * of that method then this is safe to leave out. * * @deprecated Only provide this if you have plugins that call the synchronous `getProfile` method, which is also deprecated. */ diff --git a/packages/core-components/src/layout/SignInPage/index.ts b/packages/core-components/src/layout/SignInPage/index.ts index 176cba96a6..b3c7618f50 100644 --- a/packages/core-components/src/layout/SignInPage/index.ts +++ b/packages/core-components/src/layout/SignInPage/index.ts @@ -18,5 +18,4 @@ export type { SignInProviderConfig } from './types'; export { SignInPage } from './SignInPage'; export type { SignInPageClassKey } from './styles'; export type { CustomProviderClassKey } from './customProvider'; -export { GuestUserIdentity } from './GuestUserIdentity'; -export { LegacyUserIdentity } from './LegacyUserIdentity'; +export { UserIdentity } from './UserIdentity'; diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index 2df67462cf..bbef4790fe 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -233,7 +233,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { return; } - if (!(identity.token || identity.token)) { + if (!(identity.token || identity.id)) { identity.token = await this.options.tokenIssuer.issueToken({ claims: { sub: identity.id }, }); diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 75c45a9250..2bd9c5d6a8 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -5,7 +5,7 @@ ```ts import { AuthorizeRequest } from '@backstage/plugin-permission-common'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; -import { BackstageIdentity } from '@backstage/plugin-auth-backend'; +import { BackstageIdentityResponse } from '@backstage/plugin-auth-backend'; import { PermissionCondition } from '@backstage/plugin-permission-common'; import { PermissionCriteria } from '@backstage/plugin-permission-common'; import { Router } from 'express'; @@ -98,7 +98,7 @@ export interface PermissionPolicy { // (undocumented) handle( request: PolicyAuthorizeRequest, - user?: BackstageIdentity, + user?: BackstageIdentityResponse, ): Promise; } From 29d0b45c6a660f604d5bc2fcc66ff669dacfdf3b Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 1 Dec 2021 14:13:07 +0100 Subject: [PATCH 014/116] chore: fixing issue with multi signin providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johan Haals Co-authored-by: Fredrik Adelöw Signed-off-by: blam --- .../SignInPage/IdentityApiSignOutProxy.ts | 65 +++++++++++++++++++ .../src/layout/SignInPage/SignInPage.tsx | 2 +- .../src/layout/SignInPage/customProvider.tsx | 15 +++-- .../src/layout/SignInPage/providers.tsx | 17 +++-- .../src/lib/oauth/OAuthAdapter.ts | 2 +- .../src/providers/decorateWithIdentity.ts | 39 +++++++++++ .../src/providers/github/provider.test.ts | 55 ++++++++++++++-- .../src/providers/github/provider.ts | 24 ++++++- .../src/providers/google/provider.ts | 4 +- plugins/auth-backend/src/providers/types.ts | 4 +- 10 files changed, 200 insertions(+), 27 deletions(-) create mode 100644 packages/core-components/src/layout/SignInPage/IdentityApiSignOutProxy.ts create mode 100644 plugins/auth-backend/src/providers/decorateWithIdentity.ts diff --git a/packages/core-components/src/layout/SignInPage/IdentityApiSignOutProxy.ts b/packages/core-components/src/layout/SignInPage/IdentityApiSignOutProxy.ts new file mode 100644 index 0000000000..ec45bc7399 --- /dev/null +++ b/packages/core-components/src/layout/SignInPage/IdentityApiSignOutProxy.ts @@ -0,0 +1,65 @@ +/* + * 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 { + BackstageUserIdentity, + IdentityApi, + ProfileInfo, +} from '@backstage/core-plugin-api'; + +export class IdentityApiSignOutProxy implements IdentityApi { + private constructor( + private readonly config: { + identityApi: IdentityApi; + signOut: IdentityApi['signOut']; + }, + ) {} + + static from(config: { + identityApi: IdentityApi; + signOut: IdentityApi['signOut']; + }): IdentityApi { + return new IdentityApiSignOutProxy(config); + } + + getUserId(): string { + return this.config.identityApi.getUserId(); + } + + getIdToken(): Promise { + return this.config.identityApi.getIdToken(); + } + + getProfile(): ProfileInfo { + return this.config.identityApi.getProfile(); + } + + getProfileInfo(): Promise { + return this.config.identityApi.getProfileInfo(); + } + + getBackstageIdentity(): Promise { + return this.config.identityApi.getBackstageIdentity(); + } + + getCredentials(): Promise<{ token?: string | undefined }> { + return this.config.identityApi.getCredentials(); + } + + signOut(): Promise { + return this.config.signOut(); + } +} diff --git a/packages/core-components/src/layout/SignInPage/SignInPage.tsx b/packages/core-components/src/layout/SignInPage/SignInPage.tsx index d0704c2b85..3a068decd5 100644 --- a/packages/core-components/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core-components/src/layout/SignInPage/SignInPage.tsx @@ -135,7 +135,6 @@ export const SingleSignInPage = ({ } const profile = await authApi.getProfile(); - onSignInSuccess( UserIdentity.from({ identity: identityResponse.identity, @@ -149,6 +148,7 @@ export const SingleSignInPage = ({ setShowLoginPage(true); } }; + useMount(() => login({ checkExisting: true })); return showLoginPage ? ( diff --git a/packages/core-components/src/layout/SignInPage/customProvider.tsx b/packages/core-components/src/layout/SignInPage/customProvider.tsx index a6e46aee6a..9863234a06 100644 --- a/packages/core-components/src/layout/SignInPage/customProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/customProvider.tsx @@ -26,7 +26,7 @@ import isEmpty from 'lodash/isEmpty'; import { InfoCard } from '../InfoCard/InfoCard'; import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; import { GridItem } from './styles'; -import { LegacyUserIdentity } from './LegacyUserIdentity'; +import { UserIdentity } from './UserIdentity'; // accept base64url format according to RFC7515 (https://tools.ietf.org/html/rfc7515#section-3) const ID_TOKEN_REGEX = /^[a-z0-9_\-]+\.[a-z0-9_\-]+\.[a-z0-9_\-]+$/i; @@ -69,14 +69,15 @@ const Component: ProviderComponent = ({ onSignInSuccess }) => { const { errors } = formState; - const handleResult = ({ userId, idToken }: Data) => { + const handleResult = ({ userId }: Data) => { onSignInSuccess( - LegacyUserIdentity.fromResult({ - userId, - profile: { - email: `${userId}@example.com`, + UserIdentity.fromLegacy({ + result: { + userId, + profile: { + email: `${userId}@example.com`, + }, }, - getIdToken: idToken ? async () => idToken : undefined, }), ); }; diff --git a/packages/core-components/src/layout/SignInPage/providers.tsx b/packages/core-components/src/layout/SignInPage/providers.tsx index 1ab7369dab..2abe6b3d51 100644 --- a/packages/core-components/src/layout/SignInPage/providers.tsx +++ b/packages/core-components/src/layout/SignInPage/providers.tsx @@ -30,6 +30,7 @@ import { import { commonProvider } from './commonProvider'; import { guestProvider } from './guestProvider'; import { customProvider } from './customProvider'; +import { IdentityApiProxy } from './IdentityApiProxy'; const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; @@ -90,13 +91,15 @@ export const useSignInProviders = ( // This decorates the result with sign out logic from this hook const handleWrappedResult = useCallback( (identityApi: IdentityApi) => { - onSignInSuccess({ - ...identityApi, - signOut: async () => { - localStorage.removeItem(PROVIDER_STORAGE_KEY); - await identityApi.signOut?.(); - }, - }); + onSignInSuccess( + IdentityApiProxy.from({ + identityApi, + signOut: async () => { + localStorage.removeItem(PROVIDER_STORAGE_KEY); + await identityApi.signOut?.(); + }, + }), + ); }, [onSignInSuccess], ); diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index bbef4790fe..d6a825f3fd 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -233,7 +233,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { return; } - if (!(identity.token || identity.id)) { + if (!identity.token) { identity.token = await this.options.tokenIssuer.issueToken({ claims: { sub: identity.id }, }); diff --git a/plugins/auth-backend/src/providers/decorateWithIdentity.ts b/plugins/auth-backend/src/providers/decorateWithIdentity.ts new file mode 100644 index 0000000000..dab1480db4 --- /dev/null +++ b/plugins/auth-backend/src/providers/decorateWithIdentity.ts @@ -0,0 +1,39 @@ +/* + * 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 { BackstageIdentityResponse } from './types'; + +function parseJwtPayload(token: string) { + const [_header, payload, _signature] = token.split('.'); + return JSON.parse(Buffer.from(payload, 'base64').toString()); +} + +/** + * Parses token and decorates the BackstageIdentityResponse with identity information sourced from the token + */ +export function decorateWithIdentity( + signInResolverResponse: Omit, +): BackstageIdentityResponse { + const { sub, ent } = parseJwtPayload(signInResolverResponse.token); + return { + ...signInResolverResponse, + identity: { + type: 'user', + userEntityRef: sub, + ownershipEntityRefs: ent ?? [sub], + }, + }; +} diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts index e418ab22c2..02c3367914 100644 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ b/plugins/auth-backend/src/providers/github/provider.test.ts @@ -41,7 +41,12 @@ describe('GithubAuthProvider', () => { const tokenIssuer: TokenIssuer = { listPublicKeys: jest.fn(), async issueToken(params) { - return `token-for-${params.claims.sub}`; + const tokenContents = { + sub: params.claims.sub, + ent: params.claims.ent ?? [], + }; + + return `eyblob.${btoa(JSON.stringify(tokenContents))}.eyblob`; }, }; const catalogIdentityClient = { @@ -93,7 +98,13 @@ describe('GithubAuthProvider', () => { const expected = { backstageIdentity: { id: 'jimmymarkum', - token: 'token-for-jimmymarkum', + token: + 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', + identity: { + ownershipEntityRefs: ['user:default/jimmymarkum'], + type: 'user', + userEntityRef: 'jimmymarkum', + }, }, providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', @@ -138,7 +149,13 @@ describe('GithubAuthProvider', () => { const expected = { backstageIdentity: { id: 'jimmymarkum', - token: 'token-for-jimmymarkum', + token: + 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', + identity: { + type: 'user', + ownershipEntityRefs: ['user:default/jimmymarkum'], + userEntityRef: 'jimmymarkum', + }, }, providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', @@ -181,7 +198,13 @@ describe('GithubAuthProvider', () => { const expected = { backstageIdentity: { id: 'jimmymarkum', - token: 'token-for-jimmymarkum', + token: + 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', + identity: { + type: 'user', + ownershipEntityRefs: ['user:default/jimmymarkum'], + userEntityRef: 'jimmymarkum', + }, }, providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', @@ -224,7 +247,13 @@ describe('GithubAuthProvider', () => { const expected = { backstageIdentity: { id: 'daveboyle', - token: 'token-for-daveboyle', + token: + 'eyblob.eyJzdWIiOiJkYXZlYm95bGUiLCJlbnQiOlsidXNlcjpkZWZhdWx0L2RhdmVib3lsZSJdfQ==.eyblob', + identity: { + type: 'user', + ownershipEntityRefs: ['user:default/daveboyle'], + userEntityRef: 'daveboyle', + }, }, providerInfo: { accessToken: @@ -268,7 +297,13 @@ describe('GithubAuthProvider', () => { response: { backstageIdentity: { id: 'ipd12039', - token: 'token-for-ipd12039', + token: + 'eyblob.eyJzdWIiOiJpcGQxMjAzOSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvaXBkMTIwMzkiXX0=.eyblob', + identity: { + type: 'user', + ownershipEntityRefs: ['user:default/ipd12039'], + userEntityRef: 'ipd12039', + }, }, providerInfo: { accessToken: 'a.b.c', @@ -321,7 +356,13 @@ describe('GithubAuthProvider', () => { expect(response).toEqual({ backstageIdentity: { id: 'mockuser', - token: 'token-for-mockuser', + token: + 'eyblob.eyJzdWIiOiJtb2NrdXNlciIsImVudCI6WyJ1c2VyOmRlZmF1bHQvbW9ja3VzZXIiXX0=.eyblob', + identity: { + type: 'user', + ownershipEntityRefs: ['user:default/mockuser'], + userEntityRef: 'mockuser', + }, }, profile: { displayName: 'Mocked User', diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index c7e82cc5ff..eb15afa4ab 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -32,6 +32,7 @@ import { AuthHandler, SignInResolver, StateEncoder, + BackstageIdentityResponse, } from '../types'; import { OAuthAdapter, @@ -167,7 +168,26 @@ export class GithubAuthProvider implements OAuthHandlers { }; if (this.signInResolver) { - response.backstageIdentity = await this.signInResolver( + const decorateWithIdentity = ( + signInResolverResponse: Omit, + ): BackstageIdentityResponse => { + function parseJwtPayload(token: string) { + const [_header, payload, _signature] = token.split('.'); + return JSON.parse(Buffer.from(payload, 'base64').toString()); + } + const { sub, ent } = parseJwtPayload(signInResolverResponse.token); + return { + ...signInResolverResponse, + identity: { + type: 'user', + userEntityRef: sub, + ownershipEntityRefs: ent ?? [sub], + }, + }; + // parse token + // build identity + }; + const signInResolverResult = await this.signInResolver( { result, profile, @@ -178,6 +198,8 @@ export class GithubAuthProvider implements OAuthHandlers { logger: this.logger, }, ); + + response.backstageIdentity = decorateWithIdentity(signInResolverResult); } return response; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index d85fc2de2d..293f738dfd 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -159,7 +159,7 @@ export class GoogleAuthProvider implements OAuthHandlers { }; if (this.signInResolver) { - response.backstageIdentity = await this.signInResolver( + const signInResolverResult = await this.signInResolver( { result, profile, @@ -170,6 +170,8 @@ export class GoogleAuthProvider implements OAuthHandlers { logger: this.logger, }, ); + + console.log(signInResolverResult); } return response; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 1d2c6f5134..919cbe42b4 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -196,7 +196,7 @@ export type BackstageIdentityResponse = { /** * A plaintext description of the identity that is encapsulated within the token. */ - identity?: BackstageUserIdentity; + identity: BackstageUserIdentity; }; /** @@ -242,7 +242,7 @@ export type SignInResolver = ( catalogIdentityClient: CatalogIdentityClient; logger: Logger; }, -) => Promise; +) => Promise>; export type AuthHandlerResult = { profile: ProfileInfo }; From 39645e56ac32fe9dfedf5dac7a8114cf186eb35a Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 1 Dec 2021 16:02:09 +0100 Subject: [PATCH 015/116] chore: reworking the auth providers to decorate the identity from the token that is returned from the different providers Co-authored-by: Johan Haals Signed-off-by: blam --- .../src/layout/SignInPage/providers.tsx | 4 +-- .../src/lib/flow/authFlowHelpers.test.ts | 15 +++++++++ .../src/lib/oauth/OAuthAdapter.test.ts | 10 ++++-- .../src/lib/oauth/OAuthAdapter.ts | 33 ++++++++++++------- plugins/auth-backend/src/lib/oauth/types.ts | 18 +++++++--- .../src/providers/atlassian/provider.ts | 5 ++- .../src/providers/aws-alb/provider.test.ts | 14 ++++++-- .../src/providers/aws-alb/provider.ts | 3 +- .../src/providers/decorateWithIdentity.ts | 2 +- .../src/providers/github/provider.ts | 22 +------------ .../src/providers/saml/provider.ts | 11 ++++++- 11 files changed, 89 insertions(+), 48 deletions(-) diff --git a/packages/core-components/src/layout/SignInPage/providers.tsx b/packages/core-components/src/layout/SignInPage/providers.tsx index 2abe6b3d51..23d6c95b3c 100644 --- a/packages/core-components/src/layout/SignInPage/providers.tsx +++ b/packages/core-components/src/layout/SignInPage/providers.tsx @@ -30,7 +30,7 @@ import { import { commonProvider } from './commonProvider'; import { guestProvider } from './guestProvider'; import { customProvider } from './customProvider'; -import { IdentityApiProxy } from './IdentityApiProxy'; +import { IdentityApiSignOutProxy } from './IdentityApiSignOutProxy'; const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; @@ -92,7 +92,7 @@ export const useSignInProviders = ( const handleWrappedResult = useCallback( (identityApi: IdentityApi) => { onSignInSuccess( - IdentityApiProxy.from({ + IdentityApiSignOutProxy.from({ identityApi, signOut: async () => { localStorage.removeItem(PROVIDER_STORAGE_KEY); diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts index 8b4b95d59b..07c8196dd2 100644 --- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts +++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.test.ts @@ -52,6 +52,11 @@ describe('oauth helpers', () => { backstageIdentity: { id: 'a', token: 'a.b.c', + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'a', + }, }, }, }; @@ -107,6 +112,11 @@ describe('oauth helpers', () => { backstageIdentity: { id: 'a', token: 'a.b.c', + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'a', + }, }, }, }; @@ -149,6 +159,11 @@ describe('oauth helpers', () => { backstageIdentity: { id: 'a', token: 'a.b.c', + identity: { + type: 'user', + ownershipEntityRefs: [], + userEntityRef: 'a', + }, }, }, }; diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index 5e52e60f46..91711f67ad 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -31,7 +31,8 @@ const mockResponseData = { }, backstageIdentity: { id: 'foo', - token: '', + token: + 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', }, }; @@ -218,7 +219,12 @@ describe('OAuthAdapter', () => { ...mockResponseData, backstageIdentity: { id: mockResponseData.backstageIdentity.id, - token: 'my-id-token', + token: mockResponseData.backstageIdentity.token, + identity: { + ownershipEntityRefs: ['user:default/jimmymarkum'], + type: 'user', + userEntityRef: 'jimmymarkum', + }, }, }); }); diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index d6a825f3fd..970fe95b55 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -37,6 +37,7 @@ import { OAuthRefreshRequest, OAuthState, } from './types'; +import { decorateWithIdentity } from '../../providers/decorateWithIdentity'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export const TEN_MINUTES_MS = 600 * 1000; @@ -150,12 +151,12 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { this.setRefreshTokenCookie(res, refreshToken); } - await this.populateIdentity(response.backstageIdentity); + const identity = await this.populateIdentity(response.backstageIdentity); // post message back to popup if successful return postMessageResponse(res, appOrigin, { type: 'authorization_response', - response, + response: { ...response, backstageIdentity: identity }, }); } catch (error) { const { name, message } = isError(error) @@ -209,7 +210,9 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { forwardReq as OAuthRefreshRequest, ); - await this.populateIdentity(response.backstageIdentity); + const backstageIdentity = await this.populateIdentity( + response.backstageIdentity, + ); if ( response.providerInfo.refreshToken && @@ -218,7 +221,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { this.setRefreshTokenCookie(res, response.providerInfo.refreshToken); } - res.status(200).json(response); + res.status(200).json({ ...response, backstageIdentity }); } catch (error) { throw new AuthenticationError('Refresh failed', error); } @@ -228,18 +231,24 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { * If the response from the OAuth provider includes a Backstage identity, we * make sure it's populated with all the information we can derive from the user ID. */ - private async populateIdentity(identity?: BackstageIdentityResponse) { + private async populateIdentity( + identity?: Omit, + ): Promise { if (!identity) { - return; + return undefined; } - if (!identity.token) { - identity.token = await this.options.tokenIssuer.issueToken({ - claims: { sub: identity.id }, - }); - } else if (!identity.token && identity.token) { - identity.token = identity.token; + if (identity.token) { + return decorateWithIdentity(identity); } + + const token = await this.options.tokenIssuer.issueToken({ + claims: { sub: identity.id }, + }); + + console.log(token); + + return decorateWithIdentity({ ...identity, token }); } private setNonceCookie = (res: express.Response, nonce: string) => { diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index 7912dd16a5..f1ff9e763d 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -16,8 +16,11 @@ import express from 'express'; import { Profile as PassportProfile } from 'passport'; -import { AuthResponse, RedirectInfo } from '../../providers/types'; - +import { + AuthResponse, + RedirectInfo, + BackstageIdentityResponse, +} from '../../providers/types'; /** * Common options for passport.js-based OAuth providers */ @@ -47,7 +50,12 @@ export type OAuthResult = { refreshToken?: string; }; -export type OAuthResponse = AuthResponse; +export type OAuthResponse = Omit< + AuthResponse, + 'backstageIdentity' +> & { + backstageIdentity?: Omit; +}; export type OAuthProviderInfo = { /** @@ -108,7 +116,7 @@ export interface OAuthHandlers { * @param {express.Request} req */ handler(req: express.Request): Promise<{ - response: AuthResponse; + response: OAuthResponse; refreshToken?: string; }>; @@ -117,7 +125,7 @@ export interface OAuthHandlers { * @param {string} refreshToken * @param {string} scope */ - refresh?(req: OAuthRefreshRequest): Promise>; + refresh?(req: OAuthRefreshRequest): Promise; /** * (Optional) Sign out of the auth provider. diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts index e19f29a3a4..99e1ebd2d8 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.ts @@ -45,6 +45,7 @@ import express from 'express'; import { TokenIssuer } from '../../identity'; import { CatalogIdentityClient } from '../../lib/catalog'; import { Logger } from 'winston'; +import { decorateWithIdentity } from '../decorateWithIdentity'; export type AtlassianAuthProviderOptions = OAuthProviderOptions & { scopes: string; @@ -136,7 +137,7 @@ export class AtlassianAuthProvider implements OAuthHandlers { }; if (this.signInResolver) { - response.backstageIdentity = await this.signInResolver( + const resolverResponse = await this.signInResolver( { result, profile, @@ -147,6 +148,8 @@ export class AtlassianAuthProvider implements OAuthHandlers { logger: this.logger, }, ); + + response.backstageIdentity = decorateWithIdentity(resolverResponse); } return response; diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts index 3f72004d48..9e4aed2030 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts @@ -122,7 +122,11 @@ describe('AwsAlbAuthProvider', () => { profile: makeProfileInfo(fullProfile), }), signInResolver: async () => { - return { id: 'user.name', token: 'TOKEN' }; + return { + id: 'user.name', + token: + 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', + }; }, }); @@ -133,7 +137,13 @@ describe('AwsAlbAuthProvider', () => { expect(mockResponse.json).toHaveBeenCalledWith({ backstageIdentity: { id: 'user.name', - token: 'TOKEN', + token: + 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', + identity: { + ownershipEntityRefs: ['user:default/jimmymarkum'], + type: 'user', + userEntityRef: 'jimmymarkum', + }, }, profile: { displayName: 'User Name', diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index b0b9070d50..027d5190de 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -32,6 +32,7 @@ import { CatalogIdentityClient } from '../../lib/catalog'; import { Profile as PassportProfile } from 'passport'; import { makeProfileInfo } from '../../lib/passport'; import { AuthenticationError } from '@backstage/errors'; +import { decorateWithIdentity } from '../decorateWithIdentity'; export const ALB_JWT_HEADER = 'x-amzn-oidc-data'; export const ALB_ACCESSTOKEN_HEADER = 'x-amzn-oidc-accesstoken'; @@ -198,7 +199,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { accessToken: result.accessToken, expiresInSeconds: result.expiresInSeconds, }, - backstageIdentity, + backstageIdentity: decorateWithIdentity(backstageIdentity), profile, }; } diff --git a/plugins/auth-backend/src/providers/decorateWithIdentity.ts b/plugins/auth-backend/src/providers/decorateWithIdentity.ts index dab1480db4..76fa97bd81 100644 --- a/plugins/auth-backend/src/providers/decorateWithIdentity.ts +++ b/plugins/auth-backend/src/providers/decorateWithIdentity.ts @@ -33,7 +33,7 @@ export function decorateWithIdentity( identity: { type: 'user', userEntityRef: sub, - ownershipEntityRefs: ent ?? [sub], + ownershipEntityRefs: ent ?? [], }, }; } diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index eb15afa4ab..e9d019f4ac 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -32,7 +32,6 @@ import { AuthHandler, SignInResolver, StateEncoder, - BackstageIdentityResponse, } from '../types'; import { OAuthAdapter, @@ -46,6 +45,7 @@ import { } from '../../lib/oauth'; import { CatalogIdentityClient } from '../../lib/catalog'; import { TokenIssuer } from '../../identity'; +import { decorateWithIdentity } from '../decorateWithIdentity'; type PrivateInfo = { refreshToken?: string; @@ -168,25 +168,6 @@ export class GithubAuthProvider implements OAuthHandlers { }; if (this.signInResolver) { - const decorateWithIdentity = ( - signInResolverResponse: Omit, - ): BackstageIdentityResponse => { - function parseJwtPayload(token: string) { - const [_header, payload, _signature] = token.split('.'); - return JSON.parse(Buffer.from(payload, 'base64').toString()); - } - const { sub, ent } = parseJwtPayload(signInResolverResponse.token); - return { - ...signInResolverResponse, - identity: { - type: 'user', - userEntityRef: sub, - ownershipEntityRefs: ent ?? [sub], - }, - }; - // parse token - // build identity - }; const signInResolverResult = await this.signInResolver( { result, @@ -198,7 +179,6 @@ export class GithubAuthProvider implements OAuthHandlers { logger: this.logger, }, ); - response.backstageIdentity = decorateWithIdentity(signInResolverResult); } diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 3ab00ce401..f760c9e8ee 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -36,8 +36,13 @@ import { import { postMessageResponse } from '../../lib/flow'; import { TokenIssuer } from '../../identity/types'; import { isError } from '@backstage/errors'; +<<<<<<< HEAD import { CatalogIdentityClient } from '../../lib/catalog'; import { Logger } from 'winston'; +import { decorateWithIdentity } from '../decorateWithIdentity'; +======= +import { decorateWithIdentity } from '../decorateWithIdentity'; +>>>>>>> chore: reworking the auth providers to decorate the identity from the token that is returned from the different providers /** @public */ export type SamlAuthResult = { @@ -105,7 +110,7 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { }; if (this.signInResolver) { - response.backstageIdentity = await this.signInResolver( + const signInResponse = await this.signInResolver( { result, profile, @@ -116,8 +121,12 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { logger: this.logger, }, ); + + response.backstageIdentity = decorateWithIdentity(signInResponse); } + + return postMessageResponse(res, this.appUrl, { type: 'authorization_response', response, From b3ac79d7c29f9eaecd3a2f78d9b22d57302af87d Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 1 Dec 2021 16:19:24 +0100 Subject: [PATCH 016/116] chore: updated the api-report for auth backend. probably need to make this a bit better. Signed-off-by: blam --- plugins/auth-backend/api-report.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 2004490261..51d280988a 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -121,7 +121,7 @@ export type BackstageIdentityResponse = { id: string; entity?: Entity; token: string; - identity?: BackstageUserIdentity; + identity: BackstageUserIdentity; }; // @public @@ -453,7 +453,7 @@ export interface OAuthHandlers { // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' handler(req: express.Request): Promise<{ - response: AuthResponse; + response: OAuthResponse; refreshToken?: string; }>; logout?(): Promise; @@ -461,7 +461,7 @@ export interface OAuthHandlers { // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' - refresh?(req: OAuthRefreshRequest): Promise>; + refresh?(req: OAuthRefreshRequest): Promise; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen @@ -499,7 +499,12 @@ export type OAuthRefreshRequest = express.Request<{}> & { // Warning: (ae-missing-release-tag) "OAuthResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type OAuthResponse = AuthResponse; +export type OAuthResponse = Omit< + AuthResponse, + 'backstageIdentity' +> & { + backstageIdentity?: Omit; +}; // Warning: (ae-missing-release-tag) "OAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // From 11a90d6a79e82ea2028eabc6f1ffc4d081fe1b72 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 1 Dec 2021 16:29:14 +0100 Subject: [PATCH 017/116] chore: revert some of the handling Signed-off-by: blam --- plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts | 2 -- plugins/auth-backend/src/providers/google/provider.ts | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index 970fe95b55..b6c6473031 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -246,8 +246,6 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { claims: { sub: identity.id }, }); - console.log(token); - return decorateWithIdentity({ ...identity, token }); } diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 293f738dfd..d85fc2de2d 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -159,7 +159,7 @@ export class GoogleAuthProvider implements OAuthHandlers { }; if (this.signInResolver) { - const signInResolverResult = await this.signInResolver( + response.backstageIdentity = await this.signInResolver( { result, profile, @@ -170,8 +170,6 @@ export class GoogleAuthProvider implements OAuthHandlers { logger: this.logger, }, ); - - console.log(signInResolverResult); } return response; From 3b39323f2631862c690d5c1235395f9d389227a2 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 2 Dec 2021 11:04:33 +0100 Subject: [PATCH 018/116] chore: revert some of the provider changes are they are handled in the OAuthAdapter now Signed-off-by: blam --- .../src/providers/atlassian/provider.ts | 5 +- .../src/providers/github/provider.test.ts | 55 +++---------------- .../src/providers/github/provider.ts | 4 +- 3 files changed, 9 insertions(+), 55 deletions(-) diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts index 99e1ebd2d8..e19f29a3a4 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.ts @@ -45,7 +45,6 @@ import express from 'express'; import { TokenIssuer } from '../../identity'; import { CatalogIdentityClient } from '../../lib/catalog'; import { Logger } from 'winston'; -import { decorateWithIdentity } from '../decorateWithIdentity'; export type AtlassianAuthProviderOptions = OAuthProviderOptions & { scopes: string; @@ -137,7 +136,7 @@ export class AtlassianAuthProvider implements OAuthHandlers { }; if (this.signInResolver) { - const resolverResponse = await this.signInResolver( + response.backstageIdentity = await this.signInResolver( { result, profile, @@ -148,8 +147,6 @@ export class AtlassianAuthProvider implements OAuthHandlers { logger: this.logger, }, ); - - response.backstageIdentity = decorateWithIdentity(resolverResponse); } return response; diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts index 02c3367914..e418ab22c2 100644 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ b/plugins/auth-backend/src/providers/github/provider.test.ts @@ -41,12 +41,7 @@ describe('GithubAuthProvider', () => { const tokenIssuer: TokenIssuer = { listPublicKeys: jest.fn(), async issueToken(params) { - const tokenContents = { - sub: params.claims.sub, - ent: params.claims.ent ?? [], - }; - - return `eyblob.${btoa(JSON.stringify(tokenContents))}.eyblob`; + return `token-for-${params.claims.sub}`; }, }; const catalogIdentityClient = { @@ -98,13 +93,7 @@ describe('GithubAuthProvider', () => { const expected = { backstageIdentity: { id: 'jimmymarkum', - token: - 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', - identity: { - ownershipEntityRefs: ['user:default/jimmymarkum'], - type: 'user', - userEntityRef: 'jimmymarkum', - }, + token: 'token-for-jimmymarkum', }, providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', @@ -149,13 +138,7 @@ describe('GithubAuthProvider', () => { const expected = { backstageIdentity: { id: 'jimmymarkum', - token: - 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', - identity: { - type: 'user', - ownershipEntityRefs: ['user:default/jimmymarkum'], - userEntityRef: 'jimmymarkum', - }, + token: 'token-for-jimmymarkum', }, providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', @@ -198,13 +181,7 @@ describe('GithubAuthProvider', () => { const expected = { backstageIdentity: { id: 'jimmymarkum', - token: - 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', - identity: { - type: 'user', - ownershipEntityRefs: ['user:default/jimmymarkum'], - userEntityRef: 'jimmymarkum', - }, + token: 'token-for-jimmymarkum', }, providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', @@ -247,13 +224,7 @@ describe('GithubAuthProvider', () => { const expected = { backstageIdentity: { id: 'daveboyle', - token: - 'eyblob.eyJzdWIiOiJkYXZlYm95bGUiLCJlbnQiOlsidXNlcjpkZWZhdWx0L2RhdmVib3lsZSJdfQ==.eyblob', - identity: { - type: 'user', - ownershipEntityRefs: ['user:default/daveboyle'], - userEntityRef: 'daveboyle', - }, + token: 'token-for-daveboyle', }, providerInfo: { accessToken: @@ -297,13 +268,7 @@ describe('GithubAuthProvider', () => { response: { backstageIdentity: { id: 'ipd12039', - token: - 'eyblob.eyJzdWIiOiJpcGQxMjAzOSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvaXBkMTIwMzkiXX0=.eyblob', - identity: { - type: 'user', - ownershipEntityRefs: ['user:default/ipd12039'], - userEntityRef: 'ipd12039', - }, + token: 'token-for-ipd12039', }, providerInfo: { accessToken: 'a.b.c', @@ -356,13 +321,7 @@ describe('GithubAuthProvider', () => { expect(response).toEqual({ backstageIdentity: { id: 'mockuser', - token: - 'eyblob.eyJzdWIiOiJtb2NrdXNlciIsImVudCI6WyJ1c2VyOmRlZmF1bHQvbW9ja3VzZXIiXX0=.eyblob', - identity: { - type: 'user', - ownershipEntityRefs: ['user:default/mockuser'], - userEntityRef: 'mockuser', - }, + token: 'token-for-mockuser', }, profile: { displayName: 'Mocked User', diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index e9d019f4ac..c7e82cc5ff 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -45,7 +45,6 @@ import { } from '../../lib/oauth'; import { CatalogIdentityClient } from '../../lib/catalog'; import { TokenIssuer } from '../../identity'; -import { decorateWithIdentity } from '../decorateWithIdentity'; type PrivateInfo = { refreshToken?: string; @@ -168,7 +167,7 @@ export class GithubAuthProvider implements OAuthHandlers { }; if (this.signInResolver) { - const signInResolverResult = await this.signInResolver( + response.backstageIdentity = await this.signInResolver( { result, profile, @@ -179,7 +178,6 @@ export class GithubAuthProvider implements OAuthHandlers { logger: this.logger, }, ); - response.backstageIdentity = decorateWithIdentity(signInResolverResult); } return response; From 7b61f2c3b6d9dbe1c495b5dbe518412cb9937d31 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 2 Dec 2021 14:19:02 +0100 Subject: [PATCH 019/116] Make migrations object required Signed-off-by: Marcus Eide --- .../src/database/DatabaseManager.test.ts | 4 ++-- .../backend-common/src/database/DatabaseManager.ts | 10 +++++----- packages/backend-common/src/database/types.ts | 4 ++-- packages/backend-tasks/src/tasks/TaskScheduler.test.ts | 1 + plugins/auth-backend/src/service/standaloneServer.ts | 1 + plugins/bazaar-backend/src/service/standaloneServer.ts | 2 +- .../src/legacy/service/CatalogBuilder.test.ts | 2 +- .../catalog-backend/src/service/NextCatalogBuilder.ts | 2 +- .../catalog-backend/src/service/standaloneServer.ts | 2 +- .../src/service/standaloneServer.ts | 2 +- .../tech-insights-backend/src/service/router.test.ts | 1 + 11 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/backend-common/src/database/DatabaseManager.test.ts b/packages/backend-common/src/database/DatabaseManager.test.ts index 2905775494..4a44feec4a 100644 --- a/packages/backend-common/src/database/DatabaseManager.test.ts +++ b/packages/backend-common/src/database/DatabaseManager.test.ts @@ -63,7 +63,7 @@ describe('DatabaseManager', () => { const database = DatabaseManager.fromConfig(config); const client = database.forPlugin('test'); - expect(client.migrations?.apply).toBe(true); + expect(client.migrations.apply).toBe(true); }); it('handles migrations options', () => { @@ -73,7 +73,7 @@ describe('DatabaseManager', () => { }); const client = database.forPlugin('test'); - expect(client.migrations?.apply).toBe(false); + expect(client.migrations.apply).toBe(false); }); }); diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index 2c76c0f163..e564f2e0e6 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -37,7 +37,7 @@ function pluginPath(pluginId: string): string { } type Options = { - migrations?: PluginDatabaseManager['migrations']; + migrations: PluginDatabaseManager['migrations']; }; /** @public */ @@ -78,15 +78,15 @@ export class DatabaseManager { */ forPlugin(pluginId: string): PluginDatabaseManager { const _this = this; - const defaultMigrationOptions = { - apply: true, - }; return { getClient(): Promise { return _this.getDatabase(pluginId); }, - migrations: _this.options?.migrations ?? defaultMigrationOptions, + migrations: { + apply: true, + ..._this.options?.migrations, + }, }; } diff --git a/packages/backend-common/src/database/types.ts b/packages/backend-common/src/database/types.ts index 3c5bbf19bb..4cfc86e240 100644 --- a/packages/backend-common/src/database/types.ts +++ b/packages/backend-common/src/database/types.ts @@ -32,9 +32,9 @@ export interface PluginDatabaseManager { getClient(): Promise; /** - * This optional property is used to control the behavior of database migrations. + * This property is used to control the behavior of database migrations. */ - migrations?: { + migrations: { /** * apply can be used to determine if database migrations * should be performed. diff --git a/packages/backend-tasks/src/tasks/TaskScheduler.test.ts b/packages/backend-tasks/src/tasks/TaskScheduler.test.ts index ce8e797503..6c9a6989c7 100644 --- a/packages/backend-tasks/src/tasks/TaskScheduler.test.ts +++ b/packages/backend-tasks/src/tasks/TaskScheduler.test.ts @@ -33,6 +33,7 @@ describe('TaskScheduler', () => { const databaseManager: Partial = { forPlugin: () => ({ getClient: async () => knex, + migrations: { apply: true }, }), }; return databaseManager as DatabaseManager; diff --git a/plugins/auth-backend/src/service/standaloneServer.ts b/plugins/auth-backend/src/service/standaloneServer.ts index 15ffe1d053..9009af4aa6 100644 --- a/plugins/auth-backend/src/service/standaloneServer.ts +++ b/plugins/auth-backend/src/service/standaloneServer.ts @@ -56,6 +56,7 @@ export async function startStandaloneServer( async getClient() { return database; }, + migrations: { apply: true }, }, discovery, }); diff --git a/plugins/bazaar-backend/src/service/standaloneServer.ts b/plugins/bazaar-backend/src/service/standaloneServer.ts index b229f5bcf8..4ef46b7f66 100644 --- a/plugins/bazaar-backend/src/service/standaloneServer.ts +++ b/plugins/bazaar-backend/src/service/standaloneServer.ts @@ -52,7 +52,7 @@ export async function startStandaloneServer( const router = await createRouter({ logger, - database: { getClient: async () => db }, + database: { getClient: async () => db, migrations: { apply: true } }, config: config, }); diff --git a/plugins/catalog-backend/src/legacy/service/CatalogBuilder.test.ts b/plugins/catalog-backend/src/legacy/service/CatalogBuilder.test.ts index 926aa67635..3ba9340716 100644 --- a/plugins/catalog-backend/src/legacy/service/CatalogBuilder.test.ts +++ b/plugins/catalog-backend/src/legacy/service/CatalogBuilder.test.ts @@ -49,7 +49,7 @@ describe('CatalogBuilder', () => { }; const env: CatalogEnvironment = { logger: getVoidLogger(), - database: { getClient: async () => db }, + database: { getClient: async () => db, migrations: { apply: true } }, config: new ConfigReader({}), reader, }; diff --git a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts index 712ab0ae44..382cac362e 100644 --- a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts @@ -336,7 +336,7 @@ export class NextCatalogBuilder { const parser = this.parser || defaultEntityDataParser; const dbClient = await database.getClient(); - if (database.migrations?.apply) { + if (database.migrations.apply) { logger.info('Performing database migration'); await applyDatabaseMigrations(dbClient); } diff --git a/plugins/catalog-backend/src/service/standaloneServer.ts b/plugins/catalog-backend/src/service/standaloneServer.ts index 7aae3cd47c..66154b0ddc 100644 --- a/plugins/catalog-backend/src/service/standaloneServer.ts +++ b/plugins/catalog-backend/src/service/standaloneServer.ts @@ -46,7 +46,7 @@ export async function startStandaloneServer( logger.debug('Creating application...'); const builder = new CatalogBuilder({ logger, - database: { getClient: () => db }, + database: { getClient: () => db, migrations: { apply: true } }, config, reader, }); diff --git a/plugins/code-coverage-backend/src/service/standaloneServer.ts b/plugins/code-coverage-backend/src/service/standaloneServer.ts index 291f78ffc5..ca913a2a67 100644 --- a/plugins/code-coverage-backend/src/service/standaloneServer.ts +++ b/plugins/code-coverage-backend/src/service/standaloneServer.ts @@ -54,7 +54,7 @@ export async function startStandaloneServer( logger.debug('Starting application server...'); const router = await createRouter({ - database: { getClient: async () => db }, + database: { getClient: async () => db, migrations: { apply: true } }, config, discovery: SingleHostDiscovery.fromConfig(config), urlReader: UrlReaders.default({ logger, config }), diff --git a/plugins/tech-insights-backend/src/service/router.test.ts b/plugins/tech-insights-backend/src/service/router.test.ts index 0b7d3b7c45..b435136d4d 100644 --- a/plugins/tech-insights-backend/src/service/router.test.ts +++ b/plugins/tech-insights-backend/src/service/router.test.ts @@ -53,6 +53,7 @@ describe('Tech Insights router tests', () => { }, }) as unknown as Promise; }, + migrations: { apply: true }, }, logger: getVoidLogger(), factRetrievers: [], From 259922bfb27c92d0f93e89ab7316b254cece2788 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 2 Dec 2021 14:30:49 +0100 Subject: [PATCH 020/116] Update api-reports Signed-off-by: Marcus Eide --- packages/backend-common/api-report.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 9c66c0573e..09a95b00ea 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -175,7 +175,8 @@ export function createStatusCheckRouter(options: { // @public (undocumented) export class DatabaseManager { forPlugin(pluginId: string): PluginDatabaseManager; - static fromConfig(config: Config): DatabaseManager; + // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts + static fromConfig(config: Config, options?: Options): DatabaseManager; } // @public (undocumented) @@ -395,6 +396,9 @@ export type PluginCacheManager = { // @public export interface PluginDatabaseManager { getClient(): Promise; + migrations: { + apply: boolean; + }; } // @public @@ -642,4 +646,8 @@ export function useHotCleanup( // @public export function useHotMemoize(_module: NodeModule, valueFactory: () => T): T; + +// Warnings were encountered during analysis: +// +// src/database/types.d.ts:26:12 - (tsdoc-undefined-tag) The TSDoc tag "@default" is not defined in this configuration ``` From 98a9c35f0cdd668f23f652b94587e967494ac023 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Thu, 2 Dec 2021 15:38:56 +0100 Subject: [PATCH 021/116] Add changeset Signed-off-by: Marcus Eide --- .changeset/old-dingos-shave.md | 5 +++++ .changeset/seven-rabbits-shave.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/old-dingos-shave.md create mode 100644 .changeset/seven-rabbits-shave.md diff --git a/.changeset/old-dingos-shave.md b/.changeset/old-dingos-shave.md new file mode 100644 index 0000000000..3263a89c0d --- /dev/null +++ b/.changeset/old-dingos-shave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Honor database migration configuration diff --git a/.changeset/seven-rabbits-shave.md b/.changeset/seven-rabbits-shave.md new file mode 100644 index 0000000000..9e14cde2e7 --- /dev/null +++ b/.changeset/seven-rabbits-shave.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Add options argument to support additional database migrations configuration From acbb4cedd4c9b40f7392f6403641eda9aac74f7e Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 3 Dec 2021 10:08:04 +0100 Subject: [PATCH 022/116] chore: fix derpy merge conflicts Signed-off-by: blam --- plugins/auth-backend/src/providers/saml/provider.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index f760c9e8ee..d0f7791aaa 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -36,13 +36,9 @@ import { import { postMessageResponse } from '../../lib/flow'; import { TokenIssuer } from '../../identity/types'; import { isError } from '@backstage/errors'; -<<<<<<< HEAD import { CatalogIdentityClient } from '../../lib/catalog'; import { Logger } from 'winston'; import { decorateWithIdentity } from '../decorateWithIdentity'; -======= -import { decorateWithIdentity } from '../decorateWithIdentity'; ->>>>>>> chore: reworking the auth providers to decorate the identity from the token that is returned from the different providers /** @public */ export type SamlAuthResult = { @@ -125,8 +121,6 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { response.backstageIdentity = decorateWithIdentity(signInResponse); } - - return postMessageResponse(res, this.appUrl, { type: 'authorization_response', response, From 388fd9bb7f1ec165ed938ab7abe314f97d404e32 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 3 Dec 2021 10:21:01 +0100 Subject: [PATCH 023/116] chore: fixing some code review comments Signed-off-by: blam --- .../core-components/src/layout/SignInPage/SignInPage.tsx | 2 +- .../core-components/src/layout/SignInPage/UserIdentity.ts | 4 ++-- .../src/layout/SignInPage/auth0Provider.tsx | 4 ++-- .../src/layout/SignInPage/commonProvider.tsx | 4 ++-- .../src/layout/SignInPage/customProvider.tsx | 8 +++----- packages/core-plugin-api/src/apis/definitions/auth.ts | 2 +- plugins/auth-backend/src/providers/index.ts | 2 ++ plugins/auth-backend/src/providers/oidc/provider.ts | 2 ++ 8 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/core-components/src/layout/SignInPage/SignInPage.tsx b/packages/core-components/src/layout/SignInPage/SignInPage.tsx index 3a068decd5..304e427f33 100644 --- a/packages/core-components/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core-components/src/layout/SignInPage/SignInPage.tsx @@ -136,7 +136,7 @@ export const SingleSignInPage = ({ const profile = await authApi.getProfile(); onSignInSuccess( - UserIdentity.from({ + UserIdentity.create({ identity: identityResponse.identity, authApi, profile, diff --git a/packages/core-components/src/layout/SignInPage/UserIdentity.ts b/packages/core-components/src/layout/SignInPage/UserIdentity.ts index 088f76e60e..1461c48a49 100644 --- a/packages/core-components/src/layout/SignInPage/UserIdentity.ts +++ b/packages/core-components/src/layout/SignInPage/UserIdentity.ts @@ -32,11 +32,11 @@ export class UserIdentity implements IdentityApi { return new GuestUserIdentity(); } - static fromLegacy({ result }: { result: SignInResult }) { + static fromLegacy(result: SignInResult) { return LegacyUserIdentity.fromResult(result); } - static from(options: { + static create(options: { identity: BackstageUserIdentity; authApi: ProfileInfoApi & BackstageIdentityApi & SessionApi; /** diff --git a/packages/core-components/src/layout/SignInPage/auth0Provider.tsx b/packages/core-components/src/layout/SignInPage/auth0Provider.tsx index 8e2728a95e..739c3709a9 100644 --- a/packages/core-components/src/layout/SignInPage/auth0Provider.tsx +++ b/packages/core-components/src/layout/SignInPage/auth0Provider.tsx @@ -46,7 +46,7 @@ const Component: ProviderComponent = ({ onSignInSuccess }) => { const profile = await auth0AuthApi.getProfile(); onSignInSuccess( - UserIdentity.from({ + UserIdentity.create({ identity: identityResponse.identity, authApi: auth0AuthApi, profile, @@ -85,7 +85,7 @@ const loader: ProviderLoader = async apis => { } const profile = await auth0AuthApi.getProfile(); - return UserIdentity.from({ + return UserIdentity.create({ identity: identityResponse.identity, authApi: auth0AuthApi, profile, diff --git a/packages/core-components/src/layout/SignInPage/commonProvider.tsx b/packages/core-components/src/layout/SignInPage/commonProvider.tsx index 48dd7df62c..8f426a3907 100644 --- a/packages/core-components/src/layout/SignInPage/commonProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/commonProvider.tsx @@ -48,7 +48,7 @@ const Component: ProviderComponent = ({ config, onSignInSuccess }) => { const profile = await authApi.getProfile(); onSignInSuccess( - UserIdentity.from({ + UserIdentity.create({ identity: identityResponse.identity, profile, authApi, @@ -89,7 +89,7 @@ const loader: ProviderLoader = async (apis, apiRef) => { const profile = await authApi.getProfile(); - return UserIdentity.from({ + return UserIdentity.create({ identity: identityResponse.identity, profile, authApi, diff --git a/packages/core-components/src/layout/SignInPage/customProvider.tsx b/packages/core-components/src/layout/SignInPage/customProvider.tsx index 9863234a06..13ba906079 100644 --- a/packages/core-components/src/layout/SignInPage/customProvider.tsx +++ b/packages/core-components/src/layout/SignInPage/customProvider.tsx @@ -72,11 +72,9 @@ const Component: ProviderComponent = ({ onSignInSuccess }) => { const handleResult = ({ userId }: Data) => { onSignInSuccess( UserIdentity.fromLegacy({ - result: { - userId, - profile: { - email: `${userId}@example.com`, - }, + userId, + profile: { + email: `${userId}@example.com`, }, }), ); diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index 8da7fbe6fd..e093a6b29b 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -188,7 +188,7 @@ export type BackstageUserIdentity = { }; /** - * A (user id, token) pair. + * Token and Identity response, with the users claims in the Identity. * * @public */ diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 5b4fbc6338..34c2ff06c2 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -44,3 +44,5 @@ export type { BackstageIdentityResponse, ProfileInfo, } from './types'; + +export { decorateWithIdentity } from './decorateWithIdentity'; diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 2e58111868..158af6f830 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -205,6 +205,8 @@ export class OidcAuthProvider implements OAuthHandlers { }, ); } + + return response; } } From a76dacda24fe414f550e06d9b8efa84f1542e74a Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 3 Dec 2021 10:24:01 +0100 Subject: [PATCH 024/116] chore: fix up the api reports Signed-off-by: blam --- packages/core-components/api-report.md | 14 +++++++------- plugins/auth-backend/api-report.md | 7 +++++++ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index f237932baa..31008624db 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -2325,20 +2325,20 @@ export function UserIcon(props: IconComponentProps): JSX.Element; // // @public (undocumented) export class UserIdentity implements IdentityApi { - // Warning: (ae-forgotten-export) The symbol "GuestUserIdentity" needs to be exported by the entry point index.d.ts - // // (undocumented) - static createGuest(): GuestUserIdentity; - // (undocumented) - static from(options: { + static create(options: { identity: BackstageUserIdentity; authApi: ProfileInfoApi & BackstageIdentityApi & SessionApi; profile?: ProfileInfo; }): UserIdentity; + // Warning: (ae-forgotten-export) The symbol "GuestUserIdentity" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static createGuest(): GuestUserIdentity; // Warning: (ae-forgotten-export) The symbol "LegacyUserIdentity" needs to be exported by the entry point index.d.ts // // (undocumented) - static fromLegacy({ result }: { result: SignInResult }): LegacyUserIdentity; + static fromLegacy(result: SignInResult): LegacyUserIdentity; // (undocumented) getBackstageIdentity(): Promise; // (undocumented) @@ -2400,5 +2400,5 @@ export type WarningPanelClassKey = // src/components/TabbedLayout/RoutedTabs.d.ts:9:5 - (ae-forgotten-export) The symbol "SubRoute" needs to be exported by the entry point index.d.ts // src/components/Table/Table.d.ts:20:5 - (ae-forgotten-export) The symbol "SelectedFilters" needs to be exported by the entry point index.d.ts // src/layout/ErrorBoundary/ErrorBoundary.d.ts:8:5 - (ae-forgotten-export) The symbol "SlackChannel" needs to be exported by the entry point index.d.ts -// src/layout/SignInPage/UserIdentity.d.ts:22:9 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "IdentityApi" +// src/layout/SignInPage/UserIdentity.d.ts:20:9 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "IdentityApi" ``` diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 51d280988a..dffb0414db 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -288,6 +288,13 @@ export const createSamlProvider: ( options?: SamlProviderOptions | undefined, ) => AuthProviderFactory; +// Warning: (ae-missing-release-tag) "decorateWithIdentity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export function decorateWithIdentity( + signInResolverResponse: Omit, +): BackstageIdentityResponse; + // Warning: (ae-missing-release-tag) "factories" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) From e0f5814037453d49e74a1804c7a7eae37cdf4345 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 3 Dec 2021 10:33:05 +0100 Subject: [PATCH 025/116] chore: more code review comments Signed-off-by: blam --- .../core-components/src/layout/SignInPage/LegacyUserIdentity.ts | 2 +- plugins/auth-backend/src/providers/decorateWithIdentity.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts b/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts index ff6d38c860..e28f94072e 100644 --- a/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts +++ b/packages/core-components/src/layout/SignInPage/LegacyUserIdentity.ts @@ -65,7 +65,7 @@ export class LegacyUserIdentity implements IdentityApi { return { type: 'user', userEntityRef: sub, - ownershipEntityRefs: ent ?? [sub], + ownershipEntityRefs: ent ?? [], }; } diff --git a/plugins/auth-backend/src/providers/decorateWithIdentity.ts b/plugins/auth-backend/src/providers/decorateWithIdentity.ts index 76fa97bd81..0e0756f8a8 100644 --- a/plugins/auth-backend/src/providers/decorateWithIdentity.ts +++ b/plugins/auth-backend/src/providers/decorateWithIdentity.ts @@ -22,6 +22,8 @@ function parseJwtPayload(token: string) { } /** + * @public + * * Parses token and decorates the BackstageIdentityResponse with identity information sourced from the token */ export function decorateWithIdentity( From e847694fbbd3e5d4180056133c1dc75ad61c9265 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 3 Dec 2021 14:10:00 +0100 Subject: [PATCH 026/116] chore: rebuild api-report now I fixed some issues Signed-off-by: blam --- plugins/auth-backend/api-report.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index dffb0414db..7d776986a7 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -288,8 +288,6 @@ export const createSamlProvider: ( options?: SamlProviderOptions | undefined, ) => AuthProviderFactory; -// Warning: (ae-missing-release-tag) "decorateWithIdentity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export function decorateWithIdentity( signInResolverResponse: Omit, From 7d1a522abc464a53b6f8fd6d2a27392c5ff383cf Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sun, 31 Oct 2021 16:49:16 -0500 Subject: [PATCH 027/116] Added getting builds by definition name Signed-off-by: Andre Wanlin --- .../src/api/AzureDevOpsApi.ts | 72 ++++++++++++++-- .../src/service/router.test.ts | 85 ++++++++++++++++++- .../src/service/router.ts | 27 ++++++ 3 files changed, 175 insertions(+), 9 deletions(-) diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts index 53dc19c68d..7acc5fc066 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts @@ -35,7 +35,7 @@ import { getArtifactId, } from '../utils'; -import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces'; +import { Build, BuildDefinitionReference } from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { Logger } from 'winston'; import { PolicyEvaluationRecord } from 'azure-devops-node-api/interfaces/PolicyInterfaces'; import { TeamMember } from 'azure-devops-node-api/interfaces/common/VSSInterfaces'; @@ -60,19 +60,46 @@ export class AzureDevOpsApi { return client.getRepository(repoName, projectName); } - public async getBuildList( + public async getBuildDefinitions( projectName: string, - repoId: string, - top: number, - ): Promise { + definitionName: string, + ): Promise { this.logger?.debug( - `Calling Azure DevOps REST API, getting up to ${top} Builds for Repository Id ${repoId} for Project ${projectName}`, + `Calling Azure DevOps REST API, getting Build Definitions for ${definitionName} in Project ${projectName}`, ); + const client = await this.webApi.getBuildApi(); + return client.getDefinitions( + projectName, + definitionName, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ); + } + + public async getBuildList( + projectName: string, + definitions?: number[], + repoId?: string, + top?: number, + ): Promise { const client = await this.webApi.getBuildApi(); return client.getBuilds( projectName, - undefined, + definitions, undefined, undefined, undefined, @@ -91,7 +118,7 @@ export class AzureDevOpsApi { undefined, undefined, repoId, - 'TfsGit', + repoId ? 'TfsGit' : undefined, ); } @@ -107,6 +134,7 @@ export class AzureDevOpsApi { const gitRepository = await this.getGitRepository(projectName, repoName); const buildList = await this.getBuildList( projectName, + undefined, gitRepository.id as string, top, ); @@ -118,6 +146,34 @@ export class AzureDevOpsApi { return repoBuilds; } + public async getDefinitionBuilds( + projectName: string, + definitionName: string, + top: number, + ) { + this.logger?.debug( + `Calling Azure DevOps REST API, getting up to ${top} Builds for ${definitionName} in Project ${projectName}`, + ); + + const buildDefinitions = await this.getBuildDefinitions( + projectName, + definitionName, + ); + const definitions = buildDefinitions.map(bd => bd.id) as number[]; + const buildList = await this.getBuildList( + projectName, + definitions, + undefined, + top, + ); + + const repoBuilds: RepoBuild[] = buildList.map(build => { + return mappedRepoBuild(build); + }); + + return repoBuilds; + } + public async getPullRequests( projectName: string, repoName: string, diff --git a/plugins/azure-devops-backend/src/service/router.test.ts b/plugins/azure-devops-backend/src/service/router.test.ts index ce5f712392..82d4df1265 100644 --- a/plugins/azure-devops-backend/src/service/router.test.ts +++ b/plugins/azure-devops-backend/src/service/router.test.ts @@ -23,7 +23,7 @@ import { } from '@backstage/plugin-azure-devops-common'; import { AzureDevOpsApi } from '../api'; -import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces'; +import { Build, BuildDefinitionReference } from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { ConfigReader } from '@backstage/config'; import { GitRepository } from 'azure-devops-node-api/interfaces/GitInterfaces'; import { createRouter } from './router'; @@ -39,7 +39,9 @@ describe('createRouter', () => { azureDevOpsApi = { getGitRepository: jest.fn(), getBuildList: jest.fn(), + getBuildDefinitions: jest.fn(), getRepoBuilds: jest.fn(), + getDefinitionBuilds: jest.fn(), getPullRequests: jest.fn(), } as any; const router = await createRouter({ @@ -136,6 +138,7 @@ describe('createRouter', () => { expect(azureDevOpsApi.getBuildList).toHaveBeenCalledWith( 'myProject', + undefined, 'af4ae3af-e747-4129-9bbc-d1329f6b0998', 40, ); @@ -144,6 +147,32 @@ describe('createRouter', () => { }); }); + describe('GET /build-definitions/:projectName/:definitionName', () => { + it('fetches a list of build definitions', async () => { + const inputDefinition: BuildDefinitionReference = { + id: 1, + name: 'myBuildDefinition', + }; + + const inputDefinitions: BuildDefinitionReference[] = [inputDefinition]; + + azureDevOpsApi.getBuildDefinitions.mockResolvedValueOnce( + inputDefinitions, + ); + + const response = await request(app).get( + '/build-definitions/myProject/myBuildDefinition', + ); + + expect(azureDevOpsApi.getBuildDefinitions).toHaveBeenCalledWith( + 'myProject', + 'myBuildDefinition', + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(inputDefinitions); + }); + }); + describe('GET /repo-builds/:projectName/:repoName', () => { it('fetches a list of repo builds', async () => { const firstRepoBuild: RepoBuild = { @@ -198,6 +227,60 @@ describe('createRouter', () => { }); }); + describe('GET /definition-builds/:projectName/:definitionName', () => { + it('fetches a list of repo builds', async () => { + const firstRepoBuild: RepoBuild = { + id: 1, + title: 'My Build Definition - Build 1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.PartiallySucceeded, + queueTime: undefined, + source: 'refs/heads/develop (f4f78b31)', + }; + + const secondRepoBuild: RepoBuild = { + id: 2, + title: 'My Build Definition - Build 2', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=2', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: undefined, + source: 'refs/heads/develop (13c988d4)', + }; + + const thirdRepoBuild: RepoBuild = { + id: 3, + title: 'My Build Definition - Build 3', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=3', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: undefined, + source: 'refs/heads/develop (9bedf678)', + }; + + const repoBuilds: RepoBuild[] = [ + firstRepoBuild, + secondRepoBuild, + thirdRepoBuild, + ]; + + azureDevOpsApi.getDefinitionBuilds.mockResolvedValueOnce(repoBuilds); + + const response = await request(app) + .get('/definition-builds/myProject/myDefinition') + .query({ top: '30' }); + + expect(azureDevOpsApi.getDefinitionBuilds).toHaveBeenCalledWith( + 'myProject', + 'myDefinition', + 30, + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(repoBuilds); + }); + }); + describe('GET /pull-requests/:projectName/:repoName', () => { it('fetches a list of pull requests', async () => { const firstPullRequest: PullRequest = { diff --git a/plugins/azure-devops-backend/src/service/router.ts b/plugins/azure-devops-backend/src/service/router.ts index 57d54df18f..5e6369bb11 100644 --- a/plugins/azure-devops-backend/src/service/router.ts +++ b/plugins/azure-devops-backend/src/service/router.ts @@ -73,12 +73,25 @@ export async function createRouter( const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP; const buildList = await azureDevOpsApi.getBuildList( projectName, + undefined, repoId, top, ); res.status(200).json(buildList); }); + router.get( + '/build-definitions/:projectName/:definitionName', + async (req, res) => { + const { projectName, definitionName } = req.params; + const buildDefinitionList = await azureDevOpsApi.getBuildDefinitions( + projectName, + definitionName, + ); + res.status(200).json(buildDefinitionList); + }, + ); + router.get('/repo-builds/:projectName/:repoName', async (req, res) => { const { projectName, repoName } = req.params; @@ -93,6 +106,20 @@ export async function createRouter( res.status(200).json(gitRepository); }); + router.get( + '/definition-builds/:projectName/:definitionName', + async (req, res) => { + const { projectName, definitionName } = req.params; + const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP; + const gitRepository = await azureDevOpsApi.getDefinitionBuilds( + projectName, + definitionName, + top, + ); + res.status(200).json(gitRepository); + }, + ); + router.get('/pull-requests/:projectName/:repoName', async (req, res) => { const { projectName, repoName } = req.params; From a77526afcdb23a019a6a925552bde4b02f9b2178 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sun, 31 Oct 2021 16:54:08 -0500 Subject: [PATCH 028/116] Added changeset and updated API Report Signed-off-by: Andre Wanlin --- .changeset/rare-toes-burn.md | 5 +++++ plugins/azure-devops-backend/api-report.md | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .changeset/rare-toes-burn.md diff --git a/.changeset/rare-toes-burn.md b/.changeset/rare-toes-burn.md new file mode 100644 index 0000000000..2524df1daa --- /dev/null +++ b/.changeset/rare-toes-burn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-azure-devops-backend': patch +--- + +Added getting builds by definition name diff --git a/plugins/azure-devops-backend/api-report.md b/plugins/azure-devops-backend/api-report.md index 19d96dc728..9fd1e43bf6 100644 --- a/plugins/azure-devops-backend/api-report.md +++ b/plugins/azure-devops-backend/api-report.md @@ -25,8 +25,9 @@ export class AzureDevOpsApi { // (undocumented) getBuildList( projectName: string, - repoId: string, - top: number, + definitions?: number[], + repoId?: string, + top?: number, ): Promise; // (undocumented) getDashboardPullRequests( From 868f1dae99086ebeb43f96282d746a41e6ad6664 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 13 Nov 2021 11:25:46 -0600 Subject: [PATCH 029/116] Refactored to provide better API for Builds Signed-off-by: Andre Wanlin --- plugins/azure-devops-backend/api-report.md | 7 +- .../src/api/AzureDevOpsApi.test.ts | 691 ++++++++++++------ .../src/api/AzureDevOpsApi.ts | 190 +++-- .../src/service/router.test.ts | 227 +++--- .../src/service/router.ts | 53 +- plugins/azure-devops-common/api-report.md | 23 + plugins/azure-devops-common/src/types.ts | 16 + 7 files changed, 801 insertions(+), 406 deletions(-) diff --git a/plugins/azure-devops-backend/api-report.md b/plugins/azure-devops-backend/api-report.md index 9fd1e43bf6..cbb512d00e 100644 --- a/plugins/azure-devops-backend/api-report.md +++ b/plugins/azure-devops-backend/api-report.md @@ -4,6 +4,8 @@ ```ts import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces'; +import { BuildDefinitionReference } from 'azure-devops-node-api/interfaces/BuildInterfaces'; +import { BuildRun } from '@backstage/plugin-azure-devops-common'; import { Config } from '@backstage/config'; import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; import express from 'express'; @@ -25,9 +27,8 @@ export class AzureDevOpsApi { // (undocumented) getBuildList( projectName: string, - definitions?: number[], - repoId?: string, - top?: number, + repoId: string, + top: number, ): Promise; // (undocumented) getDashboardPullRequests( diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts index 2cb7d93c0b..76b5089bbc 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.test.ts @@ -29,277 +29,510 @@ import { GitPullRequest, GitRepository, } from 'azure-devops-node-api/interfaces/GitInterfaces'; -import { mappedPullRequest, mappedRepoBuild } from './AzureDevOpsApi'; +import { + mappedBuildRun, + mappedPullRequest, + mappedRepoBuild, +} from './AzureDevOpsApi'; import { IdentityRef } from 'azure-devops-node-api/interfaces/common/VSSInterfaces'; describe('AzureDevOpsApi', () => { describe('mappedRepoBuild', () => { - it('should return RepoBuild from Build', () => { - const inputBuildDefinition: DefinitionReference = { - name: 'My Build Definition', - }; + describe('mappedRepoBuild happy path', () => { + it('should return RepoBuild from Build', () => { + const inputBuildDefinition: DefinitionReference = { + name: 'My Build Definition', + }; - const inputLinks: any = { - web: { - href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - }, - }; + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; - const inputIdentityRef: IdentityRef = { - displayName: 'Jane Doe', - uniqueName: 'DOMAIN\\jdoe', - }; + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; - const inputBuild: Build = { - id: 1, - buildNumber: 'Build-1', - status: BuildStatus.Completed, - result: BuildResult.Succeeded, - queueTime: new Date('2020-09-12T06:10:23.932Z'), - startTime: new Date('2020-09-12T06:15:23.932Z'), - finishTime: new Date('2020-09-12T06:20:23.932Z'), - sourceBranch: 'refs/heads/develop', - sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', - definition: inputBuildDefinition, - _links: inputLinks, - requestedFor: inputIdentityRef, - }; + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: inputBuildDefinition, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; - const outputRepoBuild: RepoBuild = { - id: 1, - title: 'My Build Definition - Build-1', - link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - status: BuildStatus.Completed, - result: BuildResult.Succeeded, - queueTime: '2020-09-12T06:10:23.932Z', - startTime: '2020-09-12T06:15:23.932Z', - finishTime: '2020-09-12T06:20:23.932Z', - source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAIN\\jdoe', - }; + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'My Build Definition - Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; - expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + }); }); - }); - describe('mappedRepoBuild with no Build definition name', () => { - it('should return RepoBuild with only Build Number for title', () => { - const inputLinks: any = { - web: { - href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - }, - }; + describe('mappedRepoBuild with no Build definition name', () => { + it('should return RepoBuild with only Build Number for title', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; - const inputIdentityRef: IdentityRef = { - displayName: 'Jane Doe', - uniqueName: 'DOMAIN\\jdoe', - }; + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; - const inputBuild: Build = { - id: 1, - buildNumber: 'Build-1', - status: BuildStatus.Completed, - result: BuildResult.Succeeded, - queueTime: new Date('2020-09-12T06:10:23.932Z'), - startTime: new Date('2020-09-12T06:15:23.932Z'), - finishTime: new Date('2020-09-12T06:20:23.932Z'), - sourceBranch: 'refs/heads/develop', - sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', - definition: undefined, - _links: inputLinks, - requestedFor: inputIdentityRef, - }; + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; - const outputRepoBuild: RepoBuild = { - id: 1, - title: 'Build-1', - link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - status: BuildStatus.Completed, - result: BuildResult.Succeeded, - queueTime: '2020-09-12T06:10:23.932Z', - startTime: '2020-09-12T06:15:23.932Z', - finishTime: '2020-09-12T06:20:23.932Z', - source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAIN\\jdoe', - }; + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; - expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + }); }); - }); - describe('mappedRepoBuild with undefined status', () => { - it('should return BuildStatus of None for status', () => { - const inputLinks: any = { - web: { - href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - }, - }; + describe('mappedRepoBuild with undefined status', () => { + it('should return BuildStatus of None for status', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; - const inputIdentityRef: IdentityRef = { - displayName: 'Jane Doe', - uniqueName: 'DOMAIN\\jdoe', - }; + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; - const inputBuild: Build = { - id: 1, - buildNumber: 'Build-1', - status: undefined, - result: BuildResult.Succeeded, - queueTime: new Date('2020-09-12T06:10:23.932Z'), - startTime: new Date('2020-09-12T06:15:23.932Z'), - finishTime: new Date('2020-09-12T06:20:23.932Z'), - sourceBranch: 'refs/heads/develop', - sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', - definition: undefined, - _links: inputLinks, - requestedFor: inputIdentityRef, - }; + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: undefined, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; - const outputRepoBuild: RepoBuild = { - id: 1, - title: 'Build-1', - link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - status: BuildStatus.None, - result: BuildResult.Succeeded, - queueTime: '2020-09-12T06:10:23.932Z', - startTime: '2020-09-12T06:15:23.932Z', - finishTime: '2020-09-12T06:20:23.932Z', - source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAIN\\jdoe', - }; + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.None, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; - expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + }); }); - }); - describe('mappedRepoBuild with undefined result', () => { - it('should return BuildResult of None for result', () => { - const inputLinks: any = { - web: { - href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - }, - }; + describe('mappedRepoBuild with undefined result', () => { + it('should return BuildResult of None for result', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; - const inputIdentityRef: IdentityRef = { - displayName: 'Jane Doe', - uniqueName: 'DOMAIN\\jdoe', - }; + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; - const inputBuild: Build = { - id: 1, - buildNumber: 'Build-1', - status: BuildStatus.InProgress, - result: undefined, - queueTime: new Date('2020-09-12T06:10:23.932Z'), - startTime: new Date('2020-09-12T06:15:23.932Z'), - finishTime: new Date('2020-09-12T06:20:23.932Z'), - sourceBranch: 'refs/heads/develop', - sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', - definition: undefined, - _links: inputLinks, - requestedFor: inputIdentityRef, - }; + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.InProgress, + result: undefined, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; - const outputRepoBuild: RepoBuild = { - id: 1, - title: 'Build-1', - link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - status: BuildStatus.InProgress, - result: BuildResult.None, - queueTime: '2020-09-12T06:10:23.932Z', - startTime: '2020-09-12T06:15:23.932Z', - finishTime: '2020-09-12T06:20:23.932Z', - source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAIN\\jdoe', - }; + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; - expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + }); }); - }); - describe('mappedRepoBuild with undefined link', () => { - it('should return empty string for link', () => { - const inputIdentityRef: IdentityRef = { - displayName: 'Jane Doe', - uniqueName: 'DOMAIN\\jdoe', - }; + describe('mappedRepoBuild with undefined link', () => { + it('should return empty string for link', () => { + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; - const inputBuild: Build = { - id: 1, - buildNumber: 'Build-1', - status: BuildStatus.InProgress, - result: undefined, - queueTime: new Date('2020-09-12T06:10:23.932Z'), - startTime: new Date('2020-09-12T06:15:23.932Z'), - finishTime: new Date('2020-09-12T06:20:23.932Z'), - sourceBranch: 'refs/heads/develop', - sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', - definition: undefined, - _links: undefined, - requestedFor: inputIdentityRef, - }; + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.InProgress, + result: undefined, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: undefined, + requestedFor: inputIdentityRef, + }; - const outputRepoBuild: RepoBuild = { - id: 1, - title: 'Build-1', - link: '', - status: BuildStatus.InProgress, - result: BuildResult.None, - queueTime: '2020-09-12T06:10:23.932Z', - startTime: '2020-09-12T06:15:23.932Z', - finishTime: '2020-09-12T06:20:23.932Z', - source: 'refs/heads/develop (f4f78b31)', - uniqueName: 'DOMAIN\\jdoe', - }; + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: '', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; - expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + expect(mappedRepoBuild(inputBuild)).toEqual(outputRepoBuild); + }); }); }); describe('mappedPullRequest', () => { - it('should return PullRequest from GitPullRequest', () => { - const inputGitRepository: GitRepository = { - name: 'super-feature-repo', - }; + describe('mappedPullRequest happy path', () => { + it('should return PullRequest from GitPullRequest', () => { + const inputGitRepository: GitRepository = { + name: 'super-feature-repo', + }; - const inputIdentityRef: IdentityRef = { - displayName: 'Jane Doe', - uniqueName: 'DOMAIN\\jdoe', - }; + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; - const inputPullRequest: GitPullRequest = { - pullRequestId: 7181, - repository: inputGitRepository, - title: 'My Awesome New Feature', - createdBy: inputIdentityRef, - creationDate: new Date('2020-09-12T06:10:23.932Z'), - sourceRefName: 'refs/heads/topic/super-awesome-feature', - targetRefName: 'refs/heads/main', - status: PullRequestStatus.Active, - isDraft: false, - }; + const inputPullRequest: GitPullRequest = { + pullRequestId: 7181, + repository: inputGitRepository, + title: 'My Awesome New Feature', + createdBy: inputIdentityRef, + creationDate: new Date('2020-09-12T06:10:23.932Z'), + sourceRefName: 'refs/heads/topic/super-awesome-feature', + targetRefName: 'refs/heads/main', + status: PullRequestStatus.Active, + isDraft: false, + }; - const inputBaseUrl = - 'https://host.com/myOrg/_git/super-feature-repo/pullrequest'; + const inputBaseUrl = + 'https://host.com/myOrg/_git/super-feature-repo/pullrequest'; - const outputPullRequest: PullRequest = { - pullRequestId: 7181, - repoName: 'super-feature-repo', - title: 'My Awesome New Feature', - uniqueName: 'DOMAIN\\jdoe', - createdBy: 'Jane Doe', - creationDate: '2020-09-12T06:10:23.932Z', - sourceRefName: 'refs/heads/topic/super-awesome-feature', - targetRefName: 'refs/heads/main', - status: PullRequestStatus.Active, - isDraft: false, - link: 'https://host.com/myOrg/_git/super-feature-repo/pullrequest/7181', - }; + const outputPullRequest: PullRequest = { + pullRequestId: 7181, + repoName: 'super-feature-repo', + title: 'My Awesome New Feature', + uniqueName: 'DOMAIN\\jdoe', + createdBy: 'Jane Doe', + creationDate: '2020-09-12T06:10:23.932Z', + sourceRefName: 'refs/heads/topic/super-awesome-feature', + targetRefName: 'refs/heads/main', + status: PullRequestStatus.Active, + isDraft: false, + link: 'https://host.com/myOrg/_git/super-feature-repo/pullrequest/7181', + }; - expect(mappedPullRequest(inputPullRequest, inputBaseUrl)).toEqual( - outputPullRequest, - ); + expect(mappedPullRequest(inputPullRequest, inputBaseUrl)).toEqual( + outputPullRequest, + ); + }); + }); + }); + + describe('mappedBuildRun', () => { + describe('mappedBuildRun happy path', () => { + it('should return RepoBuild from Build', () => { + const inputBuildDefinition: DefinitionReference = { + name: 'My Build Definition', + }; + + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; + + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; + + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: inputBuildDefinition, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; + + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'My Build Definition - Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; + + expect(mappedBuildRun(inputBuild)).toEqual(outputRepoBuild); + }); + }); + + describe('mappedBuildRun with no Build definition name', () => { + it('should return RepoBuild with only Build Number for title', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; + + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; + + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; + + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; + + expect(mappedBuildRun(inputBuild)).toEqual(outputRepoBuild); + }); + }); + + describe('mappedBuildRun with undefined status', () => { + it('should return BuildStatus of None for status', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; + + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; + + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: undefined, + result: BuildResult.Succeeded, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; + + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.None, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; + + expect(mappedBuildRun(inputBuild)).toEqual(outputRepoBuild); + }); + }); + + describe('mappedBuildRun with undefined result', () => { + it('should return BuildResult of None for result', () => { + const inputLinks: any = { + web: { + href: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + }, + }; + + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; + + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.InProgress, + result: undefined, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: inputLinks, + requestedFor: inputIdentityRef, + }; + + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; + + expect(mappedBuildRun(inputBuild)).toEqual(outputRepoBuild); + }); + }); + + describe('mappedBuildRun with undefined link', () => { + it('should return empty string for link', () => { + const inputIdentityRef: IdentityRef = { + displayName: 'Jane Doe', + uniqueName: 'DOMAIN\\jdoe', + }; + + const inputBuild: Build = { + id: 1, + buildNumber: 'Build-1', + status: BuildStatus.InProgress, + result: undefined, + queueTime: new Date('2020-09-12T06:10:23.932Z'), + startTime: new Date('2020-09-12T06:15:23.932Z'), + finishTime: new Date('2020-09-12T06:20:23.932Z'), + sourceBranch: 'refs/heads/develop', + sourceVersion: 'f4f78b3100b2923982bdf60c89c57ce6fd2d9a1c', + definition: undefined, + _links: undefined, + requestedFor: inputIdentityRef, + }; + + const outputRepoBuild: RepoBuild = { + id: 1, + title: 'Build-1', + link: '', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: '2020-09-12T06:10:23.932Z', + startTime: '2020-09-12T06:15:23.932Z', + finishTime: '2020-09-12T06:20:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + uniqueName: 'DOMAIN\\jdoe', + }; + + expect(mappedBuildRun(inputBuild)).toEqual(outputRepoBuild); + }); }); }); }); diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts index 7acc5fc066..c98092b5ac 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts @@ -14,8 +14,13 @@ * limitations under the License. */ +import { + Build, + BuildDefinitionReference, +} from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { BuildResult, + BuildRun, BuildStatus, DashboardPullRequest, Policy, @@ -35,7 +40,6 @@ import { getArtifactId, } from '../utils'; -import { Build, BuildDefinitionReference } from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { Logger } from 'winston'; import { PolicyEvaluationRecord } from 'azure-devops-node-api/interfaces/PolicyInterfaces'; import { TeamMember } from 'azure-devops-node-api/interfaces/common/VSSInterfaces'; @@ -60,46 +64,19 @@ export class AzureDevOpsApi { return client.getRepository(repoName, projectName); } - public async getBuildDefinitions( - projectName: string, - definitionName: string, - ): Promise { - this.logger?.debug( - `Calling Azure DevOps REST API, getting Build Definitions for ${definitionName} in Project ${projectName}`, - ); - - const client = await this.webApi.getBuildApi(); - return client.getDefinitions( - projectName, - definitionName, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - ); - } - public async getBuildList( projectName: string, - definitions?: number[], - repoId?: string, - top?: number, + repoId: string, + top: number, ): Promise { + this.logger?.debug( + `Calling Azure DevOps REST API, getting up to ${top} Builds for Repository Id ${repoId} for Project ${projectName}`, + ); + const client = await this.webApi.getBuildApi(); return client.getBuilds( projectName, - definitions, + undefined, undefined, undefined, undefined, @@ -118,7 +95,7 @@ export class AzureDevOpsApi { undefined, undefined, repoId, - repoId ? 'TfsGit' : undefined, + 'TfsGit', ); } @@ -134,7 +111,6 @@ export class AzureDevOpsApi { const gitRepository = await this.getGitRepository(projectName, repoName); const buildList = await this.getBuildList( projectName, - undefined, gitRepository.id as string, top, ); @@ -146,34 +122,6 @@ export class AzureDevOpsApi { return repoBuilds; } - public async getDefinitionBuilds( - projectName: string, - definitionName: string, - top: number, - ) { - this.logger?.debug( - `Calling Azure DevOps REST API, getting up to ${top} Builds for ${definitionName} in Project ${projectName}`, - ); - - const buildDefinitions = await this.getBuildDefinitions( - projectName, - definitionName, - ); - const definitions = buildDefinitions.map(bd => bd.id) as number[]; - const buildList = await this.getBuildList( - projectName, - definitions, - undefined, - top, - ); - - const repoBuilds: RepoBuild[] = buildList.map(build => { - return mappedRepoBuild(build); - }); - - return repoBuilds; - } - public async getPullRequests( projectName: string, repoName: string, @@ -313,6 +261,101 @@ export class AzureDevOpsApi { return teamMembers .map(teamMember => teamMember.identity?.id) .filter((id): id is string => Boolean(id)); + public async getBuildDefinitions( + projectName: string, + definitionName: string, + ): Promise { + this.logger?.debug( + `Calling Azure DevOps REST API, getting Build Definitions for ${definitionName} in Project ${projectName}`, + ); + + const client = await this.webApi.getBuildApi(); + return client.getDefinitions( + projectName, + definitionName, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ); + } + + public async getBuilds( + projectName: string, + top: number, + repoId?: string, + definitions?: number[], + ): Promise { + this.logger?.debug( + `Calling Azure DevOps REST API, getting up to ${top} Builds for Repository Id ${repoId} for Project ${projectName}`, + ); + + const client = await this.webApi.getBuildApi(); + return client.getBuilds( + projectName, + definitions, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + top, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + repoId, + repoId ? 'TfsGit' : undefined, + ); + } + + public async getBuildRuns( + projectName: string, + top: number, + repoName?: string, + definitionName?: string, + ) { + let repoId: string | undefined; + let definitions: number[] | undefined; + + if (repoName) { + const gitRepository = await this.getGitRepository(projectName, repoName); + repoId = gitRepository.id; + } + + if (definitionName) { + const buildDefinitions = await this.getBuildDefinitions( + projectName, + definitionName, + ); + definitions = buildDefinitions.map(bd => bd.id) as number[]; + } + + const builds = await this.getBuilds(projectName, top, repoId, definitions); + + const buildRuns: BuildRun[] = builds.map(build => { + return mappedBuildRun(build); + }); + + return buildRuns; } } @@ -351,3 +394,20 @@ export function mappedPullRequest( link: `${linkBaseUrl}/${pullRequest.pullRequestId}`, }; } + +export function mappedBuildRun(build: Build): BuildRun { + return { + id: build.id, + title: [build.definition?.name, build.buildNumber] + .filter(Boolean) + .join(' - '), + link: build._links?.web.href ?? '', + status: build.status ?? BuildStatus.None, + result: build.result ?? BuildResult.None, + queueTime: build.queueTime?.toISOString(), + startTime: build.startTime?.toISOString(), + finishTime: build.finishTime?.toISOString(), + source: `${build.sourceBranch} (${build.sourceVersion?.substr(0, 8)})`, + uniqueName: build.requestedFor?.uniqueName ?? 'N/A', + }; +} diff --git a/plugins/azure-devops-backend/src/service/router.test.ts b/plugins/azure-devops-backend/src/service/router.test.ts index 82d4df1265..87135781e6 100644 --- a/plugins/azure-devops-backend/src/service/router.test.ts +++ b/plugins/azure-devops-backend/src/service/router.test.ts @@ -14,8 +14,13 @@ * limitations under the License. */ +import { + Build, + BuildDefinitionReference, +} from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { BuildResult, + BuildRun, BuildStatus, PullRequest, PullRequestStatus, @@ -23,7 +28,6 @@ import { } from '@backstage/plugin-azure-devops-common'; import { AzureDevOpsApi } from '../api'; -import { Build, BuildDefinitionReference } from 'azure-devops-node-api/interfaces/BuildInterfaces'; import { ConfigReader } from '@backstage/config'; import { GitRepository } from 'azure-devops-node-api/interfaces/GitInterfaces'; import { createRouter } from './router'; @@ -43,6 +47,8 @@ describe('createRouter', () => { getRepoBuilds: jest.fn(), getDefinitionBuilds: jest.fn(), getPullRequests: jest.fn(), + getBuilds: jest.fn(), + getBuildRuns: jest.fn(), } as any; const router = await createRouter({ azureDevOpsApi, @@ -138,7 +144,6 @@ describe('createRouter', () => { expect(azureDevOpsApi.getBuildList).toHaveBeenCalledWith( 'myProject', - undefined, 'af4ae3af-e747-4129-9bbc-d1329f6b0998', 40, ); @@ -147,32 +152,6 @@ describe('createRouter', () => { }); }); - describe('GET /build-definitions/:projectName/:definitionName', () => { - it('fetches a list of build definitions', async () => { - const inputDefinition: BuildDefinitionReference = { - id: 1, - name: 'myBuildDefinition', - }; - - const inputDefinitions: BuildDefinitionReference[] = [inputDefinition]; - - azureDevOpsApi.getBuildDefinitions.mockResolvedValueOnce( - inputDefinitions, - ); - - const response = await request(app).get( - '/build-definitions/myProject/myBuildDefinition', - ); - - expect(azureDevOpsApi.getBuildDefinitions).toHaveBeenCalledWith( - 'myProject', - 'myBuildDefinition', - ); - expect(response.status).toEqual(200); - expect(response.body).toEqual(inputDefinitions); - }); - }); - describe('GET /repo-builds/:projectName/:repoName', () => { it('fetches a list of repo builds', async () => { const firstRepoBuild: RepoBuild = { @@ -227,60 +206,6 @@ describe('createRouter', () => { }); }); - describe('GET /definition-builds/:projectName/:definitionName', () => { - it('fetches a list of repo builds', async () => { - const firstRepoBuild: RepoBuild = { - id: 1, - title: 'My Build Definition - Build 1', - link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', - status: BuildStatus.Completed, - result: BuildResult.PartiallySucceeded, - queueTime: undefined, - source: 'refs/heads/develop (f4f78b31)', - }; - - const secondRepoBuild: RepoBuild = { - id: 2, - title: 'My Build Definition - Build 2', - link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=2', - status: BuildStatus.InProgress, - result: BuildResult.None, - queueTime: undefined, - source: 'refs/heads/develop (13c988d4)', - }; - - const thirdRepoBuild: RepoBuild = { - id: 3, - title: 'My Build Definition - Build 3', - link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=3', - status: BuildStatus.Completed, - result: BuildResult.Succeeded, - queueTime: undefined, - source: 'refs/heads/develop (9bedf678)', - }; - - const repoBuilds: RepoBuild[] = [ - firstRepoBuild, - secondRepoBuild, - thirdRepoBuild, - ]; - - azureDevOpsApi.getDefinitionBuilds.mockResolvedValueOnce(repoBuilds); - - const response = await request(app) - .get('/definition-builds/myProject/myDefinition') - .query({ top: '30' }); - - expect(azureDevOpsApi.getDefinitionBuilds).toHaveBeenCalledWith( - 'myProject', - 'myDefinition', - 30, - ); - expect(response.status).toEqual(200); - expect(response.body).toEqual(repoBuilds); - }); - }); - describe('GET /pull-requests/:projectName/:repoName', () => { it('fetches a list of pull requests', async () => { const firstPullRequest: PullRequest = { @@ -343,4 +268,142 @@ describe('createRouter', () => { expect(response.body).toEqual(pullRequests); }); }); + + describe('GET /build-definitions/:projectName/:definitionName', () => { + it('fetches a list of build definitions', async () => { + const inputDefinition: BuildDefinitionReference = { + id: 1, + name: 'myBuildDefinition', + }; + + const inputDefinitions: BuildDefinitionReference[] = [inputDefinition]; + + azureDevOpsApi.getBuildDefinitions.mockResolvedValueOnce( + inputDefinitions, + ); + + const response = await request(app).get( + '/build-definitions/myProject/myBuildDefinition', + ); + + expect(azureDevOpsApi.getBuildDefinitions).toHaveBeenCalledWith( + 'myProject', + 'myBuildDefinition', + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(inputDefinitions); + }); + }); + + describe('GET /builds/:projectName', () => { + describe('GET /builds/:projectName with repoName', () => { + it('fetches a list of build runs using repoName', async () => { + const firstBuildRun: BuildRun = { + id: 1, + title: 'My Build Definition - Build 1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.PartiallySucceeded, + queueTime: '2020-09-12T06:10:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + }; + + const secondBuildRun: BuildRun = { + id: 2, + title: 'My Build Definition - Build 2', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=2', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: '2020-09-12T06:10:23.932Z', + source: 'refs/heads/develop (13c988d4)', + }; + + const thirdBuildRun: BuildRun = { + id: 3, + title: 'My Build Definition - Build 3', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=3', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + source: 'refs/heads/develop (9bedf678)', + }; + + const buildRuns: BuildRun[] = [ + firstBuildRun, + secondBuildRun, + thirdBuildRun, + ]; + + azureDevOpsApi.getBuildRuns.mockResolvedValueOnce(buildRuns); + + const response = await request(app) + .get('/builds/myProject') + .query({ top: '50', repoName: 'myRepo' }); + + expect(azureDevOpsApi.getBuildRuns).toHaveBeenCalledWith( + 'myProject', + 50, + 'myRepo', + undefined, + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(buildRuns); + }); + }); + + describe('GET /builds/:projectName with definitionName', () => { + it('fetches a list of build runs using definitionName', async () => { + const firstBuildRun: BuildRun = { + id: 1, + title: 'My Build Definition - Build 1', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=1', + status: BuildStatus.Completed, + result: BuildResult.PartiallySucceeded, + queueTime: '2020-09-12T06:10:23.932Z', + source: 'refs/heads/develop (f4f78b31)', + }; + + const secondBuildRun: BuildRun = { + id: 2, + title: 'My Build Definition - Build 2', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=2', + status: BuildStatus.InProgress, + result: BuildResult.None, + queueTime: '2020-09-12T06:10:23.932Z', + source: 'refs/heads/develop (13c988d4)', + }; + + const thirdBuildRun: BuildRun = { + id: 3, + title: 'My Build Definition - Build 3', + link: 'https://host.com/myOrg/0bcc0c0d-2d02/_build/results?buildId=3', + status: BuildStatus.Completed, + result: BuildResult.Succeeded, + queueTime: '2020-09-12T06:10:23.932Z', + source: 'refs/heads/develop (9bedf678)', + }; + + const buildRuns: BuildRun[] = [ + firstBuildRun, + secondBuildRun, + thirdBuildRun, + ]; + + azureDevOpsApi.getBuildRuns.mockResolvedValueOnce(buildRuns); + + const response = await request(app) + .get('/builds/myProject') + .query({ top: '50', definitionName: 'myDefinition' }); + + expect(azureDevOpsApi.getBuildRuns).toHaveBeenCalledWith( + 'myProject', + 50, + undefined, + 'myDefinition', + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual(buildRuns); + }); + }); + }); }); diff --git a/plugins/azure-devops-backend/src/service/router.ts b/plugins/azure-devops-backend/src/service/router.ts index 5e6369bb11..99c140a424 100644 --- a/plugins/azure-devops-backend/src/service/router.ts +++ b/plugins/azure-devops-backend/src/service/router.ts @@ -73,25 +73,12 @@ export async function createRouter( const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP; const buildList = await azureDevOpsApi.getBuildList( projectName, - undefined, repoId, top, ); res.status(200).json(buildList); }); - router.get( - '/build-definitions/:projectName/:definitionName', - async (req, res) => { - const { projectName, definitionName } = req.params; - const buildDefinitionList = await azureDevOpsApi.getBuildDefinitions( - projectName, - definitionName, - ); - res.status(200).json(buildDefinitionList); - }, - ); - router.get('/repo-builds/:projectName/:repoName', async (req, res) => { const { projectName, repoName } = req.params; @@ -106,20 +93,6 @@ export async function createRouter( res.status(200).json(gitRepository); }); - router.get( - '/definition-builds/:projectName/:definitionName', - async (req, res) => { - const { projectName, definitionName } = req.params; - const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP; - const gitRepository = await azureDevOpsApi.getDefinitionBuilds( - projectName, - definitionName, - top, - ); - res.status(200).json(gitRepository); - }, - ); - router.get('/pull-requests/:projectName/:repoName', async (req, res) => { const { projectName, repoName } = req.params; @@ -171,6 +144,32 @@ export async function createRouter( res.status(200).json(allTeams); }); + router.get( + '/build-definitions/:projectName/:definitionName', + async (req, res) => { + const { projectName, definitionName } = req.params; + const buildDefinitionList = await azureDevOpsApi.getBuildDefinitions( + projectName, + definitionName, + ); + res.status(200).json(buildDefinitionList); + }, + ); + + router.get('/builds/:projectName', async (req, res) => { + const { projectName } = req.params; + const repoName = req.query.repoName?.toString(); + const definitionName = req.query.definitionName?.toString(); + const top = req.query.top ? Number(req.query.top) : DEFAULT_TOP; + const builds = await azureDevOpsApi.getBuildRuns( + projectName, + top, + repoName, + definitionName, + ); + res.status(200).json(builds); + }); + router.use(errorHandler()); return router; } diff --git a/plugins/azure-devops-common/api-report.md b/plugins/azure-devops-common/api-report.md index 17cccb05cb..bb458f169a 100644 --- a/plugins/azure-devops-common/api-report.md +++ b/plugins/azure-devops-common/api-report.md @@ -14,6 +14,29 @@ export enum BuildResult { Succeeded = 2, } +// Warning: (ae-missing-release-tag) "BuildRun" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type BuildRun = { + id?: number; + title: string; + link?: string; + status?: BuildStatus; + result?: BuildResult; + queueTime?: string; + startTime?: string; + finishTime?: string; + source: string; + uniqueName?: string; +}; + +// Warning: (ae-missing-release-tag) "BuildRunOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type BuildRunOptions = { + top?: number; +}; + // Warning: (ae-missing-release-tag) "BuildStatus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/azure-devops-common/src/types.ts b/plugins/azure-devops-common/src/types.ts index da41e08d1f..e6bd174079 100644 --- a/plugins/azure-devops-common/src/types.ts +++ b/plugins/azure-devops-common/src/types.ts @@ -251,3 +251,19 @@ export enum PullRequestVoteStatus { WaitingForAuthor = -5, Rejected = -10, } +export type BuildRun = { + id?: number; + title: string; + link?: string; + status?: BuildStatus; + result?: BuildResult; + queueTime?: string; + startTime?: string; + finishTime?: string; + source: string; + uniqueName?: string; +}; + +export type BuildRunOptions = { + top?: number; +}; From 5b41684f6aca83d499dca3ff599cab2718b4873f Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 26 Nov 2021 07:09:05 -0600 Subject: [PATCH 030/116] Updated changeset to minor Signed-off-by: Andre Wanlin --- .changeset/rare-toes-burn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/rare-toes-burn.md b/.changeset/rare-toes-burn.md index 2524df1daa..264576eb58 100644 --- a/.changeset/rare-toes-burn.md +++ b/.changeset/rare-toes-burn.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-azure-devops-backend': patch +'@backstage/plugin-azure-devops-backend': minor --- Added getting builds by definition name From 07ac314fb2f891be0313d5d89ab3bd45f2b5b361 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 4 Dec 2021 11:50:38 -0600 Subject: [PATCH 031/116] Improvements based on feedback Signed-off-by: Andre Wanlin --- plugins/azure-devops-backend/api-report.md | 19 ++++++++++++ .../src/api/AzureDevOpsApi.ts | 30 +++++-------------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/plugins/azure-devops-backend/api-report.md b/plugins/azure-devops-backend/api-report.md index cbb512d00e..9574215e3e 100644 --- a/plugins/azure-devops-backend/api-report.md +++ b/plugins/azure-devops-backend/api-report.md @@ -25,12 +25,31 @@ export class AzureDevOpsApi { // (undocumented) getAllTeams(): Promise; // (undocumented) + getBuildDefinitions( + projectName: string, + definitionName: string, + ): Promise; + // (undocumented) getBuildList( projectName: string, repoId: string, top: number, ): Promise; // (undocumented) + getBuildRuns( + projectName: string, + top: number, + repoName?: string, + definitionName?: string, + ): Promise; + // (undocumented) + getBuilds( + projectName: string, + top: number, + repoId?: string, + definitions?: number[], + ): Promise; + // (undocumented) getDashboardPullRequests( projectName: string, options: PullRequestOptions, diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts index c98092b5ac..6e45a5f4e5 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts @@ -261,6 +261,8 @@ export class AzureDevOpsApi { return teamMembers .map(teamMember => teamMember.identity?.id) .filter((id): id is string => Boolean(id)); + } + public async getBuildDefinitions( projectName: string, definitionName: string, @@ -270,25 +272,7 @@ export class AzureDevOpsApi { ); const client = await this.webApi.getBuildApi(); - return client.getDefinitions( - projectName, - definitionName, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - ); + return client.getDefinitions(projectName, definitionName); } public async getBuilds( @@ -346,14 +330,14 @@ export class AzureDevOpsApi { projectName, definitionName, ); - definitions = buildDefinitions.map(bd => bd.id) as number[]; + definitions = buildDefinitions + .map(bd => bd.id) + .filter((bd): bd is number => Boolean(bd)); } const builds = await this.getBuilds(projectName, top, repoId, definitions); - const buildRuns: BuildRun[] = builds.map(build => { - return mappedBuildRun(build); - }); + const buildRuns: BuildRun[] = builds.map(mappedBuildRun); return buildRuns; } From 94c3583f2b3478f9078cbde4ad73c81d0f6dfcf1 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Mon, 6 Dec 2021 11:23:16 +0530 Subject: [PATCH 032/116] scaffolder: update docs with v1beta3 syntax migration guide: https://backstage.io/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3 Signed-off-by: Himanshu Mishra --- .../software-templates/adding-templates.md | 14 ++--- .../software-templates/writing-templates.md | 55 +++++++++---------- 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/docs/features/software-templates/adding-templates.md b/docs/features/software-templates/adding-templates.md index 2cdf6f1b35..aa565b5960 100644 --- a/docs/features/software-templates/adding-templates.md +++ b/docs/features/software-templates/adding-templates.md @@ -11,13 +11,13 @@ would be good to also have some files in there that can be templated in. A simple `template.yaml` definition might look something like this: ```yaml -apiVersion: backstage.io/v1beta2 +apiVersion: backstage.io/v1beta3 kind: Template # some metadata about the template itself metadata: - name: v1beta2-demo + name: v1beta3-demo title: Test Action template - description: scaffolder v1beta2 template demo + description: scaffolder v1beta3 template demo spec: owner: backstage/techdocs-core type: service @@ -55,7 +55,7 @@ spec: input: url: ./template values: - name: '{{ parameters.name }}' + name: ${{ parameters.name }} - id: fetch-docs name: Fetch Docs @@ -69,14 +69,14 @@ spec: action: publish:github input: allowedHosts: ['github.com'] - description: 'This is {{ parameters.name }}' - repoUrl: '{{ parameters.repoUrl }}' + description: This is ${{ parameters.name }} + repoUrl: ${{ parameters.repoUrl }} - id: register name: Register action: catalog:register input: - repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}' + repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }} catalogInfoPath: '/catalog-info.yaml' ``` diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 81f2622312..dc9a2ed1e7 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -6,21 +6,21 @@ description: Details around creating your own custom Software Templates Templates are stored in the **Software Catalog** under a kind `Template`. You can create your own templates with a small `yaml` definition which describes the -template and it's metadata, along with some input variables that your template +template and its metadata, along with some input variables that your template will need, and then a list of actions which are then executed by the scaffolding service. Let's take a look at a simple example: ```yaml -# Notice the v1beta2 version -apiVersion: backstage.io/v1beta2 +# Notice the v1beta3 version +apiVersion: backstage.io/v1beta3 kind: Template # some metadata about the template itself metadata: - name: v1beta2-demo + name: v1beta3-demo title: Test Action template - description: scaffolder v1beta2 template demo + description: scaffolder v1beta3 template demo spec: owner: backstage/techdocs-core type: service @@ -66,8 +66,8 @@ spec: input: url: ./template values: - name: '{{ parameters.name }}' - owner: '{{ parameters.owner }}' + name: ${{ parameters.name }} + owner: ${{ parameters.owner }} - id: fetch-docs name: Fetch Docs @@ -81,20 +81,20 @@ spec: action: publish:github input: allowedHosts: ['github.com'] - description: 'This is {{ parameters.name }}' - repoUrl: '{{ parameters.repoUrl }}' + description: This is ${{ parameters.name }} + repoUrl: ${{ parameters.repoUrl }} - id: register name: Register action: catalog:register input: - repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}' + repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }} catalogInfoPath: '/catalog-info.yaml' # some outputs which are saved along with the job for use in the frontend output: - remoteUrl: '{{ steps.publish.output.remoteUrl }}' - entityRef: '{{ steps.register.output.entityRef }}' + remoteUrl: ${{ steps.publish.output.remoteUrl }} + entityRef: ${{ steps.register.output.entityRef }} ``` Let's dive in and pick apart what each of these sections do and what they are. @@ -183,12 +183,12 @@ this: It would look something like the following in a template: ```yaml -apiVersion: backstage.io/v1beta2 +apiVersion: backstage.io/v1beta3 kind: Template metadata: - name: v1beta2-demo + name: v1beta3-demo title: Test Action template - description: scaffolder v1beta2 template demo + description: scaffolder v1beta3 template demo spec: owner: backstage/techdocs-core type: service @@ -315,12 +315,12 @@ template. These follow the same standard format: ```yaml - id: fetch-base # A unique id for the step name: Fetch Base # A title displayed in the frontend - if: '{{ parameters.name }}' # Optional condition, skip the step if not truthy + if: ${{ parameters.name }} # Optional condition, skip the step if not truthy action: fetch:template # An action to call input: # Input that is passed as arguments to the action handler url: ./template values: - name: '{{ parameters.name }}' + name: ${{ parameters.name }} ``` By default we ship some [built in actions](./builtin-actions.md) that you can @@ -338,22 +338,19 @@ The main two that are used are the following: ```yaml output: - remoteUrl: '{{ steps.publish.output.remoteUrl }}' # link to the remote repository - entityRef: '{{ steps.register.output.entityRef }}' # link to the entity that has been ingested to the catalog + remoteUrl: ${{ steps.publish.output.remoteUrl }} # link to the remote repository + entityRef: ${{ steps.register.output.entityRef }} # link to the entity that has been ingested to the catalog ``` ### The templating syntax -You might have noticed variables wrapped in `{{ }}` in the examples. These are -`handlebars` template strings for linking and gluing the different parts of the -template together. All the form inputs from the `parameters` section will be -available by using this template syntax (for example, -`{{ parameters.firstName }}` inserts the value of `firstName` from the -parameters). This is great for passing the values from the form into different -steps and reusing these input variables. To pass arrays or objects use the -`json` custom [helper](https://handlebarsjs.com/guide/expressions.html#helpers). -For example, `{{ json parameters.nicknames }}` will insert the result of calling -`JSON.stringify` on the value of the `nicknames` parameter. +You might have noticed variables wrapped in `${{ }}` in the examples. These are +template strings for linking and gluing the different parts of the template +together. All the form inputs from the `parameters` section will be available by +using this template syntax (for example, `${{ parameters.firstName }}` inserts +the value of `firstName` from the parameters). This is great for passing the +values from the form into different steps and reusing these input variables. +These template strings preserve the type of the parameter. As you can see above in the `Outputs` section, `actions` and `steps` can also output things. You can grab that output using `steps.$stepId.output.$property`. From 73c73b71d15f1a25ee07cdbbc7a6467cf1a7f445 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Dec 2021 11:55:42 +0100 Subject: [PATCH 033/116] auth: simplify types and reintroduce deprecated BackstageIdentity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- packages/core-plugin-api/api-report.md | 2 +- .../src/apis/definitions/auth.ts | 2 ++ plugins/auth-backend/api-report.md | 30 ++++++++++++------- .../src/lib/oauth/OAuthAdapter.ts | 3 +- plugins/auth-backend/src/lib/oauth/types.ts | 14 ++++----- plugins/auth-backend/src/providers/index.ts | 2 ++ plugins/auth-backend/src/providers/types.ts | 23 +++++++++----- 7 files changed, 50 insertions(+), 26 deletions(-) diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 9245f039df..ad7d0b80a9 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -237,7 +237,7 @@ export type AuthRequestOptions = { instantPopup?: boolean; }; -// @public @deprecated (undocumented) +// @public @deprecated export type BackstageIdentity = BackstageIdentityResponse; // @public diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index e093a6b29b..37308ec29b 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -212,6 +212,8 @@ export type BackstageIdentityResponse = { }; /** + * The old exported symbol for {@link BackstageIdentityResponse}. + * * @public * @deprecated use {@link BackstageIdentityResponse} instead. */ diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 7d776986a7..41c017ef6b 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -116,13 +116,24 @@ export type AwsAlbProviderOptions = { }; }; +// @public @deprecated +export type BackstageIdentity = BackstageSignInResult; + // @public -export type BackstageIdentityResponse = { - id: string; - entity?: Entity; - token: string; +export interface BackstageIdentityResponse extends BackstageSignInResult { identity: BackstageUserIdentity; -}; +} + +// Warning: (ae-missing-release-tag) "BackstageSignInResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface BackstageSignInResult { + // @deprecated + entity?: Entity; + // @deprecated + id: string; + token: string; +} // @public export type BackstageUserIdentity = { @@ -504,11 +515,10 @@ export type OAuthRefreshRequest = express.Request<{}> & { // Warning: (ae-missing-release-tag) "OAuthResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type OAuthResponse = Omit< - AuthResponse, - 'backstageIdentity' -> & { - backstageIdentity?: Omit; +export type OAuthResponse = { + profile: ProfileInfo; + providerInfo: OAuthProviderInfo; + backstageIdentity?: BackstageSignInResult; }; // Warning: (ae-missing-release-tag) "OAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index b6c6473031..bb0bbf9dbd 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -21,6 +21,7 @@ import { AuthProviderRouteHandlers, AuthProviderConfig, BackstageIdentityResponse, + BackstageSignInResult, } from '../../providers/types'; import { AuthenticationError, @@ -232,7 +233,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { * make sure it's populated with all the information we can derive from the user ID. */ private async populateIdentity( - identity?: Omit, + identity?: BackstageSignInResult, ): Promise { if (!identity) { return undefined; diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index f1ff9e763d..48e3ffe6ea 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -17,10 +17,11 @@ import express from 'express'; import { Profile as PassportProfile } from 'passport'; import { - AuthResponse, RedirectInfo, - BackstageIdentityResponse, + BackstageSignInResult, + ProfileInfo, } from '../../providers/types'; + /** * Common options for passport.js-based OAuth providers */ @@ -50,11 +51,10 @@ export type OAuthResult = { refreshToken?: string; }; -export type OAuthResponse = Omit< - AuthResponse, - 'backstageIdentity' -> & { - backstageIdentity?: Omit; +export type OAuthResponse = { + profile: ProfileInfo; + providerInfo: OAuthProviderInfo; + backstageIdentity?: BackstageSignInResult; }; export type OAuthProviderInfo = { diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 34c2ff06c2..9fe77fe593 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -40,8 +40,10 @@ export type { // to the frontend export type { AuthResponse, + BackstageIdentity, BackstageUserIdentity, BackstageIdentityResponse, + BackstageSignInResult, ProfileInfo, } from './types'; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 919cbe42b4..9f93e6fa21 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -164,11 +164,7 @@ export type BackstageUserIdentity = { ownershipEntityRefs: string[]; }; -/** - * Response object containing the {@link BackstageUserIdentity} and the token from the authentication provider. - * @public - */ -export type BackstageIdentityResponse = { +export interface BackstageSignInResult { /** * An opaque ID that uniquely identifies the user within Backstage. * @@ -192,12 +188,25 @@ export type BackstageIdentityResponse = { * The token used to authenticate the user within Backstage. */ token: string; +} +/** + * The old exported symbol for {@link BackstageSignInResult}. + * @public + * @deprecated Use the `BackstageSignInResult` type instead. + */ +export type BackstageIdentity = BackstageSignInResult; + +/** + * Response object containing the {@link BackstageUserIdentity} and the token from the authentication provider. + * @public + */ +export interface BackstageIdentityResponse extends BackstageSignInResult { /** * A plaintext description of the identity that is encapsulated within the token. */ identity: BackstageUserIdentity; -}; +} /** * Used to display login information to user, i.e. sidebar popup. @@ -242,7 +251,7 @@ export type SignInResolver = ( catalogIdentityClient: CatalogIdentityClient; logger: Logger; }, -) => Promise>; +) => Promise; export type AuthHandlerResult = { profile: ProfileInfo }; From ad8ed6f91b2972a8374a0a84f0d926e0fd119552 Mon Sep 17 00:00:00 2001 From: Radoslaw Wielonski Date: Mon, 6 Dec 2021 10:48:06 +0100 Subject: [PATCH 034/116] feat: add ability to set custom errorHandler Signed-off-by: Radoslaw Wielonski --- .../service/lib/ServiceBuilderImpl.test.ts | 21 ++++++++++++++++++- .../src/service/lib/ServiceBuilderImpl.ts | 12 ++++++++--- packages/backend-common/src/service/types.ts | 11 +++++++++- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts index bd97f444de..1d214d0bff 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { applyCspDirectives } from './ServiceBuilderImpl'; +import { NextFunction, Request, Response } from 'express'; +import { applyCspDirectives, ServiceBuilderImpl } from './ServiceBuilderImpl'; describe('ServiceBuilderImpl', () => { describe('applyCspDirectives', () => { @@ -33,4 +34,22 @@ describe('ServiceBuilderImpl', () => { expect(result!['upgrade-insecure-requests']).toBeUndefined(); }); }); + + describe('setCustomErrorHandler', () => { + it('adds custom error handler', () => { + const serviceBuilder = new ServiceBuilderImpl(module); + const customErrorHandler = ( + error: Error, + req: Request, + res: Response, + next: NextFunction, + ) => {}; + serviceBuilder.setErrorHandler(customErrorHandler); + expect(serviceBuilder.errorHandler).toEqual(customErrorHandler); + }); + it('use default error handler', () => { + const serviceBuilder = new ServiceBuilderImpl(module); + expect(serviceBuilder.errorHandler).toBeUndefined(); + }); + }); }); diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index b085cccfd6..8d6404076f 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -17,7 +17,7 @@ import { Config } from '@backstage/config'; import compression from 'compression'; import cors from 'cors'; -import express, { Router } from 'express'; +import express, { Router, ErrorRequestHandler } from 'express'; import helmet from 'helmet'; import * as http from 'http'; import stoppable from 'stoppable'; @@ -25,7 +25,7 @@ import { Logger } from 'winston'; import { useHotCleanup } from '../../hot'; import { getRootLogger } from '../../logging'; import { - errorHandler, + errorHandler as defaultErrorHandler, notFoundHandler, requestLoggingHandler as defaultRequestLoggingHandler, } from '../../middleware'; @@ -66,6 +66,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { private httpsSettings: HttpsSettings | undefined; private routers: [string, Router][]; private requestLoggingHandler: RequestLoggingHandlerFactory | undefined; + private errorHandler: ErrorRequestHandler | undefined; // Reference to the module where builder is created - needed for hot module // reloading private module: NodeModule; @@ -152,6 +153,11 @@ export class ServiceBuilderImpl implements ServiceBuilder { return this; } + setErrorHandler(errorHandler: ErrorRequestHandler) { + this.errorHandler = errorHandler; + return this; + } + async start(): Promise { const app = express(); const { port, host, logger, corsOptions, httpsSettings, helmetOptions } = @@ -169,7 +175,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { app.use(root, route); } app.use(notFoundHandler()); - app.use(errorHandler()); + app.use(this.errorHandler ?? defaultErrorHandler()); const server: http.Server = httpsSettings ? await createHttpsServer(app, httpsSettings, logger) diff --git a/packages/backend-common/src/service/types.ts b/packages/backend-common/src/service/types.ts index 2ad379f31e..37bfec3c43 100644 --- a/packages/backend-common/src/service/types.ts +++ b/packages/backend-common/src/service/types.ts @@ -16,7 +16,7 @@ import { Config } from '@backstage/config'; import cors from 'cors'; -import { Router, RequestHandler } from 'express'; +import { Router, RequestHandler, ErrorRequestHandler } from 'express'; import { Server } from 'http'; import { Logger } from 'winston'; @@ -98,6 +98,15 @@ export type ServiceBuilder = { requestLoggingHandler: RequestLoggingHandlerFactory, ): ServiceBuilder; + /** + * Set the error handler + * + * If no handler is given the default one is used + * + * @param errorHandler - an error handler + */ + setErrorHandler(errorHandler: ErrorRequestHandler): ServiceBuilder; + /** * Starts the server using the given settings. */ From 5a008576c421ba16b6ab19fa29e122470969cfb7 Mon Sep 17 00:00:00 2001 From: Radoslaw Wielonski Date: Mon, 6 Dec 2021 10:56:14 +0100 Subject: [PATCH 035/116] docs: add note to changeset Signed-off-by: Radoslaw Wielonski --- .changeset/hot-toys-grab.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/hot-toys-grab.md diff --git a/.changeset/hot-toys-grab.md b/.changeset/hot-toys-grab.md new file mode 100644 index 0000000000..a2d4faa469 --- /dev/null +++ b/.changeset/hot-toys-grab.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Add possibility to use custom error handler From f0064d4ee3b118f1d11860ce024be5d2298b43c3 Mon Sep 17 00:00:00 2001 From: Radoslaw Wielonski Date: Mon, 6 Dec 2021 15:40:02 +0100 Subject: [PATCH 036/116] fix: fix tests for custom error handler Signed-off-by: Radoslaw Wielonski --- .../src/service/lib/ServiceBuilderImpl.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts index 1d214d0bff..665eaa6550 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts @@ -43,12 +43,17 @@ describe('ServiceBuilderImpl', () => { req: Request, res: Response, next: NextFunction, - ) => {}; + ) => { + console.log(req, res); + next(error); + }; serviceBuilder.setErrorHandler(customErrorHandler); + // @ts-ignore check private attribute expect(serviceBuilder.errorHandler).toEqual(customErrorHandler); }); it('use default error handler', () => { const serviceBuilder = new ServiceBuilderImpl(module); + // @ts-ignore check private attribute expect(serviceBuilder.errorHandler).toBeUndefined(); }); }); From a036b65c2f1a16553e9413b5f97629d0a610bce9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Dec 2021 14:53:16 +0100 Subject: [PATCH 037/116] changesets: added changesets for sign-in and identity changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- .changeset/chilly-files-greet.md | 9 ++++++++ .changeset/cold-ties-pay.md | 17 +++++++++++++++ .changeset/fast-trainers-unite.md | 35 +++++++++++++++++++++++++++++++ .changeset/lovely-goats-eat.md | 8 +++++++ .changeset/odd-ears-pump.md | 11 ++++++++++ 5 files changed, 80 insertions(+) create mode 100644 .changeset/chilly-files-greet.md create mode 100644 .changeset/cold-ties-pay.md create mode 100644 .changeset/fast-trainers-unite.md create mode 100644 .changeset/lovely-goats-eat.md create mode 100644 .changeset/odd-ears-pump.md diff --git a/.changeset/chilly-files-greet.md b/.changeset/chilly-files-greet.md new file mode 100644 index 0000000000..d8cd71f1be --- /dev/null +++ b/.changeset/chilly-files-greet.md @@ -0,0 +1,9 @@ +--- +'@backstage/core-components': minor +--- + +The `SignInPage` has been updated to use the new `onSignInSuccess` callback that was introduced in the same release. While existing code will usually continue to work, it is technically a breaking change because of the dependency on `SignInProps` from the `@backstage/core-plugin-api`. For more information on this change and instructions on how to migrate existing code, see the [`@backstage/core-app-api` CHANGELOG.md](https://github.com/backstage/backstage/blob/master/packages/core-app-api/CHANGELOG.md). + +Added a new `UserIdentity` class which helps create implementations of the `IdentityApi`. It provides a couple of static factory methods such as the most relevant `create`, and `createGuest` to create an `IdentityApi` for a guest user. + +Also provides a deprecated `fromLegacy` method to create an `IdentityApi` from the now deprecated `SignInResult`. This method will be removed in the future when `SignInResult` is also removed. diff --git a/.changeset/cold-ties-pay.md b/.changeset/cold-ties-pay.md new file mode 100644 index 0000000000..3062761444 --- /dev/null +++ b/.changeset/cold-ties-pay.md @@ -0,0 +1,17 @@ +--- +'@backstage/core-plugin-api': minor +--- + +The `IdentityApi` has received several updates. The `getUserId`, `getProfile`, and `getIdToken` have all been deprecated. + +The replacement for `getUserId` is the new `getBackstageIdentity` method, which provides both the `userEntityRef` as well as the `ownershipEntityRefs` that are used to resolve ownership. Existing usage of the user ID would typically be using a fixed entity kind and namespace, for example `` `user:default/${identityApi.getUserId()}` ``, this kind of usage should now instead use the `userEntityRef` directly. + +The replacement for `getProfile` is the new async `getProfileInfo`. + +The replacement for `getIdToken` is the new `getCredentials` method, which provides an optional token to the caller like before, but it is now wrapped in an object for forwards compatibility. + +The deprecated `idToken` field of the `BackstageIdentity` type has been removed, leaving only the new `token` field, which should be used instead. The `BackstageIdentity` also received a new `identity` field, which is a decoded version of the information within the token. Furthermore the `BackstageIdentity` has been renamed to `BackstageIdentityResponse`, with the old name being deprecated. + +We expect most of the breaking changes in this update to have low impact since the `IdentityApi` implementation is provided by the app, but it is likely that some tests need to be updated. + +Another breaking change is that the `SignInPage` props have been updated, and the `SignInResult` type is now deprecated. This is unlikely to have any impact on the usage of this package, but it is an important change that you can find more information about in the [`@backstage/core-app-api` CHANGELOG.md](https://github.com/backstage/backstage/blob/master/packages/core-app-api/CHANGELOG.md). diff --git a/.changeset/fast-trainers-unite.md b/.changeset/fast-trainers-unite.md new file mode 100644 index 0000000000..48ba0eca55 --- /dev/null +++ b/.changeset/fast-trainers-unite.md @@ -0,0 +1,35 @@ +--- +'@backstage/core-app-api': minor +--- + +**BREAKING CHANGE** + +The app `SignInPage` component has been updated to switch out the `onResult` callback for a new `onSignInSuccess` callback. This is an immediate breaking change without any deprecation period, as it was deemed to be the way of making this change that had the lowest impact. + +The new `onSignInSuccess` callback directly accepts an implementation of an `IdentityApi`, rather than a `SignInResult`. The `SignInPage` from `@backstage/core-component` has been updated to fit this new API, and as long as you pass on `props` directly you should not see any breakage. + +However, if you implement your own custom `SignInPage`, then this will be a breaking change and you need to migrate over to using the new callback. While doing so you can take advantage of the `UserIdentity.fromLegacy` helper from `@backstage/core-components` to make the migration simpler by still using the `SignInResult` type. This helper is also deprecated though and is only provided for immediate migration. Long-term it will be necessary to build the `IdentityApi` using for example `UserIdentity.create` instead. + +The following is an example of how you can migrate existing usage immediately using `UserIdentity.fromLegacy`: + +```ts +onResult(signInResult); +// becomes +onSignInSuccess(UserIdentity.fromLegacy(signInResult)); +``` + +The following is an example of how implement the new `onSignInSuccess` callback of the `SignInPage` using `UserIdentity.create`: + +```ts +const identityResponse = await authApi.getBackstageIdentity(); +// Profile is optional and will be removed, but allows the +// synchronous getProfile method of the IdentityApi to be used. +const profile = await authApi.getProfile(); +onSignInSuccess( + UserIdentity.create({ + identity: identityResponse.identity, + authApi, + profile, + }), +); +``` diff --git a/.changeset/lovely-goats-eat.md b/.changeset/lovely-goats-eat.md new file mode 100644 index 0000000000..0fea320358 --- /dev/null +++ b/.changeset/lovely-goats-eat.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-permission-backend': patch +'@backstage/plugin-permission-node': patch +--- + +Updated to use the new `BackstageIdentityResponse` type from `@backstage/plugin-auth-backend`. + +The `BackstageIdentityResponse` type is backwards compatible with the `BackstageIdentity`, and provides an additional `identity` field with the claims of the user. diff --git a/.changeset/odd-ears-pump.md b/.changeset/odd-ears-pump.md new file mode 100644 index 0000000000..453b0d083d --- /dev/null +++ b/.changeset/odd-ears-pump.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-auth-backend': minor +--- + +**BREAKING CHANGE** The `idToken` field of `BackstageIdentity` has been removed, with the `token` taking its place. This means you may need to update existing `signIn.resolver` implementations to return an `token` rather than an `idToken`. This also applies to custom auth providers. + +The `BackstageIdentity` type has been deprecated and will be removed in the future. Taking its place is the new `BackstageSignInResult` type with the same shape. + +This change also introduces the new `BackstageIdentityResponse` that mirrors the type with the same name from `@backstage/core-plugin-api`. The `BackstageIdentityResponse` type is different from the `BackstageSignInResult` in that it also has a `identity` field which is of type `BackstageUserIdentity` and is a decoded version of the information within the token. + +When implementing a custom auth provider that is not based on the `OAuthAdapter` you may need to convert `BackstageSignInResult` into a `BackstageIdentityResponse`, this can be done using the new `prepareBackstageIdentityResponse` function. From 1154ec0017643d24fd40b0e301872659681255e5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Dec 2021 17:13:26 +0100 Subject: [PATCH 038/116] auth-backend: decorateWithIdentity -> prepareBackstageIdentityResponse + API report fixes Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/api-report.md | 18 +++++++----------- .../auth-backend/src/lib/oauth/OAuthAdapter.ts | 6 +++--- plugins/auth-backend/src/lib/oauth/types.ts | 5 +++++ .../src/providers/aws-alb/provider.ts | 4 ++-- plugins/auth-backend/src/providers/index.ts | 2 +- ....ts => prepareBackstageIdentityResponse.ts} | 18 +++++++++++------- .../src/providers/saml/provider.ts | 5 +++-- plugins/auth-backend/src/providers/types.ts | 8 ++++++++ 8 files changed, 40 insertions(+), 26 deletions(-) rename plugins/auth-backend/src/providers/{decorateWithIdentity.ts => prepareBackstageIdentityResponse.ts} (74%) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 41c017ef6b..a4076208ae 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -124,9 +124,7 @@ export interface BackstageIdentityResponse extends BackstageSignInResult { identity: BackstageUserIdentity; } -// Warning: (ae-missing-release-tag) "BackstageSignInResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface BackstageSignInResult { // @deprecated entity?: Entity; @@ -299,11 +297,6 @@ export const createSamlProvider: ( options?: SamlProviderOptions | undefined, ) => AuthProviderFactory; -// @public -export function decorateWithIdentity( - signInResolverResponse: Omit, -): BackstageIdentityResponse; - // Warning: (ae-missing-release-tag) "factories" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -512,9 +505,7 @@ export type OAuthRefreshRequest = express.Request<{}> & { refreshToken: string; }; -// Warning: (ae-missing-release-tag) "OAuthResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type OAuthResponse = { profile: ProfileInfo; providerInfo: OAuthProviderInfo; @@ -576,6 +567,11 @@ export const postMessageResponse: ( response: WebMessageResponse, ) => void; +// @public +export function prepareBackstageIdentityResponse( + result: BackstageSignInResult, +): BackstageIdentityResponse; + // @public export type ProfileInfo = { email?: string; diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index bb0bbf9dbd..eb3f7efa42 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -38,7 +38,7 @@ import { OAuthRefreshRequest, OAuthState, } from './types'; -import { decorateWithIdentity } from '../../providers/decorateWithIdentity'; +import { prepareBackstageIdentityResponse } from '../../providers/prepareBackstageIdentityResponse'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export const TEN_MINUTES_MS = 600 * 1000; @@ -240,14 +240,14 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { } if (identity.token) { - return decorateWithIdentity(identity); + return prepareBackstageIdentityResponse(identity); } const token = await this.options.tokenIssuer.issueToken({ claims: { sub: identity.id }, }); - return decorateWithIdentity({ ...identity, token }); + return prepareBackstageIdentityResponse({ ...identity, token }); } private setNonceCookie = (res: express.Response, nonce: string) => { diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index 48e3ffe6ea..cd1439b399 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -51,6 +51,11 @@ export type OAuthResult = { refreshToken?: string; }; +/** + * The expected response from an OAuth flow. + * + * @public + */ export type OAuthResponse = { profile: ProfileInfo; providerInfo: OAuthProviderInfo; diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index 027d5190de..114bc9a204 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -32,7 +32,7 @@ import { CatalogIdentityClient } from '../../lib/catalog'; import { Profile as PassportProfile } from 'passport'; import { makeProfileInfo } from '../../lib/passport'; import { AuthenticationError } from '@backstage/errors'; -import { decorateWithIdentity } from '../decorateWithIdentity'; +import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; export const ALB_JWT_HEADER = 'x-amzn-oidc-data'; export const ALB_ACCESSTOKEN_HEADER = 'x-amzn-oidc-accesstoken'; @@ -199,7 +199,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { accessToken: result.accessToken, expiresInSeconds: result.expiresInSeconds, }, - backstageIdentity: decorateWithIdentity(backstageIdentity), + backstageIdentity: prepareBackstageIdentityResponse(backstageIdentity), profile, }; } diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 9fe77fe593..4ecbb98fd2 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -47,4 +47,4 @@ export type { ProfileInfo, } from './types'; -export { decorateWithIdentity } from './decorateWithIdentity'; +export { prepareBackstageIdentityResponse } from './prepareBackstageIdentityResponse'; diff --git a/plugins/auth-backend/src/providers/decorateWithIdentity.ts b/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts similarity index 74% rename from plugins/auth-backend/src/providers/decorateWithIdentity.ts rename to plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts index 0e0756f8a8..31df7a4cef 100644 --- a/plugins/auth-backend/src/providers/decorateWithIdentity.ts +++ b/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { BackstageIdentityResponse } from './types'; +import { BackstageIdentityResponse, BackstageSignInResult } from './types'; function parseJwtPayload(token: string) { const [_header, payload, _signature] = token.split('.'); @@ -22,16 +22,20 @@ function parseJwtPayload(token: string) { } /** - * @public - * * Parses token and decorates the BackstageIdentityResponse with identity information sourced from the token + * + * @public */ -export function decorateWithIdentity( - signInResolverResponse: Omit, +export function prepareBackstageIdentityResponse( + result: BackstageSignInResult, ): BackstageIdentityResponse { - const { sub, ent } = parseJwtPayload(signInResolverResponse.token); + const { sub, ent } = parseJwtPayload(result.token); return { - ...signInResolverResponse, + ...{ + // TODO: idToken is for backwards compatibility and can be removed in the future + idToken: result.token, + ...result, + }, identity: { type: 'user', userEntityRef: sub, diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index d0f7791aaa..8a4afd1fa6 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -38,7 +38,7 @@ import { TokenIssuer } from '../../identity/types'; import { isError } from '@backstage/errors'; import { CatalogIdentityClient } from '../../lib/catalog'; import { Logger } from 'winston'; -import { decorateWithIdentity } from '../decorateWithIdentity'; +import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; /** @public */ export type SamlAuthResult = { @@ -118,7 +118,8 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { }, ); - response.backstageIdentity = decorateWithIdentity(signInResponse); + response.backstageIdentity = + prepareBackstageIdentityResponse(signInResponse); } return postMessageResponse(res, this.appUrl, { diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 9f93e6fa21..dafa52163c 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -164,6 +164,14 @@ export type BackstageUserIdentity = { ownershipEntityRefs: string[]; }; +/** + * A representation of a successful Backstage sign-in. + * + * Compared to the {@link BackstageIdentityResponse} this type omits + * the decoded identity information embedded in the token. + * + * @public + */ export interface BackstageSignInResult { /** * An opaque ID that uniquely identifies the user within Backstage. From 18b7b795c102a3026abf0e472e0949e3aa2fc787 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Dec 2021 17:14:18 +0100 Subject: [PATCH 039/116] core-components: docs + fix API report warnings Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- packages/core-components/api-report.md | 18 ++------- .../src/layout/SignInPage/UserIdentity.ts | 39 ++++++++++++++++--- 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 31008624db..d57f618b57 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -2321,24 +2321,15 @@ export function useQueryParamState( // @public (undocumented) export function UserIcon(props: IconComponentProps): JSX.Element; -// Warning: (ae-missing-release-tag) "UserIdentity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class UserIdentity implements IdentityApi { - // (undocumented) static create(options: { identity: BackstageUserIdentity; authApi: ProfileInfoApi & BackstageIdentityApi & SessionApi; profile?: ProfileInfo; - }): UserIdentity; - // Warning: (ae-forgotten-export) The symbol "GuestUserIdentity" needs to be exported by the entry point index.d.ts - // - // (undocumented) - static createGuest(): GuestUserIdentity; - // Warning: (ae-forgotten-export) The symbol "LegacyUserIdentity" needs to be exported by the entry point index.d.ts - // - // (undocumented) - static fromLegacy(result: SignInResult): LegacyUserIdentity; + }): IdentityApi; + static createGuest(): IdentityApi; + static fromLegacy(result: SignInResult): IdentityApi; // (undocumented) getBackstageIdentity(): Promise; // (undocumented) @@ -2400,5 +2391,4 @@ export type WarningPanelClassKey = // src/components/TabbedLayout/RoutedTabs.d.ts:9:5 - (ae-forgotten-export) The symbol "SubRoute" needs to be exported by the entry point index.d.ts // src/components/Table/Table.d.ts:20:5 - (ae-forgotten-export) The symbol "SelectedFilters" needs to be exported by the entry point index.d.ts // src/layout/ErrorBoundary/ErrorBoundary.d.ts:8:5 - (ae-forgotten-export) The symbol "SlackChannel" needs to be exported by the entry point index.d.ts -// src/layout/SignInPage/UserIdentity.d.ts:20:9 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/core-components" does not have an export "IdentityApi" ``` diff --git a/packages/core-components/src/layout/SignInPage/UserIdentity.ts b/packages/core-components/src/layout/SignInPage/UserIdentity.ts index 1461c48a49..f749711d3c 100644 --- a/packages/core-components/src/layout/SignInPage/UserIdentity.ts +++ b/packages/core-components/src/layout/SignInPage/UserIdentity.ts @@ -27,27 +27,49 @@ import { import { GuestUserIdentity } from './GuestUserIdentity'; import { LegacyUserIdentity } from './LegacyUserIdentity'; +/** + * An implementation of the IdentityApi that is constructed using + * various backstage user identity representations. + * + * @public + */ export class UserIdentity implements IdentityApi { - static createGuest() { + /** + * Creates a new IdentityApi that acts as a Guest User. + * + * @public + */ + static createGuest(): IdentityApi { return new GuestUserIdentity(); } - static fromLegacy(result: SignInResult) { + /** + * Creates a new IdentityApi using a legacy SignInResult object. + * + * @public + */ + static fromLegacy(result: SignInResult): IdentityApi { return LegacyUserIdentity.fromResult(result); } + /** + * Creates a new IdentityApi implementation using a user identity + * and an auth API that will be used to request backstage tokens. + * + * @public + */ static create(options: { identity: BackstageUserIdentity; authApi: ProfileInfoApi & BackstageIdentityApi & SessionApi; /** * Passing a profile synchronously allows the deprecated `getProfile` method to be - * called by consumers of the {@link IdentityApi}. If you do not have any consumers - * of that method then this is safe to leave out. + * called by consumers of the {@link @backstage/core-plugin-api#IdentityApi}. If you + * do not have any consumers of that method then this is safe to leave out. * * @deprecated Only provide this if you have plugins that call the synchronous `getProfile` method, which is also deprecated. */ profile?: ProfileInfo; - }) { + }): IdentityApi { return new UserIdentity(options.identity, options.authApi, options.profile); } @@ -59,6 +81,7 @@ export class UserIdentity implements IdentityApi { private readonly profile?: ProfileInfo, ) {} + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getUserId} */ getUserId(): string { const ref = this.identity.userEntityRef; const match = /^([^:/]+:)?([^:/]+\/)?([^:/]+)$/.exec(ref); @@ -69,11 +92,13 @@ export class UserIdentity implements IdentityApi { return match[3]; } + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getIdToken} */ async getIdToken(): Promise { const identity = await this.authApi.getBackstageIdentity(); return identity!.token; } + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getProfile} */ getProfile(): ProfileInfo { if (!this.profile) { throw new Error( @@ -83,20 +108,24 @@ export class UserIdentity implements IdentityApi { return this.profile; } + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getProfileInfo} */ async getProfileInfo(): Promise { const profile = await this.authApi.getProfile(); return profile!; } + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getBackstageIdentity} */ async getBackstageIdentity(): Promise { return this.identity; } + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getCredentials} */ async getCredentials(): Promise<{ token?: string | undefined }> { const identity = await this.authApi.getBackstageIdentity(); return { token: identity!.token }; } + /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.signOut} */ async signOut(): Promise { return this.authApi.signOut(); } From eb56221f522491438640f74844f6a03da29c37fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Dec 2021 04:10:23 +0000 Subject: [PATCH 040/116] chore(deps): bump rc-progress from 3.1.3 to 3.1.4 Bumps [rc-progress](https://github.com/react-component/progress) from 3.1.3 to 3.1.4. - [Release notes](https://github.com/react-component/progress/releases) - [Changelog](https://github.com/react-component/progress/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-component/progress/compare/v3.1.3...v3.1.4) --- updated-dependencies: - dependency-name: rc-progress 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 98e69c70a9..8bdb8e7d40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24039,9 +24039,9 @@ raw-loader@^4.0.2: schema-utils "^3.0.0" rc-progress@^3.0.0: - version "3.1.3" - resolved "https://registry.npmjs.org/rc-progress/-/rc-progress-3.1.3.tgz#d77d8fd26d9d948d72c2a28b64b71a6e86df2426" - integrity sha512-Jl4fzbBExHYMoC6HBPzel0a9VmhcSXx24LVt/mdhDM90MuzoMCJjXZAlhA0V0CJi+SKjMhfBoIQ6Lla1nD4QNw== + version "3.1.4" + resolved "https://registry.npmjs.org/rc-progress/-/rc-progress-3.1.4.tgz#66040d0fae7d8ced2b38588378eccb2864bad615" + integrity sha512-XBAif08eunHssGeIdxMXOmRQRULdHaDdIFENQ578CMb4dyewahmmfJRyab+hw4KH4XssEzzYOkAInTLS7JJG+Q== dependencies: "@babel/runtime" "^7.10.1" classnames "^2.2.6" From 11b7c39fb6c19a76d0b8d663013aadca05fa0814 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 7 Dec 2021 10:52:27 +0100 Subject: [PATCH 041/116] chore: added caching for successful profile retrieval Signed-off-by: blam --- .../layout/SignInPage/UserIdentity.test.ts | 83 +++++++++++++++++++ .../src/layout/SignInPage/UserIdentity.ts | 14 +++- .../src/lib/oauth/OAuthAdapter.test.ts | 1 + .../src/providers/aws-alb/provider.test.ts | 2 + 4 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 packages/core-components/src/layout/SignInPage/UserIdentity.test.ts diff --git a/packages/core-components/src/layout/SignInPage/UserIdentity.test.ts b/packages/core-components/src/layout/SignInPage/UserIdentity.test.ts new file mode 100644 index 0000000000..6b136ed65d --- /dev/null +++ b/packages/core-components/src/layout/SignInPage/UserIdentity.test.ts @@ -0,0 +1,83 @@ +/* + * 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 { BackstageUserIdentity, ProfileInfo } from '@backstage/core-plugin-api'; +import { UserIdentity } from './UserIdentity'; + +describe('UserIdentity', () => { + it('should cache a successful response from the AuthApi for getProfile', async () => { + const mockIdentity: BackstageUserIdentity = { + type: 'user', + userEntityRef: 'user:default/blam', + ownershipEntityRefs: [], + }; + + const mockProfileInfo: ProfileInfo = { + displayName: 'Blam', + email: 'blob@boop.com', + }; + + const mockAuthApi: any = { + getProfile: jest.fn().mockResolvedValue(mockProfileInfo), + }; + + const userIdentity = UserIdentity.create({ + authApi: mockAuthApi, + identity: mockIdentity, + }); + + await userIdentity.getProfileInfo(); + await userIdentity.getProfileInfo(); + + const response = await userIdentity.getProfileInfo(); + + expect(mockAuthApi.getProfile).toHaveBeenCalledTimes(1); + + expect(response).toEqual(mockProfileInfo); + }); + + it('should not cache failures for the AuthApi for getProfile', async () => { + const mockIdentity: BackstageUserIdentity = { + type: 'user', + userEntityRef: 'user:default/blam', + ownershipEntityRefs: [], + }; + + const mockProfileInfo: ProfileInfo = { + displayName: 'Blam', + email: 'blob@boop.com', + }; + + const mockAuthApi: any = { + getProfile: jest + .fn() + .mockRejectedValueOnce(new Error('boop')) + .mockResolvedValueOnce(mockProfileInfo), + }; + + const userIdentity = UserIdentity.create({ + authApi: mockAuthApi, + identity: mockIdentity, + }); + + await expect(() => userIdentity.getProfileInfo()).rejects.toThrow('boop'); + const response = await userIdentity.getProfileInfo(); + + expect(mockAuthApi.getProfile).toHaveBeenCalledTimes(2); + + expect(response).toEqual(mockProfileInfo); + }); +}); diff --git a/packages/core-components/src/layout/SignInPage/UserIdentity.ts b/packages/core-components/src/layout/SignInPage/UserIdentity.ts index f749711d3c..7781c79154 100644 --- a/packages/core-components/src/layout/SignInPage/UserIdentity.ts +++ b/packages/core-components/src/layout/SignInPage/UserIdentity.ts @@ -34,6 +34,7 @@ import { LegacyUserIdentity } from './LegacyUserIdentity'; * @public */ export class UserIdentity implements IdentityApi { + private profilePromise?: Promise; /** * Creates a new IdentityApi that acts as a Guest User. * @@ -110,8 +111,17 @@ export class UserIdentity implements IdentityApi { /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getProfileInfo} */ async getProfileInfo(): Promise { - const profile = await this.authApi.getProfile(); - return profile!; + if (this.profilePromise) { + return await this.profilePromise; + } + + try { + this.profilePromise = this.authApi.getProfile() as Promise; + return await this.profilePromise; + } catch (ex) { + this.profilePromise = undefined; + throw ex; + } } /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getBackstageIdentity} */ diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index 91711f67ad..92c76b04b7 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -220,6 +220,7 @@ describe('OAuthAdapter', () => { backstageIdentity: { id: mockResponseData.backstageIdentity.id, token: mockResponseData.backstageIdentity.token, + idToken: mockResponseData.backstageIdentity.token, identity: { ownershipEntityRefs: ['user:default/jimmymarkum'], type: 'user', diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts index 9e4aed2030..048128f942 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts @@ -139,6 +139,8 @@ describe('AwsAlbAuthProvider', () => { id: 'user.name', token: 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', + idToken: + 'eyblob.eyJzdWIiOiJqaW1teW1hcmt1bSIsImVudCI6WyJ1c2VyOmRlZmF1bHQvamltbXltYXJrdW0iXX0=.eyblob', identity: { ownershipEntityRefs: ['user:default/jimmymarkum'], type: 'user', From 8f461e6043288a3f5dc459de62bf1919c81e142c Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 7 Dec 2021 10:59:52 +0100 Subject: [PATCH 042/116] auth-backend(fix): Add basicAuth option to OAuth provider Signed-off-by: Johan Haals --- .changeset/clean-apples-breathe.md | 6 ++++++ .../src/providers/oauth2/provider.ts | 17 +++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 .changeset/clean-apples-breathe.md diff --git a/.changeset/clean-apples-breathe.md b/.changeset/clean-apples-breathe.md new file mode 100644 index 0000000000..f946b9496a --- /dev/null +++ b/.changeset/clean-apples-breathe.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Fixes potential bug introduced in `0.4.10` which causes `OAuth2AuthProvider` to authenticate using credentials in both POST payload and headers. +This might break some stricter OAuth2 implementations so there is now a `basicAuth` config option that can manually be set to `true` to enable this behavior. diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 1160ad19c9..b7978bcf79 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -59,6 +59,7 @@ export type OAuth2AuthProviderOptions = OAuthProviderOptions & { tokenUrl: string; scope?: string; logger: Logger; + basicAuth?: boolean; }; export class OAuth2AuthProvider implements OAuthHandlers { @@ -85,12 +86,14 @@ export class OAuth2AuthProvider implements OAuthHandlers { tokenURL: options.tokenUrl, passReqToCallback: false as true, scope: options.scope, - customHeaders: { - Authorization: `Basic ${this.encodeClientCredentials( - options.clientId, - options.clientSecret, - )}`, - }, + customHeaders: options.basicAuth + ? { + Authorization: `Basic ${this.encodeClientCredentials( + options.clientId, + options.clientSecret, + )}`, + } + : undefined, }, ( accessToken: any, @@ -244,6 +247,7 @@ export const createOAuth2Provider = ( const authorizationUrl = envConfig.getString('authorizationUrl'); const tokenUrl = envConfig.getString('tokenUrl'); const scope = envConfig.getOptionalString('scope'); + const basicAuth = envConfig.getOptionalBoolean('basicAuth'); const disableRefresh = envConfig.getOptionalBoolean('disableRefresh') ?? false; @@ -280,6 +284,7 @@ export const createOAuth2Provider = ( tokenUrl, scope, logger, + basicAuth, }); return OAuthAdapter.fromConfig(globalConfig, provider, { From 5c1840c16e2aff35d482d0d9801bce7f46c76bc1 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Tue, 7 Dec 2021 10:06:26 +0100 Subject: [PATCH 043/116] Rename to DatabaseManagerOptions and export Signed-off-by: Marcus Eide --- packages/backend-common/api-report.md | 11 +++++++++-- .../backend-common/src/database/DatabaseManager.ts | 14 +++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 09a95b00ea..d6edfb0ce4 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -175,10 +175,17 @@ export function createStatusCheckRouter(options: { // @public (undocumented) export class DatabaseManager { forPlugin(pluginId: string): PluginDatabaseManager; - // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts - static fromConfig(config: Config, options?: Options): DatabaseManager; + static fromConfig( + config: Config, + options?: DatabaseManagerOptions, + ): DatabaseManager; } +// @public +export type DatabaseManagerOptions = { + migrations: PluginDatabaseManager['migrations']; +}; + // @public (undocumented) export class DockerContainerRunner implements ContainerRunner { constructor({ dockerClient }: { dockerClient: Docker }); diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index e564f2e0e6..befdd51218 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -36,7 +36,12 @@ function pluginPath(pluginId: string): string { return `plugin.${pluginId}`; } -type Options = { +/** + * Configuration options object. + * + * @public + */ +export type DatabaseManagerOptions = { migrations: PluginDatabaseManager['migrations']; }; @@ -53,7 +58,10 @@ export class DatabaseManager { * @param config - The loaded application configuration. * @param options - An optional configuration object. */ - static fromConfig(config: Config, options?: Options): DatabaseManager { + static fromConfig( + config: Config, + options?: DatabaseManagerOptions, + ): DatabaseManager { const databaseConfig = config.getConfig('backend.database'); return new DatabaseManager( @@ -66,7 +74,7 @@ export class DatabaseManager { private constructor( private readonly config: Config, private readonly prefix: string = 'backstage_plugin_', - private readonly options?: Options, + private readonly options?: DatabaseManagerOptions, ) {} /** From 64db0efffe52cd5e64173fd8c6b74f141f9b157a Mon Sep 17 00:00:00 2001 From: Rogerio Angeliski Date: Mon, 6 Dec 2021 13:20:50 -0300 Subject: [PATCH 044/116] chore: update build to use cjs format Signed-off-by: Rogerio Angeliski --- .changeset/metal-timers-shout.md | 5 +++++ plugins/scaffolder-backend-module-rails/package.json | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .changeset/metal-timers-shout.md diff --git a/.changeset/metal-timers-shout.md b/.changeset/metal-timers-shout.md new file mode 100644 index 0000000000..76dd30aadc --- /dev/null +++ b/.changeset/metal-timers-shout.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-rails': minor +--- + +update publish format from ESM to CJS diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index cac3d7ea9b..51810a2c80 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -8,12 +8,12 @@ "private": false, "publishConfig": { "access": "public", - "main": "dist/index.esm.js", + "main": "dist/index.cjs.js", "types": "dist/index.d.ts" }, "scripts": { - "build": "backstage-cli plugin:build", - "start": "backstage-cli plugin:serve", + "build": "backstage-cli backend:build", + "start": "backstage-cli backend:dev", "lint": "backstage-cli lint", "test": "backstage-cli test", "prepack": "backstage-cli prepack", From 5a59f5507e2f6be0d61d1a68488007d6b4522e66 Mon Sep 17 00:00:00 2001 From: Radoslaw Wielonski Date: Tue, 7 Dec 2021 11:35:08 +0100 Subject: [PATCH 045/116] feat: add flag for default error handler Signed-off-by: Radoslaw Wielonski --- .../src/service/lib/ServiceBuilderImpl.test.ts | 3 ++- .../src/service/lib/ServiceBuilderImpl.ts | 16 +++++++++++++++- packages/backend-common/src/service/types.ts | 7 +++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts index 665eaa6550..10363f9d1b 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts @@ -40,11 +40,12 @@ describe('ServiceBuilderImpl', () => { const serviceBuilder = new ServiceBuilderImpl(module); const customErrorHandler = ( error: Error, + // @ts-ignore req: Request, + // @ts-ignore res: Response, next: NextFunction, ) => { - console.log(req, res); next(error); }; serviceBuilder.setErrorHandler(customErrorHandler); diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index 8d6404076f..54f145539f 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -67,6 +67,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { private routers: [string, Router][]; private requestLoggingHandler: RequestLoggingHandlerFactory | undefined; private errorHandler: ErrorRequestHandler | undefined; + private useDefaultErrorHandler: boolean; // Reference to the module where builder is created - needed for hot module // reloading private module: NodeModule; @@ -74,6 +75,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { constructor(moduleRef: NodeModule) { this.routers = []; this.module = moduleRef; + this.useDefaultErrorHandler = true; } loadConfig(config: Config): ServiceBuilder { @@ -158,6 +160,11 @@ export class ServiceBuilderImpl implements ServiceBuilder { return this; } + disableDefaultErrorHandler() { + this.useDefaultErrorHandler = false; + return this; + } + async start(): Promise { const app = express(); const { port, host, logger, corsOptions, httpsSettings, helmetOptions } = @@ -175,7 +182,14 @@ export class ServiceBuilderImpl implements ServiceBuilder { app.use(root, route); } app.use(notFoundHandler()); - app.use(this.errorHandler ?? defaultErrorHandler()); + + if (this.errorHandler) { + app.use(this.errorHandler); + } + + if (this.useDefaultErrorHandler) { + app.use(defaultErrorHandler()); + } const server: http.Server = httpsSettings ? await createHttpsServer(app, httpsSettings, logger) diff --git a/packages/backend-common/src/service/types.ts b/packages/backend-common/src/service/types.ts index 37bfec3c43..3ec4c7aa0e 100644 --- a/packages/backend-common/src/service/types.ts +++ b/packages/backend-common/src/service/types.ts @@ -107,6 +107,13 @@ export type ServiceBuilder = { */ setErrorHandler(errorHandler: ErrorRequestHandler): ServiceBuilder; + /** + * Disable default error handler + * + * If it's not called, default error handler is used + */ + disableDefaultErrorHandler(): ServiceBuilder; + /** * Starts the server using the given settings. */ From af77b33895ad446820aaefbdd767b261e3aa0055 Mon Sep 17 00:00:00 2001 From: Radoslaw Wielonski Date: Tue, 7 Dec 2021 11:36:01 +0100 Subject: [PATCH 046/116] docs: update api-report.md with error handler changes Signed-off-by: Radoslaw Wielonski --- packages/backend-common/api-report.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 9c66c0573e..01c714b36e 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -561,6 +561,8 @@ export type ServiceBuilder = { setRequestLoggingHandler( requestLoggingHandler: RequestLoggingHandlerFactory, ): ServiceBuilder; + setErrorHandler(errorHandler: ErrorRequestHandler): ServiceBuilder; + disableDefaultErrorHandler(): ServiceBuilder; start(): Promise; }; From 13ae212d21c137e3f1138a84f01fcf51cd904945 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 7 Dec 2021 11:55:04 +0100 Subject: [PATCH 047/116] Rename option from basicAuth to includeBasicAuth Signed-off-by: Johan Haals --- .changeset/clean-apples-breathe.md | 2 +- plugins/auth-backend/src/providers/oauth2/provider.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.changeset/clean-apples-breathe.md b/.changeset/clean-apples-breathe.md index f946b9496a..f3340bf8cd 100644 --- a/.changeset/clean-apples-breathe.md +++ b/.changeset/clean-apples-breathe.md @@ -3,4 +3,4 @@ --- Fixes potential bug introduced in `0.4.10` which causes `OAuth2AuthProvider` to authenticate using credentials in both POST payload and headers. -This might break some stricter OAuth2 implementations so there is now a `basicAuth` config option that can manually be set to `true` to enable this behavior. +This might break some stricter OAuth2 implementations so there is now a `includeBasicAuth` config option that can manually be set to `true` to enable this behavior. diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index b7978bcf79..2f9c739860 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -59,7 +59,7 @@ export type OAuth2AuthProviderOptions = OAuthProviderOptions & { tokenUrl: string; scope?: string; logger: Logger; - basicAuth?: boolean; + includeBasicAuth?: boolean; }; export class OAuth2AuthProvider implements OAuthHandlers { @@ -86,7 +86,7 @@ export class OAuth2AuthProvider implements OAuthHandlers { tokenURL: options.tokenUrl, passReqToCallback: false as true, scope: options.scope, - customHeaders: options.basicAuth + customHeaders: options.includeBasicAuth ? { Authorization: `Basic ${this.encodeClientCredentials( options.clientId, @@ -247,7 +247,7 @@ export const createOAuth2Provider = ( const authorizationUrl = envConfig.getString('authorizationUrl'); const tokenUrl = envConfig.getString('tokenUrl'); const scope = envConfig.getOptionalString('scope'); - const basicAuth = envConfig.getOptionalBoolean('basicAuth'); + const includeBasicAuth = envConfig.getOptionalBoolean('includeBasicAuth'); const disableRefresh = envConfig.getOptionalBoolean('disableRefresh') ?? false; @@ -284,7 +284,7 @@ export const createOAuth2Provider = ( tokenUrl, scope, logger, - basicAuth, + includeBasicAuth, }); return OAuthAdapter.fromConfig(globalConfig, provider, { From 06d80ac7a657b5fcf152a6e9a6d9673e92440c37 Mon Sep 17 00:00:00 2001 From: Radoslaw Wielonski Date: Tue, 7 Dec 2021 12:02:58 +0100 Subject: [PATCH 048/116] refactor: remove ts-ignore comments from tests Signed-off-by: Radoslaw Wielonski --- .../src/service/lib/ServiceBuilderImpl.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts index 10363f9d1b..853c7d9a9e 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts @@ -40,21 +40,17 @@ describe('ServiceBuilderImpl', () => { const serviceBuilder = new ServiceBuilderImpl(module); const customErrorHandler = ( error: Error, - // @ts-ignore - req: Request, - // @ts-ignore - res: Response, + _req: Request, + _res: Response, next: NextFunction, ) => { next(error); }; serviceBuilder.setErrorHandler(customErrorHandler); - // @ts-ignore check private attribute expect(serviceBuilder.errorHandler).toEqual(customErrorHandler); }); it('use default error handler', () => { const serviceBuilder = new ServiceBuilderImpl(module); - // @ts-ignore check private attribute expect(serviceBuilder.errorHandler).toBeUndefined(); }); }); From 6026ebb680ccae8306b7699c8b02564a60e5b37b Mon Sep 17 00:00:00 2001 From: Radoslaw Wielonski Date: Tue, 7 Dec 2021 12:24:14 +0100 Subject: [PATCH 049/116] refactor: use prototype to access private properties and avoid // @ts-ignore Signed-off-by: Radoslaw Wielonski --- .../src/service/lib/ServiceBuilderImpl.test.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts index 853c7d9a9e..6229cbb683 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.test.ts @@ -36,8 +36,15 @@ describe('ServiceBuilderImpl', () => { }); describe('setCustomErrorHandler', () => { + it('check if custom error handler is undefined', () => { + const serviceBuilder = new ServiceBuilderImpl(module); + const serviceBuilderProto = Object.getPrototypeOf(serviceBuilder); + expect(serviceBuilderProto.errorHandler).toBeUndefined(); + }); + it('adds custom error handler', () => { const serviceBuilder = new ServiceBuilderImpl(module); + const serviceBuilderProto = Object.getPrototypeOf(serviceBuilder); const customErrorHandler = ( error: Error, _req: Request, @@ -46,12 +53,8 @@ describe('ServiceBuilderImpl', () => { ) => { next(error); }; - serviceBuilder.setErrorHandler(customErrorHandler); - expect(serviceBuilder.errorHandler).toEqual(customErrorHandler); - }); - it('use default error handler', () => { - const serviceBuilder = new ServiceBuilderImpl(module); - expect(serviceBuilder.errorHandler).toBeUndefined(); + serviceBuilderProto.setErrorHandler(customErrorHandler); + expect(serviceBuilderProto.errorHandler).toEqual(customErrorHandler); }); }); }); From dac55f3cc76482c58ac61b994483a9397f0fef26 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Tue, 7 Dec 2021 12:44:16 +0100 Subject: [PATCH 050/116] Make optional Signed-off-by: Marcus Eide --- packages/backend-common/api-report.md | 8 ++++---- .../src/database/DatabaseManager.test.ts | 6 +++--- .../backend-common/src/database/DatabaseManager.ts | 4 ++-- packages/backend-common/src/database/types.ts | 11 ++++------- .../backend-tasks/src/tasks/TaskScheduler.test.ts | 1 - plugins/auth-backend/src/service/standaloneServer.ts | 1 - .../bazaar-backend/src/service/standaloneServer.ts | 2 +- .../src/legacy/service/CatalogBuilder.test.ts | 2 +- .../catalog-backend/src/service/NextCatalogBuilder.ts | 2 +- .../catalog-backend/src/service/standaloneServer.ts | 2 +- .../src/service/standaloneServer.ts | 2 +- .../tech-insights-backend/src/service/router.test.ts | 1 - 12 files changed, 18 insertions(+), 24 deletions(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index d6edfb0ce4..e623f656ab 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -183,7 +183,7 @@ export class DatabaseManager { // @public export type DatabaseManagerOptions = { - migrations: PluginDatabaseManager['migrations']; + migrations?: PluginDatabaseManager['migrations']; }; // @public (undocumented) @@ -403,8 +403,8 @@ export type PluginCacheManager = { // @public export interface PluginDatabaseManager { getClient(): Promise; - migrations: { - apply: boolean; + migrations?: { + skip?: boolean; }; } @@ -656,5 +656,5 @@ export function useHotMemoize(_module: NodeModule, valueFactory: () => T): T; // Warnings were encountered during analysis: // -// src/database/types.d.ts:26:12 - (tsdoc-undefined-tag) The TSDoc tag "@default" is not defined in this configuration +// src/database/types.d.ts:23:12 - (tsdoc-undefined-tag) The TSDoc tag "@default" is not defined in this configuration ``` diff --git a/packages/backend-common/src/database/DatabaseManager.test.ts b/packages/backend-common/src/database/DatabaseManager.test.ts index 4a44feec4a..4931a52f35 100644 --- a/packages/backend-common/src/database/DatabaseManager.test.ts +++ b/packages/backend-common/src/database/DatabaseManager.test.ts @@ -63,17 +63,17 @@ describe('DatabaseManager', () => { const database = DatabaseManager.fromConfig(config); const client = database.forPlugin('test'); - expect(client.migrations.apply).toBe(true); + expect(client.migrations?.skip).toBe(false); }); it('handles migrations options', () => { const config = new ConfigReader(backendConfig); const database = DatabaseManager.fromConfig(config, { - migrations: { apply: false }, + migrations: { skip: true }, }); const client = database.forPlugin('test'); - expect(client.migrations.apply).toBe(false); + expect(client.migrations?.skip).toBe(true); }); }); diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index befdd51218..ee3e8b1a89 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -42,7 +42,7 @@ function pluginPath(pluginId: string): string { * @public */ export type DatabaseManagerOptions = { - migrations: PluginDatabaseManager['migrations']; + migrations?: PluginDatabaseManager['migrations']; }; /** @public */ @@ -92,7 +92,7 @@ export class DatabaseManager { return _this.getDatabase(pluginId); }, migrations: { - apply: true, + skip: false, ..._this.options?.migrations, }, }; diff --git a/packages/backend-common/src/database/types.ts b/packages/backend-common/src/database/types.ts index 4cfc86e240..344f1088b8 100644 --- a/packages/backend-common/src/database/types.ts +++ b/packages/backend-common/src/database/types.ts @@ -34,16 +34,13 @@ export interface PluginDatabaseManager { /** * This property is used to control the behavior of database migrations. */ - migrations: { + migrations?: { /** - * apply can be used to determine if database migrations - * should be performed. + * skip database migrations. Useful if connecting to a read-only database. * - * Useful if connecting to a read-only database. - * - * @default true + * @default false */ - apply: boolean; + skip?: boolean; }; } diff --git a/packages/backend-tasks/src/tasks/TaskScheduler.test.ts b/packages/backend-tasks/src/tasks/TaskScheduler.test.ts index 6c9a6989c7..ce8e797503 100644 --- a/packages/backend-tasks/src/tasks/TaskScheduler.test.ts +++ b/packages/backend-tasks/src/tasks/TaskScheduler.test.ts @@ -33,7 +33,6 @@ describe('TaskScheduler', () => { const databaseManager: Partial = { forPlugin: () => ({ getClient: async () => knex, - migrations: { apply: true }, }), }; return databaseManager as DatabaseManager; diff --git a/plugins/auth-backend/src/service/standaloneServer.ts b/plugins/auth-backend/src/service/standaloneServer.ts index 9009af4aa6..15ffe1d053 100644 --- a/plugins/auth-backend/src/service/standaloneServer.ts +++ b/plugins/auth-backend/src/service/standaloneServer.ts @@ -56,7 +56,6 @@ export async function startStandaloneServer( async getClient() { return database; }, - migrations: { apply: true }, }, discovery, }); diff --git a/plugins/bazaar-backend/src/service/standaloneServer.ts b/plugins/bazaar-backend/src/service/standaloneServer.ts index 4ef46b7f66..b229f5bcf8 100644 --- a/plugins/bazaar-backend/src/service/standaloneServer.ts +++ b/plugins/bazaar-backend/src/service/standaloneServer.ts @@ -52,7 +52,7 @@ export async function startStandaloneServer( const router = await createRouter({ logger, - database: { getClient: async () => db, migrations: { apply: true } }, + database: { getClient: async () => db }, config: config, }); diff --git a/plugins/catalog-backend/src/legacy/service/CatalogBuilder.test.ts b/plugins/catalog-backend/src/legacy/service/CatalogBuilder.test.ts index 3ba9340716..926aa67635 100644 --- a/plugins/catalog-backend/src/legacy/service/CatalogBuilder.test.ts +++ b/plugins/catalog-backend/src/legacy/service/CatalogBuilder.test.ts @@ -49,7 +49,7 @@ describe('CatalogBuilder', () => { }; const env: CatalogEnvironment = { logger: getVoidLogger(), - database: { getClient: async () => db, migrations: { apply: true } }, + database: { getClient: async () => db }, config: new ConfigReader({}), reader, }; diff --git a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts index 382cac362e..1618ef8d7b 100644 --- a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts @@ -336,7 +336,7 @@ export class NextCatalogBuilder { const parser = this.parser || defaultEntityDataParser; const dbClient = await database.getClient(); - if (database.migrations.apply) { + if (!database.migrations?.skip) { logger.info('Performing database migration'); await applyDatabaseMigrations(dbClient); } diff --git a/plugins/catalog-backend/src/service/standaloneServer.ts b/plugins/catalog-backend/src/service/standaloneServer.ts index 66154b0ddc..7aae3cd47c 100644 --- a/plugins/catalog-backend/src/service/standaloneServer.ts +++ b/plugins/catalog-backend/src/service/standaloneServer.ts @@ -46,7 +46,7 @@ export async function startStandaloneServer( logger.debug('Creating application...'); const builder = new CatalogBuilder({ logger, - database: { getClient: () => db, migrations: { apply: true } }, + database: { getClient: () => db }, config, reader, }); diff --git a/plugins/code-coverage-backend/src/service/standaloneServer.ts b/plugins/code-coverage-backend/src/service/standaloneServer.ts index ca913a2a67..291f78ffc5 100644 --- a/plugins/code-coverage-backend/src/service/standaloneServer.ts +++ b/plugins/code-coverage-backend/src/service/standaloneServer.ts @@ -54,7 +54,7 @@ export async function startStandaloneServer( logger.debug('Starting application server...'); const router = await createRouter({ - database: { getClient: async () => db, migrations: { apply: true } }, + database: { getClient: async () => db }, config, discovery: SingleHostDiscovery.fromConfig(config), urlReader: UrlReaders.default({ logger, config }), diff --git a/plugins/tech-insights-backend/src/service/router.test.ts b/plugins/tech-insights-backend/src/service/router.test.ts index b435136d4d..0b7d3b7c45 100644 --- a/plugins/tech-insights-backend/src/service/router.test.ts +++ b/plugins/tech-insights-backend/src/service/router.test.ts @@ -53,7 +53,6 @@ describe('Tech Insights router tests', () => { }, }) as unknown as Promise; }, - migrations: { apply: true }, }, logger: getVoidLogger(), factRetrievers: [], From 285f7ec2e6276607f01814c2f7c6a56a5dc2edb2 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Tue, 7 Dec 2021 18:13:06 +0530 Subject: [PATCH 051/116] fix apiVersion for scaffolder.backstage.io/v1beta3 in docs Signed-off-by: Himanshu Mishra --- docs/features/software-templates/adding-templates.md | 2 +- docs/features/software-templates/writing-templates.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/features/software-templates/adding-templates.md b/docs/features/software-templates/adding-templates.md index aa565b5960..ed7a103bc4 100644 --- a/docs/features/software-templates/adding-templates.md +++ b/docs/features/software-templates/adding-templates.md @@ -11,7 +11,7 @@ would be good to also have some files in there that can be templated in. A simple `template.yaml` definition might look something like this: ```yaml -apiVersion: backstage.io/v1beta3 +apiVersion: scaffolder.backstage.io/v1beta3 kind: Template # some metadata about the template itself metadata: diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index dc9a2ed1e7..a1dffe1580 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -14,7 +14,7 @@ Let's take a look at a simple example: ```yaml # Notice the v1beta3 version -apiVersion: backstage.io/v1beta3 +apiVersion: scaffolder.backstage.io/v1beta3 kind: Template # some metadata about the template itself metadata: @@ -183,7 +183,7 @@ this: It would look something like the following in a template: ```yaml -apiVersion: backstage.io/v1beta3 +apiVersion: scaffolder.backstage.io/v1beta3 kind: Template metadata: name: v1beta3-demo From 9360b7fac60da6e6f64379cde8c1013f712448f2 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 7 Dec 2021 13:53:17 +0100 Subject: [PATCH 052/116] chore: bumping graphql and graphiql versions to latest to solve broken `create-app` Signed-off-by: blam --- .changeset/chatty-ligers-provide.md | 6 + plugins/api-docs/package.json | 4 +- plugins/graphiql/package.json | 4 +- yarn.lock | 483 +++++++++++++++++++++++----- 4 files changed, 413 insertions(+), 84 deletions(-) create mode 100644 .changeset/chatty-ligers-provide.md diff --git a/.changeset/chatty-ligers-provide.md b/.changeset/chatty-ligers-provide.md new file mode 100644 index 0000000000..2d0c02fbae --- /dev/null +++ b/.changeset/chatty-ligers-provide.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-api-docs': patch +'@backstage/plugin-graphiql': patch +--- + +chore(dependencies): bump `graphiql` package to latest diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index e3ee7ec0aa..a03fca6757 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -41,8 +41,8 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@types/react": "*", - "graphiql": "^1.0.0-alpha.10", - "graphql": "^15.3.0", + "graphiql": "^1.5.12", + "graphql": "^16.0.0", "isomorphic-form-data": "^2.0.0", "react": "^16.13.1", "react-dom": "^16.13.1", diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 49c4d9952c..0e064fb1ad 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -37,8 +37,8 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "graphiql": "^1.0.0-alpha.10", - "graphql": "15.5.0", + "graphiql": "^1.5.12", + "graphql": "^16.0.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-use": "^17.2.4" diff --git a/yarn.lock b/yarn.lock index 98e69c70a9..82603adffe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2626,6 +2626,71 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@codemirror/highlight@^0.19.0": + version "0.19.6" + resolved "https://registry.npmjs.org/@codemirror/highlight/-/highlight-0.19.6.tgz#7f2e066f83f5649e8e0748a3abe0aaeaf64b8ac2" + integrity sha512-+eibu6on9quY8uN3xJ/n3rH+YIDLlpX7YulVmFvqAIz/ukRQ5tWaBmB7fMixHmnmRIRBRZgB8rNtonuMwZSAHQ== + dependencies: + "@codemirror/language" "^0.19.0" + "@codemirror/rangeset" "^0.19.0" + "@codemirror/state" "^0.19.0" + "@codemirror/view" "^0.19.0" + "@lezer/common" "^0.15.0" + style-mod "^4.0.0" + +"@codemirror/language@^0.19.0": + version "0.19.7" + resolved "https://registry.npmjs.org/@codemirror/language/-/language-0.19.7.tgz#9eef8e827692d93a701b18db9d46a42be34ecca6" + integrity sha512-pNNUtYWMIMG0lUSKyUXJr8U0rFiCKsKFXbA2Oj17PC+S1FY99hV0z1vcntW67ekAIZw9DMEUQnLsKBuIbAUX7Q== + dependencies: + "@codemirror/state" "^0.19.0" + "@codemirror/text" "^0.19.0" + "@codemirror/view" "^0.19.0" + "@lezer/common" "^0.15.5" + "@lezer/lr" "^0.15.0" + +"@codemirror/rangeset@^0.19.0": + version "0.19.2" + resolved "https://registry.npmjs.org/@codemirror/rangeset/-/rangeset-0.19.2.tgz#d7a999e4273c00fecef4aba8535a426073cdcddf" + integrity sha512-5d+X8LtmeZtfFtKrSx57bIHRUpKv2HD0b74clp4fGA7qJLLfYehF6FGkJJxJb8lKsqAga1gdjjWr0jiypmIxoQ== + dependencies: + "@codemirror/state" "^0.19.0" + +"@codemirror/state@^0.19.0", "@codemirror/state@^0.19.3": + version "0.19.6" + resolved "https://registry.npmjs.org/@codemirror/state/-/state-0.19.6.tgz#d631f041d39ce41b7891b099fca26cb1fdb9763e" + integrity sha512-sqIQZE9VqwQj7D4c2oz9mfLhlT1ElAzGB5lO1lE33BPyrdNy1cJyCIOecT4cn4VeJOFrnjOeu+IftZ3zqdFETw== + dependencies: + "@codemirror/text" "^0.19.0" + +"@codemirror/stream-parser@^0.19.2": + version "0.19.2" + resolved "https://registry.npmjs.org/@codemirror/stream-parser/-/stream-parser-0.19.2.tgz#793428e55aa7b9daa64cb733973e5d5e3d9a2306" + integrity sha512-hBKRQlyu8GUOrY33xZ6/1kAfNZ8ZUm6cX9a7mPx8zAAqnpz/fpksC/qJRrkg1mPMBwxm+JG4fqAwDGJ3gLVniQ== + dependencies: + "@codemirror/highlight" "^0.19.0" + "@codemirror/language" "^0.19.0" + "@codemirror/state" "^0.19.0" + "@codemirror/text" "^0.19.0" + "@lezer/common" "^0.15.0" + "@lezer/lr" "^0.15.0" + +"@codemirror/text@^0.19.0": + version "0.19.5" + resolved "https://registry.npmjs.org/@codemirror/text/-/text-0.19.5.tgz#75033af2476214e79eae22b81ada618815441c18" + integrity sha512-Syu5Xc7tZzeUAM/y4fETkT0zgGr48rDG+w4U38bPwSIUr+L9S/7w2wDE1WGNzjaZPz12F6gb1gxWiSTg9ocLow== + +"@codemirror/view@^0.19.0": + version "0.19.27" + resolved "https://registry.npmjs.org/@codemirror/view/-/view-0.19.27.tgz#76e5dc19ecb4ce53e9fef1d29245040d7ff64183" + integrity sha512-Uz/LecEf7CyvMWaQBlKtbJCYn0hRnEZ2yYvuZVy9YMhmvGmES6ec7FaKw7lDFFOMLwLbBThc9kfw4DCHreHN1w== + dependencies: + "@codemirror/rangeset" "^0.19.0" + "@codemirror/state" "^0.19.3" + "@codemirror/text" "^0.19.0" + style-mod "^4.0.0" + w3c-keyname "^2.2.4" + "@cspotcode/source-map-consumer@0.8.0": version "0.8.0" resolved "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" @@ -2984,13 +3049,12 @@ stream-events "^1.0.1" xdg-basedir "^4.0.0" -"@graphiql/toolkit@^0.3.2": - version "0.3.2" - resolved "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.3.2.tgz#551753436ada2bc27ea870b7668e5199a958ccfb" - integrity sha512-IweIT9VC8uDovg7kuCO9YqZcnIuWU8IGzrpUisXv6CUNK2Ed1ke8yERDTMmF/rjvLd2DeVZwM8iEOjEs4sUJQw== +"@graphiql/toolkit@^0.4.2": + version "0.4.2" + resolved "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.4.2.tgz#34de819add64672f3f7d4830dffb2094fb8d5366" + integrity sha512-14uG67QrONbRrhXwvBJFsMfcQfexmGhj7dgkputesx9xuPUkcCDNmVULnVA8sGYt8P/rSvjkfQYx3rtfW+GhAQ== dependencies: - "@n1ru4l/push-pull-async-iterable-iterator" "^3.0.0" - graphql-ws "^4.9.0" + "@n1ru4l/push-pull-async-iterable-iterator" "^3.1.0" meros "^1.1.4" "@graphql-codegen/cli@^1.21.3": @@ -3173,6 +3237,16 @@ tslib "~2.2.0" value-or-promise "1.0.6" +"@graphql-tools/batch-execute@^8.3.1": + version "8.3.1" + resolved "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-8.3.1.tgz#0b74c54db5ac1c5b9a273baefc034c2343ebbb74" + integrity sha512-63kHY8ZdoO5FoeDXYHnAak1R3ysMViMPwWC2XUblFckuVLMUPmB2ONje8rjr2CvzWBHAW8c1Zsex+U3xhKtGIA== + dependencies: + "@graphql-tools/utils" "^8.5.1" + dataloader "2.0.0" + tslib "~2.3.0" + value-or-promise "1.0.11" + "@graphql-tools/code-file-loader@^6.3.1": version "6.3.1" resolved "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-6.3.1.tgz#42dfd4db5b968acdb453382f172ec684fa0c34ed" @@ -3195,6 +3269,18 @@ tslib "~2.2.0" value-or-promise "1.0.6" +"@graphql-tools/delegate@^8.4.1", "@graphql-tools/delegate@^8.4.2": + version "8.4.2" + resolved "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-8.4.2.tgz#a61d45719855720304e3656800342cfa17d82558" + integrity sha512-CjggOhiL4WtyG2I3kux+1/p8lQxSFHBj0gwa0NxnQ6Vsnpw7Ig5VP1ovPnitFuBv2k4QdC37Nj2xv2n7DRn8fw== + dependencies: + "@graphql-tools/batch-execute" "^8.3.1" + "@graphql-tools/schema" "^8.3.1" + "@graphql-tools/utils" "^8.5.3" + dataloader "2.0.0" + tslib "~2.3.0" + value-or-promise "1.0.11" + "@graphql-tools/git-loader@^6.2.6": version "6.2.6" resolved "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-6.2.6.tgz#c2226f4b8f51f1c05c9ab2649ba32d49c68cd077" @@ -3223,6 +3309,17 @@ "@graphql-tools/utils" "^7.0.0" tslib "~2.1.0" +"@graphql-tools/graphql-file-loader@^7.3.2": + version "7.3.3" + resolved "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-7.3.3.tgz#7cee2f84f08dc13fa756820b510248b857583d36" + integrity sha512-6kUJZiNpYKVhum9E5wfl5PyLLupEDYdH7c8l6oMrk6c7EPEVs6iSUyB7yQoWrtJccJLULBW2CRQ5IHp5JYK0mA== + dependencies: + "@graphql-tools/import" "^6.5.7" + "@graphql-tools/utils" "^8.5.1" + globby "^11.0.3" + tslib "~2.3.0" + unixify "^1.0.0" + "@graphql-tools/graphql-tag-pluck@^6.2.6", "@graphql-tools/graphql-tag-pluck@^6.5.1": version "6.5.1" resolved "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-6.5.1.tgz#5fb227dbb1e19f4b037792b50f646f16a2d4c686" @@ -3242,6 +3339,15 @@ resolve-from "5.0.0" tslib "~2.2.0" +"@graphql-tools/import@^6.5.7": + version "6.6.1" + resolved "https://registry.npmjs.org/@graphql-tools/import/-/import-6.6.1.tgz#2a7e1ceda10103ffeb8652a48ddc47150b035485" + integrity sha512-i9WA6k+erJMci822o9w9DoX+uncVBK60LGGYW8mdbhX0l7wEubUpA000thJ1aarCusYh0u+ZT9qX0HyVPXu25Q== + dependencies: + "@graphql-tools/utils" "8.5.3" + resolve-from "5.0.0" + tslib "~2.3.0" + "@graphql-tools/json-file-loader@^6.0.0", "@graphql-tools/json-file-loader@^6.2.6": version "6.2.6" resolved "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-6.2.6.tgz#830482cfd3721a0799cbf2fe5b09959d9332739a" @@ -3250,6 +3356,16 @@ "@graphql-tools/utils" "^7.0.0" tslib "~2.0.1" +"@graphql-tools/json-file-loader@^7.3.2": + version "7.3.3" + resolved "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-7.3.3.tgz#45cfde77b9dc4ab6c21575305ae537d2814d237f" + integrity sha512-CN2Qk9rt+Gepa3rb3X/mpxYA5MIYLwZBPj2Njw6lbZ6AaxG+O1ArDCL5ACoiWiBimn1FCOM778uhRM9znd0b3Q== + dependencies: + "@graphql-tools/utils" "^8.5.1" + globby "^11.0.3" + tslib "~2.3.0" + unixify "^1.0.0" + "@graphql-tools/load@^6.0.0", "@graphql-tools/load@^6.2.8": version "6.2.8" resolved "https://registry.npmjs.org/@graphql-tools/load/-/load-6.2.8.tgz#16900fb6e75e1d075cad8f7ea439b334feb0b96a" @@ -3265,6 +3381,16 @@ unixify "1.0.0" valid-url "1.0.9" +"@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", "@graphql-tools/merge@^6.2.14": version "6.2.14" resolved "https://registry.npmjs.org/@graphql-tools/merge/-/merge-6.2.14.tgz#694e2a2785ba47558e5665687feddd2935e9d94e" @@ -3274,6 +3400,14 @@ "@graphql-tools/utils" "^7.7.0" tslib "~2.2.0" +"@graphql-tools/merge@^8.2.1": + version "8.2.1" + resolved "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.2.1.tgz#bf83aa06a0cfc6a839e52a58057a84498d0d51ff" + integrity sha512-Q240kcUszhXiAYudjuJgNuLgy9CryDP3wp83NOZQezfA6h3ByYKU7xI6DiKrdjyVaGpYN3ppUmdj0uf5GaXzMA== + dependencies: + "@graphql-tools/utils" "^8.5.1" + tslib "~2.3.0" + "@graphql-tools/optimize@^1.0.1": version "1.0.1" resolved "https://registry.npmjs.org/@graphql-tools/optimize/-/optimize-1.0.1.tgz#9933fffc5a3c63f95102b1cb6076fb16ac7bb22d" @@ -3317,6 +3451,16 @@ relay-compiler "10.1.0" tslib "~2.0.1" +"@graphql-tools/schema@8.3.1", "@graphql-tools/schema@^8.3.1": + version "8.3.1" + resolved "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.3.1.tgz#1ee9da494d2da457643b3c93502b94c3c4b68c74" + integrity sha512-3R0AJFe715p4GwF067G5i0KCr/XIdvSfDLvTLEiTDQ8V/hwbOHEKHKWlEBHGRQwkG5lwFQlW1aOn7VnlPERnWQ== + dependencies: + "@graphql-tools/merge" "^8.2.1" + "@graphql-tools/utils" "^8.5.1" + tslib "~2.3.0" + value-or-promise "1.0.11" + "@graphql-tools/schema@^7.0.0", "@graphql-tools/schema@^7.1.5": version "7.1.5" resolved "https://registry.npmjs.org/@graphql-tools/schema/-/schema-7.1.5.tgz#07b24e52b182e736a6b77c829fc48b84d89aa711" @@ -3351,6 +3495,38 @@ valid-url "1.0.9" ws "7.4.5" +"@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": + version "8.5.3" + resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.5.3.tgz#404062e62cae9453501197039687749c4885356e" + integrity sha512-HDNGWFVa8QQkoQB0H1lftvaO1X5xUaUDk1zr1qDe0xN1NL0E/CrQdJ5UKLqOvH4hkqVUPxQsyOoAZFkaH6rLHg== + dependencies: + tslib "~2.3.0" + "@graphql-tools/utils@^7.0.0", "@graphql-tools/utils@^7.1.0", "@graphql-tools/utils@^7.1.2", "@graphql-tools/utils@^7.5.0", "@graphql-tools/utils@^7.7.0", "@graphql-tools/utils@^7.7.1", "@graphql-tools/utils@^7.8.1", "@graphql-tools/utils@^7.9.0", "@graphql-tools/utils@^7.9.1": version "7.10.0" resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-7.10.0.tgz#07a4cb5d1bec1ff1dc1d47a935919ee6abd38699" @@ -3371,6 +3547,17 @@ tslib "~2.2.0" value-or-promise "1.0.6" +"@graphql-tools/wrap@^8.3.1": + version "8.3.2" + resolved "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-8.3.2.tgz#d3bcecb7529d071e4ecc4dfc75b9566e3da79d4f" + integrity sha512-7DcOBFB+Dd84x9dxSm7qS4iJONMyfLnCJb8A19vGPffpu4SMJ3sFcgwibKFu5l6mMUiigKgXna2RRgWI+02bKQ== + dependencies: + "@graphql-tools/delegate" "^8.4.2" + "@graphql-tools/schema" "^8.3.1" + "@graphql-tools/utils" "^8.5.3" + tslib "~2.3.0" + value-or-promise "1.0.11" + "@grpc/grpc-js@~1.3.0": version "1.3.2" resolved "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.3.2.tgz#eae97e6daf5abd49a7818aadeca0744dfb1ebca1" @@ -4377,6 +4564,18 @@ npmlog "^4.1.2" write-file-atomic "^3.0.3" +"@lezer/common@^0.15.0", "@lezer/common@^0.15.5": + version "0.15.10" + resolved "https://registry.npmjs.org/@lezer/common/-/common-0.15.10.tgz#662da668f46244fb20bfaada67b43b3d0463b344" + integrity sha512-vlr+be73zTDoQBIknBVOh/633tmbQcjxUu9PIeVeYESeBK3V6TuBW96RRFg93Y2cyK9lglz241gOgSn452HFvA== + +"@lezer/lr@^0.15.0": + version "0.15.5" + resolved "https://registry.npmjs.org/@lezer/lr/-/lr-0.15.5.tgz#4bce44169c441d9dda7be398f5202ea65c5f1138" + integrity sha512-DEcLyhdmBxD1foQe7RegLrSlfS/XaTMGLkO5evkzHWAQKh/JnFWp7j7iNB7s2EpxzRrBCh0U+W7JDCeFhv2mng== + dependencies: + "@lezer/common" "^0.15.0" + "@manypkg/find-root@^1.1.0": version "1.1.0" resolved "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz#a62d8ed1cd7e7d4c11d9d52a8397460b5d4ad29f" @@ -4674,7 +4873,12 @@ outvariant "^1.2.0" strict-event-emitter "^0.2.0" -"@n1ru4l/push-pull-async-iterable-iterator@^3.0.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== + +"@n1ru4l/push-pull-async-iterable-iterator@^3.1.0": version "3.1.0" resolved "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-3.1.0.tgz#be450c97d1c7cd6af1a992d53232704454345df9" integrity sha512-K4scWxGhdQM0masHHy4gIQs2iGiLEXCrXttumknyPJqtdl4J179BjpibWSSQ1fxKdCcHgIlCTKXJU6cMM6D6Wg== @@ -7431,7 +7635,7 @@ dependencies: "@types/json-schema" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@7.0.9", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.9" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== @@ -8276,6 +8480,13 @@ dependencies: "@types/node" "*" +"@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== + dependencies: + "@types/node" "*" + "@types/whatwg-streams@^0.0.7": version "0.0.7" resolved "https://registry.npmjs.org/@types/whatwg-streams/-/whatwg-streams-0.0.7.tgz#28bfe73dc850562296367249c4b32a50db81e9d3" @@ -8295,6 +8506,13 @@ dependencies: "@types/node" "*" +"@types/ws@^8.0.0": + version "8.2.2" + resolved "https://registry.npmjs.org/@types/ws/-/ws-8.2.2.tgz#7c5be4decb19500ae6b3d563043cd407bf366c21" + integrity sha512-NOn5eIcgWLOo6qW8AcuLZ7G8PycXu0xTxxkS6Q18VWFxgPUSOwV0pBj2a/4viNZVu25i7RIB7GttdkAIUUXOOg== + dependencies: + "@types/node" "*" + "@types/xml2js@*", "@types/xml2js@^0.4.7": version "0.4.8" resolved "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.8.tgz#84c120c864a5976d0b5cf2f930a75d850fc2b03a" @@ -11273,13 +11491,13 @@ code-point-at@^1.0.0: resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= -codemirror-graphql@^1.0.3: - version "1.1.0" - resolved "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-1.1.0.tgz#dd22ddf7761efa9131fa99a70a4a85fe653484e5" - integrity sha512-bp2XUg7epL07kJcylM8VCISK6X+rFsHL2lUkPQAw2v721MVhn+80FgjMP8tiZCOfJgHn1+JgsA71L5nOHWgUdA== +codemirror-graphql@^1.2.8: + version "1.2.8" + resolved "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-1.2.8.tgz#4d3845d786665776eb5c44b0948bfa8f9860077f" + integrity sha512-/SlF24YNWirA8SyyaiFkrwPVIhPS/OkMNRHkL3TeZKhJIv3wuGAa90B4DYqY17MBN2QN1+RYd/71eYZD/fvj1Q== dependencies: - graphql-language-service-interface "^2.9.0" - graphql-language-service-parser "^1.10.0" + "@codemirror/stream-parser" "^0.19.2" + graphql-language-service "^4.1.1" codemirror@^5.58.2: version "5.63.3" @@ -11881,6 +12099,17 @@ cosmiconfig@7.0.0, cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" +cosmiconfig@7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + cosmiconfig@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" @@ -12039,6 +12268,16 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" +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" + crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -14547,6 +14786,11 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" +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" @@ -15893,19 +16137,19 @@ grapheme-splitter@^1.0.4: resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== -graphiql@^1.0.0-alpha.10: - version "1.4.7" - resolved "https://registry.npmjs.org/graphiql/-/graphiql-1.4.7.tgz#6a35acf0786d7518fbb986b75bf0a3d752c19c1a" - integrity sha512-oHsBTzdWTbRJhqazbjrC6wY7YInViErAeXLqetCxdFFu2Zk5FV3V3rs7KPrCyr7kM6lW0nfXMzIfKuIgxAqx7g== +graphiql@^1.5.12: + version "1.5.13" + resolved "https://registry.npmjs.org/graphiql/-/graphiql-1.5.13.tgz#7706c56504213598641e8300a612a7dccaba95d5" + integrity sha512-lfG7FZzjDb4jwAP5mU+IiYvkTcZ/OygbIlk6JJrB0CPBqMl4k/3dEaZFmyKjmy54xTo8bkiH28VgpcF8VPc1eg== dependencies: - "@graphiql/toolkit" "^0.3.2" + "@graphiql/toolkit" "^0.4.2" codemirror "^5.58.2" - codemirror-graphql "^1.0.3" + codemirror-graphql "^1.2.8" copy-to-clipboard "^3.2.0" dset "^3.1.0" entities "^2.0.0" escape-html "^1.0.3" - graphql-language-service "^3.1.6" + graphql-language-service "^4.1.1" markdown-it "^12.2.0" graphlib@^2.1.8: @@ -15932,6 +16176,23 @@ graphql-config@^3.0.2, graphql-config@^3.3.0: minimatch "3.0.4" string-env-interpolation "1.0.1" +graphql-config@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/graphql-config/-/graphql-config-4.1.0.tgz#a3b28d3fb537952ebeb69c75e4430605a10695e3" + integrity sha512-Myqay6pmdcmX3KqoH+bMbeKZ1cTODpHS2CxF1ZzNnfTE+YUpGTcp01bOw6LpzamRb0T/WTYtGFbZeXGo9Hab2Q== + dependencies: + "@endemolshinegroup/cosmiconfig-typescript-loader" "3.0.2" + "@graphql-tools/graphql-file-loader" "^7.3.2" + "@graphql-tools/json-file-loader" "^7.3.2" + "@graphql-tools/load" "^7.4.1" + "@graphql-tools/merge" "^8.2.1" + "@graphql-tools/url-loader" "^7.4.2" + "@graphql-tools/utils" "^8.5.1" + cosmiconfig "7.0.1" + cosmiconfig-toml-loader "1.0.0" + minimatch "3.0.4" + string-env-interpolation "1.0.1" + graphql-extensions@^0.15.0: version "0.15.0" resolved "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.15.0.tgz#3f291f9274876b0c289fa4061909a12678bd9817" @@ -15941,63 +16202,51 @@ graphql-extensions@^0.15.0: apollo-server-env "^3.1.0" apollo-server-types "^0.9.0" -graphql-language-service-interface@^2.9.0: - version "2.9.1" - resolved "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.9.1.tgz#be0b11b06b78730ea9d250e0e2290e7ed9c8d283" - integrity sha512-yGsE67fxJBXxY82+rLDMvUpmzpOUM8XFB+k+xOTUyABWs27osKaoGiuDDXAVGg1adhm+cpunWbipe763ZJkAVA== +graphql-language-service-interface@^2.10.1: + version "2.10.1" + resolved "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.10.1.tgz#0f81a3da576bad61da878b59c228901236358426" + integrity sha512-2n/nrt0QD8UnxzDKKHWomYuLK9uxGxyPztA6wI24Kng8Iw7jQk/doIti63z4xPKNi4CtbumSjk1TRYUZ/bgViw== dependencies: - graphql-language-service-parser "^1.10.0" - graphql-language-service-types "^1.8.3" - graphql-language-service-utils "^2.6.0" + graphql-config "^4.1.0" + graphql-language-service-parser "^1.10.4" + graphql-language-service-types "^1.8.7" + graphql-language-service-utils "^2.7.1" vscode-languageserver-types "^3.15.1" -graphql-language-service-parser@^1.10.0: - version "1.10.0" - resolved "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.10.0.tgz#116f4be849754f6afb4c196421a43fe96d87b278" - integrity sha512-cLExv0EjqT2hsKdwVTPmKU6eMfjZAjxqywgCPnWD48eJn6tyuePMyG7ye+jpX1PRPPx/cDHfFJGf8sUclchvng== +graphql-language-service-parser@^1.10.4: + version "1.10.4" + resolved "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.10.4.tgz#b2979deefc5c0df571dacd409b2d5fbf1cdf7a9d" + integrity sha512-duDE+0aeKLFVrb9Kf28U84ZEHhHcvTjWIT6dJbIAQJWBaDoht0D4BK9EIhd94I3DtKRc1JCJb2+70y1lvP/hiA== dependencies: - graphql-language-service-types "^1.8.0" + graphql-language-service-types "^1.8.7" -graphql-language-service-types@^1.8.0: - version "1.8.1" - resolved "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.8.1.tgz#963810010924f2b5eaea415d5b8eb0b7d42c479b" - integrity sha512-IpYS0mEHEmRsFlq+loWCpSYYYizAID7Alri6GoFN1QqUdux+8rp1Tkp2NGsGDpDmm3Dbz5ojmJWzNWQGpuwveA== - -graphql-language-service-types@^1.8.2: - version "1.8.2" - resolved "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.8.2.tgz#50ae56f69cc24fcfc3daa129b68b0eb9421e8578" - integrity sha512-Sj07RHnMwAhEvAt7Jdt1l/x56ZpoNh+V6g+T58CF6GiYqI5l4vXqqRB4d4xHDcNQX98GpJfnf3o8BqPgP3C5Sw== - -graphql-language-service-types@^1.8.3: - version "1.8.3" - resolved "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.8.3.tgz#d7d688d74c122c4d9cc4cceae761a1f2a3c396a1" - integrity sha512-m+RHnlGkKDcesW/gC4M7I2pSmWJB84uWS6LtnjplO/07JN312nJCJYCwV/DBny2m1fmSOxN7H/o+JW0l56KwBA== - -graphql-language-service-utils@^2.5.3: - version "2.5.3" - resolved "https://registry.npmjs.org/graphql-language-service-utils/-/graphql-language-service-utils-2.5.3.tgz#185f4f65cf8c010871eb9405452a3a0bfdf88748" - integrity sha512-ydevEZ0AgzEKQF3hiCbLXuS0o7189Ww/T30WtCKCLaRHDYk9Yyb2PZWdhSTWLxYZTaX2TccV6NtFWvzIC7UP3g== +graphql-language-service-types@^1.8.7: + version "1.8.7" + resolved "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.8.7.tgz#f5e909e6d9334ea2d8d1f7281b695b6f5602c07f" + integrity sha512-LP/Mx0nFBshYEyD0Ny6EVGfacJAGVx+qXtlJP4hLzUdBNOGimfDNtMVIdZANBXHXcM41MDgMHTnyEx2g6/Ttbw== dependencies: - graphql-language-service-types "^1.8.0" + graphql-config "^4.1.0" + vscode-languageserver-types "^3.15.1" + +graphql-language-service-utils@^2.7.1: + version "2.7.1" + resolved "https://registry.npmjs.org/graphql-language-service-utils/-/graphql-language-service-utils-2.7.1.tgz#c97c8d744a761480aba7e03e4a42adf28b6fce39" + integrity sha512-Wci5MbrQj+6d7rfvbORrA9uDlfMysBWYaG49ST5TKylNaXYFf3ixFOa74iM1KtM9eidosUbI3E1JlWi0JaidJA== + dependencies: + "@types/json-schema" "7.0.9" + graphql-language-service-types "^1.8.7" nullthrows "^1.0.0" -graphql-language-service-utils@^2.6.0: - version "2.6.0" - resolved "https://registry.npmjs.org/graphql-language-service-utils/-/graphql-language-service-utils-2.6.0.tgz#d04904641248167ccbb381d8705ba97daa784954" - integrity sha512-idqwmbREixhDuQMcYp8WH0btQT02xZny8MO/HduNTVjnPrmTYnZUbpZ9AejdflmaKoS0o8nNvgXQ0GpIOzbG5g== +graphql-language-service@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-4.1.1.tgz#8093655c23af6a3f3eee3f92e6a5b0a3e2a21d70" + integrity sha512-7XP+XSzTnpnmh+EYbofnK/YdSaI7/mj7bS8ffP3A1UQtNBLb9mUErgYju6VegcV6Df0bJObLG+XxSBSlgV4kXQ== dependencies: - graphql-language-service-types "^1.8.3" - nullthrows "^1.0.0" - -graphql-language-service@^3.1.6: - version "3.2.0" - resolved "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-3.2.0.tgz#e0eb6d5dea2cab92549a253d7a6b4fa0cce178b7" - integrity sha512-xM5Ua5p7ttG/oEaDy2zk35FP2O2I9qD2N0DOrjCDUVDRC06FNDG+/CvF4qX9+i8DWOI65xch5vAhSQEfS2jFsA== - dependencies: - graphql-language-service-interface "^2.9.0" - graphql-language-service-parser "^1.10.0" - graphql-language-service-types "^1.8.2" - graphql-language-service-utils "^2.5.3" + graphql-language-service-interface "^2.10.1" + graphql-language-service-parser "^1.10.4" + graphql-language-service-types "^1.8.7" + graphql-language-service-utils "^2.7.1" + picomatch "^2.3.0" graphql-request@^3.3.0: version "3.4.0" @@ -16008,6 +16257,11 @@ graphql-request@^3.3.0: extract-files "^9.0.0" form-data "^3.0.0" +graphql-sse@^1.0.1: + version "1.0.6" + resolved "https://registry.npmjs.org/graphql-sse/-/graphql-sse-1.0.6.tgz#4f98e0a06f2020542ed054399116108491263224" + integrity sha512-y2mVBN2KwNrzxX2KBncQ6kzc6JWvecxuBernrl0j65hsr6MAS3+Yn8PTFSOgRmtolxugepxveyZVQEuaNEbw3w== + graphql-subscriptions@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-1.1.0.tgz#5f2fa4233eda44cf7570526adfcf3c16937aef11" @@ -16057,15 +16311,10 @@ graphql-ws@^4.4.1: resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.7.0.tgz#b323fbf35a3736eed85dac24c0054d6d10c93e62" integrity sha512-Md8SsmC9ZlsogFPd3Ot8HbIAAqsHh8Xoq7j4AmcIat1Bh6k91tjVyQvA0Au1/BolXSYq+RDvib6rATU2Hcf1Xw== -graphql-ws@^4.9.0: - version "4.9.0" - resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.9.0.tgz#5cfd8bb490b35e86583d8322f5d5d099c26e365c" - integrity sha512-sHkK9+lUm20/BGawNEWNtVAeJzhZeBg21VmvmLoT5NdGVeZWv5PdIhkcayQIAgjSyyQ17WMKmbDijIPG2On+Ag== - -graphql@15.5.0: - version "15.5.0" - resolved "https://registry.npmjs.org/graphql/-/graphql-15.5.0.tgz#39d19494dbe69d1ea719915b578bf920344a69d5" - integrity sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA== +graphql-ws@^5.4.1: + version "5.5.5" + resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.5.5.tgz#f375486d3f196e2a2527b503644693ae3a8670a9" + integrity sha512-hvyIS71vs4Tu/yUYHPvGXsTgo0t3arU820+lT5VjZS2go0ewp2LqyCgxEN56CzOG7Iys52eRhHBiD1gGRdiQtw== graphql@^15.3.0: version "15.5.1" @@ -16077,6 +16326,11 @@ graphql@^15.5.1: resolved "https://registry.npmjs.org/graphql/-/graphql-15.6.1.tgz#9125bdf057553525da251e19e96dab3d3855ddfc" integrity sha512-3i5lu0z6dRvJ48QP9kFxBkJ7h4Kso7PS8eahyTFz5Jm6CvQfLtNIE8LX9N6JLnXTuwR+sIYnXzaWp6anOg0QQw== +graphql@^16.0.0: + version "16.0.1" + resolved "https://registry.npmjs.org/graphql/-/graphql-16.0.1.tgz#93a13cd4e0e38ca8d0832e79614c8578bfd34f10" + integrity sha512-oPvCuu6dlLdiz8gZupJ47o1clgb72r1u8NDBcQYjcV6G/iEdmE11B1bBlkhXRvV0LisP/SXRFP7tT6AgaTjpzg== + grouped-queue@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/grouped-queue/-/grouped-queue-2.0.0.tgz#a2c6713f2171e45db2c300a3a9d7c119d694dac8" @@ -21373,6 +21627,13 @@ node-fetch@2.6.1, node-fetch@^2.3.0, node-fetch@^2.6.0, node-fetch@^2.6.1: resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== +node-fetch@^2.6.5: + version "2.6.6" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89" + integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA== + dependencies: + whatwg-url "^5.0.0" + node-forge@^0.10.0: version "0.10.0" resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" @@ -22908,7 +23169,7 @@ picocolors@^1.0.0: resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== @@ -26974,6 +27235,11 @@ style-loader@^3.3.1: resolved "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575" integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ== +style-mod@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz#97e7c2d68b592975f2ca7a63d0dd6fcacfe35a01" + integrity sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw== + style-to-object@0.3.0, style-to-object@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46" @@ -26994,6 +27260,17 @@ stylis@^4.0.6: resolved "https://registry.npmjs.org/stylis/-/stylis-4.0.7.tgz#412a90c28079417f3d27c028035095e4232d2904" integrity sha512-OFFeUXFgwnGOKvEXaSv0D0KQ5ADP0n6g3SVONx6I/85JzNZ3u50FRwB3lVIk1QO2HNdI75tbVzc4Z66Gdp9voA== +subscriptions-transport-ws@^0.11.0: + version "0.11.0" + resolved "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.11.0.tgz#baf88f050cba51d52afe781de5e81b3c31f89883" + integrity sha512-8D4C6DIH5tGiAIpp5I0wD/xRlNiZAPGHygzCe7VzyzUoxHtawzjNAY9SUTXU05/EY2NMY9/9GF0ycizkXr1CWQ== + dependencies: + backo2 "^1.0.2" + eventemitter3 "^3.1.0" + iterall "^1.2.1" + symbol-observable "^1.0.4" + ws "^5.2.0 || ^6.0.0 || ^7.0.0" + subscriptions-transport-ws@^0.9.18, subscriptions-transport-ws@^0.9.19: version "0.9.19" resolved "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.19.tgz#10ca32f7e291d5ee8eb728b9c02e43c52606cdcf" @@ -27244,6 +27521,14 @@ sync-fetch@0.3.0: buffer "^5.7.0" node-fetch "^2.6.1" +sync-fetch@0.3.1: + version "0.3.1" + resolved "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.3.1.tgz#62aa82c4b4d43afd6906bfd7b5f92056458509f0" + integrity sha512-xj5qiCDap/03kpci5a+qc5wSJjc8ZSixgG2EUmH1B8Ea2sfWclQA7eH40hiHPCtkCn6MCk4Wb+dqcXdCy2PP3g== + dependencies: + buffer "^5.7.0" + node-fetch "^2.6.1" + table@^6.0.9: version "6.7.1" resolved "https://registry.npmjs.org/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2" @@ -27791,6 +28076,11 @@ tr46@^2.1.0: dependencies: punycode "^2.1.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + "traverse@>=0.3.0 <0.4": version "0.3.9" resolved "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" @@ -28239,6 +28529,11 @@ underscore@^1.12.1, underscore@^1.9.1: resolved "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1" integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g== +undici@^4.9.3: + version "4.11.0" + resolved "https://registry.npmjs.org/undici/-/undici-4.11.0.tgz#41fb4f944704d77e1c9fb472d40d2dbece64ccf2" + integrity sha512-gofXRqAdm81rzaZgPbMf98qvrNGd3ptJ26+mCcF3EXoC817p//MtL8XcDpTvHUXxdW27rAM2jvTae+KyAchorw== + unfetch@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" @@ -28497,7 +28792,7 @@ universalify@^2.0.0: resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== -unixify@1.0.0: +unixify@1.0.0, unixify@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unixify/-/unixify-1.0.0.tgz#3a641c8c2ffbce4da683a5c70f03a462940c2090" integrity sha1-OmQcjC/7zk2mg6XHDwOkYpQMIJA= @@ -28844,6 +29139,11 @@ validator@^8.0.0: resolved "https://registry.npmjs.org/validator/-/validator-8.2.0.tgz#3c1237290e37092355344fef78c231249dab77b9" integrity sha512-Yw5wW34fSv5spzTXNkokD6S6/Oq92d8q/t14TqsS3fAiA1RYnxSFSIZ+CY3n6PGGRCq5HhJTSepQvFUS2QUDxA== +value-or-promise@1.0.11: + version "1.0.11" + resolved "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.11.tgz#3e90299af31dd014fe843fe309cefa7c1d94b140" + integrity sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg== + value-or-promise@1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.6.tgz#218aa4794aa2ee24dcf48a29aba4413ed584747f" @@ -28957,6 +29257,11 @@ w3c-hr-time@^1.0.2: dependencies: browser-process-hrtime "^1.0.0" +w3c-keyname@^2.2.4: + version "2.2.4" + resolved "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz#4ade6916f6290224cdbd1db8ac49eab03d0eef6b" + integrity sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw== + w3c-xmlserializer@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a" @@ -29049,6 +29354,11 @@ web-streams-polyfill@4.0.0-beta.1: resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.1.tgz#3b19b9817374b7cee06d374ba7eeb3aeb80e8c95" integrity sha512-3ux37gEX670UUphBF9AMCq8XM6iQ8Ac6A+DSRRjDoRBm1ufCkaCDdNVbaqq60PsEkdNlLKrGtv/YBP4EJXqNtQ== +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" @@ -29264,6 +29574,14 @@ whatwg-mimetype@^2.3.0: resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + whatwg-url@^8.0.0, whatwg-url@^8.4.0: version "8.4.0" resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz#50fb9615b05469591d2b2bd6dfaed2942ed72837" @@ -29519,6 +29837,11 @@ ws@7.4.6: resolved "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@8.3.0: + version "8.3.0" + resolved "https://registry.npmjs.org/ws/-/ws-8.3.0.tgz#7185e252c8973a60d57170175ff55fdbd116070d" + integrity sha512-Gs5EZtpqZzLvmIM59w4igITU57lrtYVFneaa434VROv4thzJyV6UjIL3D42lslWlI+D4KzLYnxSwtfuiO79sNw== + "ws@^5.2.0 || ^6.0.0 || ^7.0.0", ws@^7.2.3, ws@^7.3.1: version "7.5.0" resolved "https://registry.npmjs.org/ws/-/ws-7.5.0.tgz#0033bafea031fb9df041b2026fc72a571ca44691" From 7ce4ea40795dfe834ed8f27d33e9b03d297c5427 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 7 Dec 2021 14:14:15 +0100 Subject: [PATCH 053/116] chore: release new version of `api-docs` and `graphiql` to solve issues with `graphql` versions Signed-off-by: blam --- .changeset/chatty-ligers-provide.md | 6 ------ .changeset/rotten-candles-poke.md | 5 ----- plugins/api-docs/CHANGELOG.md | 7 +++++++ plugins/api-docs/package.json | 2 +- plugins/graphiql/CHANGELOG.md | 6 ++++++ plugins/graphiql/package.json | 2 +- 6 files changed, 15 insertions(+), 13 deletions(-) delete mode 100644 .changeset/chatty-ligers-provide.md delete mode 100644 .changeset/rotten-candles-poke.md diff --git a/.changeset/chatty-ligers-provide.md b/.changeset/chatty-ligers-provide.md deleted file mode 100644 index 2d0c02fbae..0000000000 --- a/.changeset/chatty-ligers-provide.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-api-docs': patch -'@backstage/plugin-graphiql': patch ---- - -chore(dependencies): bump `graphiql` package to latest diff --git a/.changeset/rotten-candles-poke.md b/.changeset/rotten-candles-poke.md deleted file mode 100644 index b8a5b7138b..0000000000 --- a/.changeset/rotten-candles-poke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-api-docs': patch ---- - -Update AsyncAPI component to 1.0.0-x releases diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 74b7ae8ab9..5eeaf14cb6 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-api-docs +## 0.6.17 + +### Patch Changes + +- dde1681f33: chore(dependencies): bump `graphiql` package to latest +- ef64a444ca: Update AsyncAPI component to 1.0.0-x releases + ## 0.6.16 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index a03fca6757..984b534850 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.6.16", + "version": "0.6.17", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/graphiql/CHANGELOG.md b/plugins/graphiql/CHANGELOG.md index 4fbf6b0dab..571024eaec 100644 --- a/plugins/graphiql/CHANGELOG.md +++ b/plugins/graphiql/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-graphiql +## 0.2.23 + +### Patch Changes + +- dde1681f33: chore(dependencies): bump `graphiql` package to latest + ## 0.2.22 ### Patch Changes diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 0e064fb1ad..a3aaf7fa69 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.22", + "version": "0.2.23", "private": false, "publishConfig": { "access": "public", From 3421826ca872163b57a5cf6fd2f6e80b01e2227c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Can=20Bilgi=C3=A7?= Date: Tue, 7 Dec 2021 18:29:06 +0300 Subject: [PATCH 054/116] [TechDocs] Set entity triplets to lowercase if config is set on docs tab at entity page (#8394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixes #8381, set entity triplets to lowercase if config is set on docs tab at entity page Co-authored-by: Mehmet Mallı Signed-off-by: mertcbilgic * Changeset added Co-authored-by: Mehmet Mallı Signed-off-by: mertcbilgic * fixes due to review Co-authored-by: Mehmet Mallı Co-authored-by: Güven Altunsoy Co-authored-by: Nilgün Canbaz Co-authored-by: Burcu Karagöz Co-authored-by: Murat Sökücü Signed-off-by: mertcbilgic Co-authored-by: Mehmet Mallı Co-authored-by: Güven Altunsoy Co-authored-by: Nilgün Canbaz Co-authored-by: Burcu Karagöz Co-authored-by: Murat Sökücü --- .changeset/techdocs-blue-vans-care.md | 5 ++++ plugins/techdocs/src/EntityPageDocs.tsx | 9 ++++--- plugins/techdocs/src/helpers.ts | 26 +++++++++++++++++++ .../src/home/components/DocsCardGrid.tsx | 17 +++++------- .../src/home/components/DocsTable.tsx | 22 +++++++--------- 5 files changed, 52 insertions(+), 27 deletions(-) create mode 100644 .changeset/techdocs-blue-vans-care.md create mode 100644 plugins/techdocs/src/helpers.ts diff --git a/.changeset/techdocs-blue-vans-care.md b/.changeset/techdocs-blue-vans-care.md new file mode 100644 index 0000000000..a648a7aea1 --- /dev/null +++ b/.changeset/techdocs-blue-vans-care.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +The problem of lowercase entity triplets which causes docs to not load on entity page is fixed. diff --git a/plugins/techdocs/src/EntityPageDocs.tsx b/plugins/techdocs/src/EntityPageDocs.tsx index c5356289f0..b10fba6d2e 100644 --- a/plugins/techdocs/src/EntityPageDocs.tsx +++ b/plugins/techdocs/src/EntityPageDocs.tsx @@ -17,15 +17,18 @@ import React from 'react'; import { Entity } from '@backstage/catalog-model'; import { Reader } from './reader'; +import { toLowerMaybe } from './helpers'; +import { configApiRef, useApi } from '@backstage/core-plugin-api'; export const EntityPageDocs = ({ entity }: { entity: Entity }) => { + const config = useApi(configApiRef); return ( ); diff --git a/plugins/techdocs/src/helpers.ts b/plugins/techdocs/src/helpers.ts new file mode 100644 index 0000000000..7ff4dec6e7 --- /dev/null +++ b/plugins/techdocs/src/helpers.ts @@ -0,0 +1,26 @@ +/* + * 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 { Config } from '@backstage/config'; + +// Lower-case entity triplets by default, but allow override. +export function toLowerMaybe(str: string, config: Config) { + return config.getOptionalBoolean( + 'techdocs.legacyUseCaseSensitiveTripletPaths', + ) + ? str + : str.toLocaleLowerCase('en-US'); +} diff --git a/plugins/techdocs/src/home/components/DocsCardGrid.tsx b/plugins/techdocs/src/home/components/DocsCardGrid.tsx index 77e69995ae..1aef793bd2 100644 --- a/plugins/techdocs/src/home/components/DocsCardGrid.tsx +++ b/plugins/techdocs/src/home/components/DocsCardGrid.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { Entity } from '@backstage/catalog-model'; -import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { useApi, useRouteRef, configApiRef } from '@backstage/core-plugin-api'; import { Card, CardActions, CardContent, CardMedia } from '@material-ui/core'; import { rootDocsRouteRef } from '../../routes'; @@ -26,6 +26,7 @@ import { ItemCardGrid, ItemCardHeader, } from '@backstage/core-components'; +import { toLowerMaybe } from '../../helpers'; export const DocsCardGrid = ({ entities, @@ -33,14 +34,7 @@ export const DocsCardGrid = ({ entities: Entity[] | undefined; }) => { const getRouteToReaderPageFor = useRouteRef(rootDocsRouteRef); - - // Lower-case entity triplets by default, but allow override. - const toLowerMaybe = useApi(configApiRef).getOptionalBoolean( - 'techdocs.legacyUseCaseSensitiveTripletPaths', - ) - ? (str: string) => str - : (str: string) => str.toLocaleLowerCase('en-US'); - + const config = useApi(configApiRef); if (!entities) return null; return ( @@ -59,9 +53,10 @@ export const DocsCardGrid = ({ to={getRouteToReaderPageFor({ namespace: toLowerMaybe( entity.metadata.namespace ?? 'default', + config, ), - kind: toLowerMaybe(entity.kind), - name: toLowerMaybe(entity.metadata.name), + kind: toLowerMaybe(entity.kind, config), + name: toLowerMaybe(entity.metadata.name, config), })} color="primary" data-testid="read_docs" diff --git a/plugins/techdocs/src/home/components/DocsTable.tsx b/plugins/techdocs/src/home/components/DocsTable.tsx index 0e64c2e2e8..83d6f05788 100644 --- a/plugins/techdocs/src/home/components/DocsTable.tsx +++ b/plugins/techdocs/src/home/components/DocsTable.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { useCopyToClipboard } from 'react-use'; -import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { useRouteRef, useApi, configApiRef } from '@backstage/core-plugin-api'; import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; import { formatEntityRefTitle, @@ -34,6 +34,7 @@ import { import * as actionFactories from './actions'; import * as columnFactories from './columns'; import { DocsTableRow } from './types'; +import { toLowerMaybe } from '../../helpers'; export const DocsTable = ({ entities, @@ -50,26 +51,21 @@ export const DocsTable = ({ }) => { const [, copyToClipboard] = useCopyToClipboard(); const getRouteToReaderPageFor = useRouteRef(rootDocsRouteRef); - - // Lower-case entity triplets by default, but allow override. - const toLowerMaybe = useApi(configApiRef).getOptionalBoolean( - 'techdocs.legacyUseCaseSensitiveTripletPaths', - ) - ? (str: string) => str - : (str: string) => str.toLocaleLowerCase('en-US'); - + const config = useApi(configApiRef); if (!entities) return null; const documents = entities.map(entity => { const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); - return { entity, resolved: { docsUrl: getRouteToReaderPageFor({ - namespace: toLowerMaybe(entity.metadata.namespace ?? 'default'), - kind: toLowerMaybe(entity.kind), - name: toLowerMaybe(entity.metadata.name), + namespace: toLowerMaybe( + entity.metadata.namespace ?? 'default', + config, + ), + kind: toLowerMaybe(entity.kind, config), + name: toLowerMaybe(entity.metadata.name, config), }), ownedByRelations, ownedByRelationsTitle: ownedByRelations From af80fb7a013c32c99070a5ed77dd8f17bbbadad7 Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Wed, 1 Dec 2021 19:58:52 +0000 Subject: [PATCH 055/116] test: add GitLabClient coverage in catalog-backend Signed-off-by: Minn Soe --- .../processors/gitlab/client.test.ts | 238 ++++++++++++++++++ .../src/ingestion/processors/gitlab/client.ts | 26 +- 2 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts new file mode 100644 index 0000000000..e2c919ae9b --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts @@ -0,0 +1,238 @@ +/* + * 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 { 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(); +setupRequestMockHandlers(server); + +const MOCK_CONFIG = readGitLabIntegrationConfig( + new ConfigReader({ + host: 'example.com', + token: 'test-token', + apiBaseUrl: 'https://example.com/api/v4', + }), +); + +const FAKE_PAGED_ENDPOINT = `${MOCK_CONFIG.apiBaseUrl}/some-endpoint`; + +function setupFakeFourPageEndpoint(srv: SetupServerApi, endpoint: string) { + srv.use( + rest.get(endpoint, (req, res, ctx) => { + const page = req.url.searchParams.get('page'); + const currentPage = page ? Number(page) : 1; + const fakePageCount = 4; + + return res( + ctx.set({ + 'x-next-page': + currentPage < fakePageCount ? String(currentPage + 1) : '', + }), + ctx.json([{ someContentOfPage: currentPage }]), + ); + }), + ); +} + +function setupFakeGroupProjectsEndpoint( + srv: SetupServerApi, + apiBaseUrl: string, + groupID: string, +) { + srv.use( + rest.get(`${apiBaseUrl}/groups/${groupID}/projects`, (_, res, ctx) => { + return res( + ctx.set('x-next-page', ''), + ctx.json([ + { + id: 1, + description: 'Project One Description', + name: 'Project One', + path: 'project-one', + }, + ]), + ); + }), + ); +} + +function setupFakeInstanceProjectsEndpoint( + srv: SetupServerApi, + apiBaseUrl: string, +) { + srv.use( + rest.get(`${apiBaseUrl}/projects`, (_, res, ctx) => { + return res( + ctx.set('x-next-page', ''), + ctx.json([ + { + id: 1, + description: 'Project One Description', + name: 'Project One', + path: 'project-one', + }, + { + id: 2, + description: 'Project Two Description', + name: 'Project Two', + path: 'project-two', + }, + ]), + ); + }), + ); +} + +describe('GitLabClient', () => { + describe('pagedRequest', () => { + beforeEach(() => { + // setup fake paginated endpoint with 4 pages each returning one item + setupFakeFourPageEndpoint(server, FAKE_PAGED_ENDPOINT); + }); + + it('should provide immediate items within the page', async () => { + const client = new GitLabClient({ + config: MOCK_CONFIG, + logger: getVoidLogger(), + }); + + const { items } = await client.pagedRequest(FAKE_PAGED_ENDPOINT); + // fake page contains exactly one item + expect(items).toHaveLength(1); + }); + + it('should request items for a given page number', async () => { + const client = new GitLabClient({ + config: MOCK_CONFIG, + logger: getVoidLogger(), + }); + + const requestedPage = 2; + const { items, nextPage } = await client.pagedRequest( + FAKE_PAGED_ENDPOINT, + { + page: requestedPage, + }, + ); + // should contain an item from a given page + expect(items[0].someContentOfPage).toEqual(requestedPage); + // should set the nextPage property to the next page + expect(nextPage).toEqual(3); + }); + + it('should not have a next page if at the end', async () => { + const client = new GitLabClient({ + config: MOCK_CONFIG, + logger: getVoidLogger(), + }); + + const { items, nextPage } = await client.pagedRequest( + FAKE_PAGED_ENDPOINT, + { + page: 4, + }, + ); + // should contain item of last page + expect(items).toHaveLength(1); + expect(nextPage).toBeNull(); + }); + + it('should throw if response is not okay', async () => { + const endpoint = `${MOCK_CONFIG.apiBaseUrl}/unhealthy-endpoint`; + server.use( + rest.get(endpoint, (_, res, ctx) => { + return res(ctx.status(400), ctx.json({ error: 'some error' })); + }), + ); + + const client = new GitLabClient({ + config: MOCK_CONFIG, + logger: getVoidLogger(), + }); + // non-200 status code should throw + await expect(() => client.pagedRequest(endpoint)).rejects.toThrowError(); + }); + }); + + describe('listProjects', () => { + it('should get projects for a given group', async () => { + setupFakeGroupProjectsEndpoint( + server, + MOCK_CONFIG.apiBaseUrl, + 'test-group', + ); + const client = new GitLabClient({ + config: MOCK_CONFIG, + logger: getVoidLogger(), + }); + + const groupProjectsGen = paginated( + options => client.listProjects(options), + { group: 'test-group' }, + ); + const allItems = []; + for await (const item of groupProjectsGen) { + allItems.push(item); + } + expect(allItems).toHaveLength(1); + }); + + it('should get all projects for an instance', async () => { + setupFakeInstanceProjectsEndpoint(server, MOCK_CONFIG.apiBaseUrl); + const client = new GitLabClient({ + config: MOCK_CONFIG, + logger: getVoidLogger(), + }); + + const instanceProjectsGen = paginated( + options => client.listProjects(options), + {}, + ); + const allItems = []; + for await (const item of instanceProjectsGen) { + allItems.push(item); + } + expect(allItems).toHaveLength(2); + }); + }); +}); + +describe('paginated', () => { + it('should iterate through the pages until exhausted', async () => { + setupFakeFourPageEndpoint(server, FAKE_PAGED_ENDPOINT); + const client = new GitLabClient({ + config: MOCK_CONFIG, + logger: getVoidLogger(), + }); + + const paginatedAsyncGenerator = paginated( + options => client.pagedRequest(FAKE_PAGED_ENDPOINT, options), + {}, + ); + const allItems = []; + for await (const item of paginatedAsyncGenerator) { + allItems.push(item); + } + + expect(allItems).toHaveLength(4); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts index d1765521a3..75832883e4 100644 --- a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts +++ b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts @@ -46,7 +46,19 @@ export class GitLabClient { return this.pagedRequest(`${this.config.apiBaseUrl}/projects`, options); } - private async pagedRequest( + /** + * Performs a request against a given paginated GitLab endpoint. + * + * This method may be used to perform authenticated REST calls against any + * paginated GitLab endpoint which uses X-NEXT-PAGE headers. The return value + * can be be used with the {@link paginated} async-generator function to yield + * each item from the paged request. + * + * @see {@link paginated} + * @param endpoint - The complete request URL for the endpoint. + * @param options - Request queryString options which may also include page variables. + */ + async pagedRequest( endpoint: string, options?: ListOptions, ): Promise> { @@ -92,6 +104,18 @@ export type PagedResponse = { nextPage?: number; }; +/** + * Advances through each page and provides each item from a paginated request. + * + * The async generator function yields each item from repeated calls to the + * provided request function. The generator walks through each available page by + * setting the page key in the options passed into the request function and + * making repeated calls until there are no more pages. + * + * @see {@link pagedRequest} + * @param request - Function which returns a PagedResponse to walk through. + * @param options - Initial ListOptions for the request function. + */ export async function* paginated( request: (options: ListOptions) => Promise>, options: ListOptions, From d58e92a7f064a8a0c76eaf98fd35d727975cfd40 Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Tue, 7 Dec 2021 13:46:12 +0000 Subject: [PATCH 056/116] feat: allow types in gitlab client paginated reqs Signed-off-by: Minn Soe --- .../src/ingestion/processors/gitlab/client.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts index 75832883e4..fbaa816d92 100644 --- a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts +++ b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts @@ -58,10 +58,10 @@ export class GitLabClient { * @param endpoint - The complete request URL for the endpoint. * @param options - Request queryString options which may also include page variables. */ - async pagedRequest( + async pagedRequest( endpoint: string, options?: ListOptions, - ): Promise> { + ): Promise> { const request = new URL(endpoint); for (const key in options) { if (options[key]) { @@ -116,8 +116,8 @@ export type PagedResponse = { * @param request - Function which returns a PagedResponse to walk through. * @param options - Initial ListOptions for the request function. */ -export async function* paginated( - request: (options: ListOptions) => Promise>, +export async function* paginated( + request: (options: ListOptions) => Promise>, options: ListOptions, ) { let res; From 91b3ad3553a43576a352d7c9598d834845736eb5 Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Fri, 3 Dec 2021 13:32:01 +0000 Subject: [PATCH 057/116] refactor: simplify by using GitLab apiBaseUrl Signed-off-by: Minn Soe --- .../processors/gitlab/client.test.ts | 40 ++++++++++--------- .../src/ingestion/processors/gitlab/client.ts | 34 ++++++++-------- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts index e2c919ae9b..92ee65e5e7 100644 --- a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts @@ -32,21 +32,22 @@ const MOCK_CONFIG = readGitLabIntegrationConfig( apiBaseUrl: 'https://example.com/api/v4', }), ); +const FAKE_PAGED_ENDPOINT = `/some-endpoint`; +const FAKE_PAGED_URL = `${MOCK_CONFIG.apiBaseUrl}${FAKE_PAGED_ENDPOINT}`; -const FAKE_PAGED_ENDPOINT = `${MOCK_CONFIG.apiBaseUrl}/some-endpoint`; - -function setupFakeFourPageEndpoint(srv: SetupServerApi, endpoint: string) { +function setupFakeFourPageURL(srv: SetupServerApi, url: string) { srv.use( - rest.get(endpoint, (req, res, ctx) => { + rest.get(url, (req, res, ctx) => { const page = req.url.searchParams.get('page'); const currentPage = page ? Number(page) : 1; const fakePageCount = 4; return res( - ctx.set({ - 'x-next-page': - currentPage < fakePageCount ? String(currentPage + 1) : '', - }), + // set next page number header if page requested is less than count + ctx.set( + 'x-next-page', + currentPage < fakePageCount ? String(currentPage + 1) : '', + ), ctx.json([{ someContentOfPage: currentPage }]), ); }), @@ -106,7 +107,7 @@ describe('GitLabClient', () => { describe('pagedRequest', () => { beforeEach(() => { // setup fake paginated endpoint with 4 pages each returning one item - setupFakeFourPageEndpoint(server, FAKE_PAGED_ENDPOINT); + setupFakeFourPageURL(server, FAKE_PAGED_URL); }); it('should provide immediate items within the page', async () => { @@ -157,9 +158,10 @@ describe('GitLabClient', () => { }); it('should throw if response is not okay', async () => { - const endpoint = `${MOCK_CONFIG.apiBaseUrl}/unhealthy-endpoint`; + const endpoint = '/unhealthy-endpoint'; + const url = `${MOCK_CONFIG.apiBaseUrl}${endpoint}`; server.use( - rest.get(endpoint, (_, res, ctx) => { + rest.get(url, (_, res, ctx) => { return res(ctx.status(400), ctx.json({ error: 'some error' })); }), ); @@ -203,33 +205,33 @@ describe('GitLabClient', () => { logger: getVoidLogger(), }); - const instanceProjectsGen = paginated( + const instanceProjects = paginated( options => client.listProjects(options), {}, ); - const allItems = []; - for await (const item of instanceProjectsGen) { - allItems.push(item); + const allProjects = []; + for await (const project of instanceProjects) { + allProjects.push(project); } - expect(allItems).toHaveLength(2); + expect(allProjects).toHaveLength(2); }); }); }); describe('paginated', () => { it('should iterate through the pages until exhausted', async () => { - setupFakeFourPageEndpoint(server, FAKE_PAGED_ENDPOINT); + setupFakeFourPageURL(server, FAKE_PAGED_URL); const client = new GitLabClient({ config: MOCK_CONFIG, logger: getVoidLogger(), }); - const paginatedAsyncGenerator = paginated( + const paginatedItems = paginated( options => client.pagedRequest(FAKE_PAGED_ENDPOINT, options), {}, ); const allItems = []; - for await (const item of paginatedAsyncGenerator) { + for await (const item of paginatedItems) { allItems.push(item); } diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts index fbaa816d92..1df5ce962f 100644 --- a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts +++ b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts @@ -21,6 +21,18 @@ import { } from '@backstage/integration'; import { Logger } from 'winston'; +export type ListOptions = { + [key: string]: string | number | boolean | undefined; + group?: string; + per_page?: number | undefined; + page?: number | undefined; +}; + +export type PagedResponse = { + items: T[]; + nextPage?: number; +}; + export class GitLabClient { private readonly config: GitLabIntegrationConfig; private readonly logger: Logger; @@ -33,9 +45,7 @@ export class GitLabClient { async listProjects(options?: ListOptions): Promise> { if (options?.group) { return this.pagedRequest( - `${this.config.apiBaseUrl}/groups/${encodeURIComponent( - options?.group, - )}/projects`, + `/groups/${encodeURIComponent(options?.group)}/projects`, { ...options, include_subgroups: true, @@ -43,7 +53,7 @@ export class GitLabClient { ); } - return this.pagedRequest(`${this.config.apiBaseUrl}/projects`, options); + return this.pagedRequest(`/projects`, options); } /** @@ -55,14 +65,14 @@ export class GitLabClient { * each item from the paged request. * * @see {@link paginated} - * @param endpoint - The complete request URL for the endpoint. + * @param endpoint - The request endpoint, e.g. /projects. * @param options - Request queryString options which may also include page variables. */ async pagedRequest( endpoint: string, options?: ListOptions, ): Promise> { - const request = new URL(endpoint); + const request = new URL(`${this.config.apiBaseUrl}${endpoint}`); for (const key in options) { if (options[key]) { request.searchParams.append(key, options[key]!.toString()); @@ -92,18 +102,6 @@ export class GitLabClient { } } -export type ListOptions = { - [key: string]: string | number | boolean | undefined; - group?: string; - per_page?: number | undefined; - page?: number | undefined; -}; - -export type PagedResponse = { - items: T[]; - nextPage?: number; -}; - /** * Advances through each page and provides each item from a paginated request. * From b61a5bbdf320ac2cb02506b55566f0704056f1fb Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Tue, 7 Dec 2021 13:36:28 +0000 Subject: [PATCH 058/116] feat: add gitlab self managed check method Signed-off-by: Minn Soe --- .../processors/gitlab/client.test.ts | 28 +++++++++++++++++++ .../src/ingestion/processors/gitlab/client.ts | 7 +++++ 2 files changed, 35 insertions(+) diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts index 92ee65e5e7..b249137740 100644 --- a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.test.ts @@ -104,6 +104,34 @@ function setupFakeInstanceProjectsEndpoint( } describe('GitLabClient', () => { + describe('isSelfManaged', () => { + it('returns true if self managed instance', () => { + const client = new GitLabClient({ + config: readGitLabIntegrationConfig( + new ConfigReader({ + host: 'example.com', + token: 'test-token', + apiBaseUrl: 'https://example.com/api/v4', + }), + ), + logger: getVoidLogger(), + }); + expect(client.isSelfManaged()).toBeTruthy(); + }); + it('returns false if gitlab.com', () => { + const client = new GitLabClient({ + config: readGitLabIntegrationConfig( + new ConfigReader({ + host: 'gitlab.com', + token: 'test-token', + }), + ), + logger: getVoidLogger(), + }); + expect(client.isSelfManaged()).toBeFalsy(); + }); + }); + describe('pagedRequest', () => { beforeEach(() => { // setup fake paginated endpoint with 4 pages each returning one item diff --git a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts index 1df5ce962f..4d42f5e068 100644 --- a/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts +++ b/plugins/catalog-backend/src/ingestion/processors/gitlab/client.ts @@ -42,6 +42,13 @@ export class GitLabClient { this.logger = options.logger; } + /** + * Indicates whether the client is for a SaaS or self managed GitLab instance. + */ + isSelfManaged(): boolean { + return this.config.host !== 'gitlab.com'; + } + async listProjects(options?: ListOptions): Promise> { if (options?.group) { return this.pagedRequest( From be32bc3edbe758dd864f829142023904ba65944a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Dec 2021 13:57:02 +0100 Subject: [PATCH 059/116] package,plugins: move react to peer deps and include v17 Signed-off-by: Patrik Oldsberg --- packages/app-defaults/package.json | 4 +++- packages/core-app-api/package.json | 4 +++- packages/core-components/package.json | 6 ++++-- packages/core-plugin-api/package.json | 4 +++- packages/dev-utils/package.json | 6 ++++-- packages/integration-react/package.json | 5 +++-- packages/test-utils/package.json | 5 +++-- packages/version-bridge/package.json | 6 ++++-- plugins/allure/package.json | 5 +++-- plugins/analytics-module-ga/package.json | 5 +++-- plugins/api-docs/package.json | 5 +++-- plugins/azure-devops/package.json | 5 +++-- plugins/badges/package.json | 5 +++-- plugins/bazaar/package.json | 5 +++-- plugins/bitrise/package.json | 5 +++-- plugins/catalog-graph/package.json | 5 +++-- plugins/catalog-import/package.json | 5 +++-- plugins/catalog-react/package.json | 4 +++- plugins/catalog/package.json | 5 +++-- plugins/circleci/package.json | 5 +++-- plugins/cloudbuild/package.json | 5 +++-- plugins/code-coverage/package.json | 5 +++-- plugins/config-schema/package.json | 5 +++-- plugins/cost-insights/package.json | 5 +++-- plugins/explore/package.json | 5 +++-- plugins/firehydrant/package.json | 5 +++-- plugins/fossa/package.json | 5 +++-- plugins/gcp-projects/package.json | 5 +++-- plugins/git-release-manager/package.json | 5 +++-- plugins/github-actions/package.json | 5 +++-- plugins/github-deployments/package.json | 5 +++-- plugins/gitops-profiles/package.json | 5 +++-- plugins/graphiql/package.json | 5 +++-- plugins/home/package.json | 5 +++-- plugins/ilert/package.json | 5 +++-- plugins/jenkins/package.json | 5 +++-- plugins/kafka/package.json | 5 +++-- plugins/kubernetes/package.json | 5 +++-- plugins/lighthouse/package.json | 5 +++-- plugins/newrelic/package.json | 5 +++-- plugins/org/package.json | 5 +++-- plugins/pagerduty/package.json | 5 +++-- plugins/permission-react/package.json | 4 +++- plugins/rollbar/package.json | 5 +++-- plugins/scaffolder/package.json | 5 +++-- plugins/search/package.json | 5 +++-- plugins/sentry/package.json | 5 +++-- plugins/shortcuts/package.json | 5 +++-- plugins/sonarqube/package.json | 5 +++-- plugins/splunk-on-call/package.json | 5 +++-- plugins/tech-insights/package.json | 5 +++-- plugins/tech-radar/package.json | 5 +++-- plugins/techdocs/package.json | 6 ++++-- plugins/todo/package.json | 5 +++-- plugins/user-settings/package.json | 5 +++-- plugins/xcmetrics/package.json | 5 +++-- 56 files changed, 172 insertions(+), 107 deletions(-) diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 695d457455..6a324397a8 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -35,9 +35,11 @@ "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", - "react": "^16.12.0", "react-router-dom": "6.0.0-beta.0" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/test-utils": "^0.1.22", diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 9ab3e097d1..cbf8acb219 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -41,11 +41,13 @@ "@types/react": "*", "@types/prop-types": "^15.7.3", "prop-types": "^15.7.2", - "react": "^16.12.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4", "zen-observable": "^0.8.15" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/test-utils": "^0.1.23", diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 80bfe64629..804b4f03a4 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -53,8 +53,6 @@ "prop-types": "^15.7.2", "qs": "^6.9.4", "rc-progress": "^3.0.0", - "react": "^16.12.0", - "react-dom": "^16.12.0", "react-helmet": "6.1.0", "react-hook-form": "^7.12.2", "react-markdown": "^7.0.1", @@ -67,6 +65,10 @@ "remark-gfm": "^2.0.0", "zen-observable": "^0.8.15" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0", + "react-dom": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/core-app-api": "^0.1.24", "@backstage/cli": "^0.10.0", diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 3e07207c9d..462c8e45d7 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -37,11 +37,13 @@ "@types/react": "*", "history": "^5.0.0", "prop-types": "^15.7.2", - "react": "^16.12.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4", "zen-observable": "^0.8.15" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 110d774777..1b1dd66929 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -44,14 +44,16 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/react": "*", - "react": "^16.12.0", "react-use": "^17.2.4", - "react-dom": "^16.12.0", "react-hot-loader": "^4.12.21", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "zen-observable": "^0.8.15" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0", + "react-dom": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@types/jest": "^26.0.7", diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index be38d92b64..9dcd0fdb2e 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -29,10 +29,11 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/dev-utils": "^0.2.13", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 87c5b346f6..7b14a33805 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -39,12 +39,13 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/react": "*", - "react": "^16.12.0", - "react-dom": "^16.12.0", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "zen-observable": "^0.8.15" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@types/jest": "^26.0.7", diff --git a/packages/version-bridge/package.json b/packages/version-bridge/package.json index 89baaea2f5..45aa99efe8 100644 --- a/packages/version-bridge/package.json +++ b/packages/version-bridge/package.json @@ -29,8 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@types/react": "*", - "react": "^16.12.0" + "@types/react": "*" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { "@backstage/cli": "^0.10.0", diff --git a/plugins/allure/package.json b/plugins/allure/package.json index 5c795e919f..f4e8ba1c1d 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -30,11 +30,12 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index e3a3c74111..8e36c7225e 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -28,11 +28,12 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-ga": "^3.3.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 984b534850..bafdd40b06 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -44,14 +44,15 @@ "graphiql": "^1.5.12", "graphql": "^16.0.0", "isomorphic-form-data": "^2.0.0", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4", "swagger-client": "3.16.1", "swagger-ui-react": "^4.0.0-rc.3" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index 29f133ab5b..05c64c9b76 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -39,11 +39,12 @@ "@material-ui/lab": "4.0.0-alpha.57", "humanize-duration": "^3.27.0", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 7c478c0a26..a75f3c3b85 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -36,11 +36,12 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index 2defba5cca..9cefc6c2dd 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -34,12 +34,13 @@ "@material-ui/pickers": "^3.3.10", "@testing-library/jest-dom": "^5.10.1", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-hook-form": "^7.13.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/dev-utils": "^0.2.13", diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index b036610f0a..71af8abf83 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -33,11 +33,12 @@ "lodash": "^4.17.21", "luxon": "^2.0.2", "qs": "^6.9.6", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4", "recharts": "^1.8.5" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 2a44593a4a..e6cdaa0b02 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -35,11 +35,12 @@ "lodash": "^4.17.15", "p-limit": "^3.1.0", "qs": "^6.9.4", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index b370c9fa33..7b41d63592 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -47,13 +47,14 @@ "git-url-parse": "^11.6.0", "js-base64": "^3.6.0", "lodash": "^4.17.21", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-hook-form": "^7.12.2", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4", "yaml": "^1.10.0" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index c65f9144e5..d7a3bb16a4 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -45,11 +45,13 @@ "jwt-decode": "^3.1.0", "lodash": "^4.17.21", "qs": "^6.9.4", - "react": "^16.13.1", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4", "zen-observable": "^0.8.15" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/test-utils": "^0.1.22", diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 6caadb7b42..2872270c13 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -45,12 +45,13 @@ "@types/react": "*", "history": "^5.0.0", "lodash": "^4.17.21", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-helmet": "6.1.0", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 45579ab82b..9d94d61dcd 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -44,13 +44,14 @@ "humanize-duration": "^3.27.0", "lodash": "^4.17.21", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-lazylog": "^4.5.2", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 3c9e14c9e7..6fab2b8d35 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -41,13 +41,14 @@ "@material-ui/lab": "4.0.0-alpha.57", "luxon": "^2.0.2", "qs": "^6.9.4", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-lazylog": "^4.5.3", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index 907a0abcd7..d1a7d019bb 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -34,13 +34,14 @@ "@material-ui/styles": "^4.11.0", "highlight.js": "^10.6.0", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4", "recharts": "^1.8.5" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index 6b79c5493c..be9fb7fb12 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -31,11 +31,12 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "jsonschema": "^1.2.6", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4", "zen-observable": "^0.8.15" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 6a2d582d63..eb2e45b2c4 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -46,14 +46,15 @@ "luxon": "^2.0.2", "pluralize": "^8.0.0", "qs": "^6.9.4", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4", "recharts": "^1.8.5", "regression": "^2.0.1", "yup": "^0.32.9" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/explore/package.json b/plugins/explore/package.json index b4e08a526f..98d5d3644a 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -42,12 +42,13 @@ "@material-ui/lab": "4.0.0-alpha.57", "@types/react": "*", "classnames": "^2.2.6", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index 2b3bf05b1e..e6cb9e8375 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -30,10 +30,11 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "luxon": "^1.27.0", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index 7d56fdc333..9c79b14685 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -44,10 +44,11 @@ "cross-fetch": "^3.0.6", "luxon": "^2.0.2", "p-limit": "^3.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index a3a53b317e..17d43de3d2 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -37,11 +37,12 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router-dom": "^6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index c5ef82ac16..2082aeaf0f 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -32,12 +32,13 @@ "@types/react": "*", "luxon": "^2.0.2", "qs": "^6.10.1", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4", "recharts": "^1.8.5" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index b0fa469a50..0ea8bbe9f2 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -44,13 +44,14 @@ "@material-ui/lab": "4.0.0-alpha.57", "@octokit/rest": "^18.5.3", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-lazylog": "^4.5.3", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index a1dad4ecca..4836568a1f 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -34,10 +34,11 @@ "@material-ui/lab": "4.0.0-alpha.57", "@octokit/graphql": "^4.5.8", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 6b211f59ed..5cc63e43df 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -38,11 +38,12 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index a3aaf7fa69..06dd85aeee 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -39,10 +39,11 @@ "@material-ui/lab": "4.0.0-alpha.57", "graphiql": "^1.5.12", "graphql": "^16.0.0", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/home/package.json b/plugins/home/package.json index 97ec5fdfc2..81fead15c0 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -29,11 +29,12 @@ "@material-ui/lab": "4.0.0-alpha.57", "@types/react": "*", "lodash": "^4.17.21", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 2e477b2f06..4d7cfd9bfb 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -34,10 +34,11 @@ "@material-ui/pickers": "^3.3.10", "humanize-duration": "^3.26.0", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index e879d13a88..cc23fe1265 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -41,12 +41,13 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 41d677aece..0e0bff24dc 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -29,11 +29,12 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 5e7a5bdf8d..1997da237a 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -46,11 +46,12 @@ "js-yaml": "^4.0.0", "lodash": "^4.17.21", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index cbd5db27b4..c59add10f0 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -41,11 +41,12 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 6676547a85..b064e94bcc 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -38,10 +38,11 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/org/package.json b/plugins/org/package.json index 2adb6c5d18..2e324513ef 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -30,12 +30,13 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "qs": "^6.10.1", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 13cb8854ae..c0292d008b 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -42,11 +42,12 @@ "@types/react": "*", "classnames": "^2.2.6", "luxon": "2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/permission-react/package.json b/plugins/permission-react/package.json index 26c5f71da0..288b4a7857 100644 --- a/plugins/permission-react/package.json +++ b/plugins/permission-react/package.json @@ -32,10 +32,12 @@ "@backstage/plugin-permission-common": "^0.2.0", "@types/react": "*", "cross-fetch": "^3.0.6", - "react": "^16.13.1", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/test-utils": "^0.1.22", diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index d71c5e1cfe..f28f44ab14 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -41,13 +41,14 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "lodash": "^4.17.21", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-sparklines": "^1.7.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 404bfcd387..817621c558 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -56,8 +56,6 @@ "lodash": "^4.17.21", "luxon": "^2.0.2", "qs": "^6.9.4", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-lazylog": "^4.5.2", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", @@ -65,6 +63,9 @@ "use-immer": "^0.6.0", "zen-observable": "^0.8.15" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/search/package.json b/plugins/search/package.json index 1804a7aca8..211c454573 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -44,12 +44,13 @@ "@material-ui/lab": "4.0.0-alpha.57", "@types/react": "*", "qs": "^6.9.4", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index bb99225479..7b45c2c981 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -41,12 +41,13 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-sparklines": "^1.7.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index f1f41854ef..352cae13be 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -29,14 +29,15 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@types/zen-observable": "^0.8.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-hook-form": "^7.12.2", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4", "uuid": "^8.3.2", "zen-observable": "^0.8.15" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 36c903701f..34d206846f 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -44,10 +44,11 @@ "@material-ui/styles": "^4.10.0", "cross-fetch": "^3.0.6", "rc-progress": "^3.0.0", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 28ce356d9c..757f8d6dc9 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -41,11 +41,12 @@ "@material-ui/lab": "4.0.0-alpha.57", "classnames": "^2.2.6", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/tech-insights/package.json b/plugins/tech-insights/package.json index a55a0ce904..a5d8f87232 100644 --- a/plugins/tech-insights/package.json +++ b/plugins/tech-insights/package.json @@ -26,8 +26,6 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4", "react-router-dom": "6.0.0-beta.0", "@backstage/plugin-catalog-react": "^0.6.4", @@ -36,6 +34,9 @@ "@backstage/errors": "^0.1.4", "@backstage/types": "^0.1.1" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index d377318e4b..e8a75644f8 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -40,10 +40,11 @@ "color": "^4.0.1", "d3-force": "^2.0.1", "prop-types": "^15.7.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index c65fba7210..840cb2cea1 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -52,14 +52,16 @@ "event-source-polyfill": "^1.0.25", "git-url-parse": "^11.6.0", "lodash": "^4.17.21", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-lazylog": "^4.5.2", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-text-truncate": "^0.16.0", "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.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/todo/package.json b/plugins/todo/package.json index 1fadc213af..ad00f6dee5 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -36,10 +36,11 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index feab9dcf93..ba1d464453 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -37,11 +37,12 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index aa2f6169e3..7a7113a984 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -30,11 +30,12 @@ "@material-ui/lab": "4.0.0-alpha.57", "lodash": "^4.17.21", "luxon": "^2.0.2", - "react": "^16.13.1", - "react-dom": "^16.13.1", "react-use": "^17.2.4", "recharts": "^1.8.5" }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, "devDependencies": { "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", From 39c64f50353315daf2fd156e9a1d5a3218d1cf6e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Dec 2021 13:58:15 +0100 Subject: [PATCH 060/116] packages,plugins: move @types/react to peer deps and sync query with react Signed-off-by: Patrik Oldsberg --- packages/app-defaults/package.json | 2 +- packages/core-app-api/package.json | 2 +- packages/core-components/package.json | 2 +- packages/core-plugin-api/package.json | 2 +- packages/dev-utils/package.json | 2 +- packages/test-utils/package.json | 2 +- packages/version-bridge/package.json | 4 +--- plugins/api-docs/package.json | 2 +- plugins/catalog-graph/package.json | 2 +- plugins/catalog-import/package.json | 2 +- plugins/catalog-react/package.json | 2 +- plugins/catalog/package.json | 2 +- plugins/cost-insights/package.json | 2 +- plugins/explore/package.json | 2 +- plugins/git-release-manager/package.json | 2 +- plugins/home/package.json | 2 +- plugins/lighthouse/package.json | 2 +- plugins/pagerduty/package.json | 2 +- plugins/permission-react/package.json | 2 +- plugins/rollbar/package.json | 2 +- plugins/scaffolder/package.json | 2 +- plugins/search/package.json | 2 +- plugins/sentry/package.json | 2 +- plugins/tech-radar/package.json | 2 +- plugins/techdocs/package.json | 2 +- 25 files changed, 25 insertions(+), 27 deletions(-) diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 6a324397a8..450db2cca6 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -47,7 +47,7 @@ "@testing-library/react": "^11.2.5", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "@types/react": "*" + "@types/react": "^16.13.1 || ^17.0.0" }, "files": [ "dist" diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index cbf8acb219..183f578f5c 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -38,7 +38,6 @@ "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", - "@types/react": "*", "@types/prop-types": "^15.7.3", "prop-types": "^15.7.2", "react-router-dom": "6.0.0-beta.0", @@ -46,6 +45,7 @@ "zen-observable": "^0.8.15" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 804b4f03a4..1b04173c40 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -37,7 +37,6 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@types/react": "*", "@types/react-sparklines": "^1.7.0", "@types/react-text-truncate": "^0.14.0", "classnames": "^2.2.6", @@ -66,6 +65,7 @@ "zen-observable": "^0.8.15" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0", "react-dom": "^16.13.1 || ^17.0.0" }, diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 462c8e45d7..d950a75ab4 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -34,7 +34,6 @@ "@backstage/types": "^0.1.1", "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", - "@types/react": "*", "history": "^5.0.0", "prop-types": "^15.7.2", "react-router-dom": "6.0.0-beta.0", @@ -42,6 +41,7 @@ "zen-observable": "^0.8.15" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 1b1dd66929..0c8bf8e3a0 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -43,7 +43,6 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", - "@types/react": "*", "react-use": "^17.2.4", "react-hot-loader": "^4.12.21", "react-router": "6.0.0-beta.0", @@ -51,6 +50,7 @@ "zen-observable": "^0.8.15" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0", "react-dom": "^16.13.1 || ^17.0.0" }, diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 7b14a33805..c4e850838a 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -38,12 +38,12 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", - "@types/react": "*", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "zen-observable": "^0.8.15" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { diff --git a/packages/version-bridge/package.json b/packages/version-bridge/package.json index 45aa99efe8..82846e1393 100644 --- a/packages/version-bridge/package.json +++ b/packages/version-bridge/package.json @@ -28,10 +28,8 @@ "postpack": "backstage-cli postpack", "clean": "backstage-cli clean" }, - "dependencies": { - "@types/react": "*" - }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index bafdd40b06..7ab2eb7f06 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -40,7 +40,6 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@types/react": "*", "graphiql": "^1.5.12", "graphql": "^16.0.0", "isomorphic-form-data": "^2.0.0", @@ -51,6 +50,7 @@ "swagger-ui-react": "^4.0.0-rc.3" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index e6cdaa0b02..edd187eddb 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -30,7 +30,6 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@types/react": "*", "classnames": "^2.3.1", "lodash": "^4.17.15", "p-limit": "^3.1.0", @@ -39,6 +38,7 @@ "react-use": "^17.2.4" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 7b41d63592..cadb50a6af 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -43,7 +43,6 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@octokit/rest": "^18.5.3", - "@types/react": "*", "git-url-parse": "^11.6.0", "js-base64": "^3.6.0", "lodash": "^4.17.21", @@ -53,6 +52,7 @@ "yaml": "^1.10.0" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index d7a3bb16a4..74f115c267 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -41,7 +41,6 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@types/react": "*", "jwt-decode": "^3.1.0", "lodash": "^4.17.21", "qs": "^6.9.4", @@ -50,6 +49,7 @@ "zen-observable": "^0.8.15" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 2872270c13..6fd875249e 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -42,7 +42,6 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@types/react": "*", "history": "^5.0.0", "lodash": "^4.17.21", "react-helmet": "6.1.0", @@ -50,6 +49,7 @@ "react-use": "^17.2.4" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index eb2e45b2c4..6e196fffcb 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -39,7 +39,6 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@material-ui/styles": "^4.9.6", - "@types/react": "*", "@types/recharts": "^1.8.14", "classnames": "^2.2.6", "history": "^5.0.0", @@ -53,6 +52,7 @@ "yup": "^0.32.9" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 98d5d3644a..62a7aae2dc 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -40,13 +40,13 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@types/react": "*", "classnames": "^2.2.6", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index 2082aeaf0f..0c19efc211 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -29,7 +29,6 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@octokit/rest": "^18.5.3", - "@types/react": "*", "luxon": "^2.0.2", "qs": "^6.10.1", "react-router": "6.0.0-beta.0", @@ -37,6 +36,7 @@ "recharts": "^1.8.5" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { diff --git a/plugins/home/package.json b/plugins/home/package.json index 81fead15c0..6d8df2e7a8 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -27,12 +27,12 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@types/react": "*", "lodash": "^4.17.21", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index c59add10f0..9942747d64 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -58,7 +58,7 @@ "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "@types/react": "*", + "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.0.6", "msw": "^0.35.0" }, diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index c0292d008b..632875a043 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -39,13 +39,13 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@types/react": "*", "classnames": "^2.2.6", "luxon": "2.0.2", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { diff --git a/plugins/permission-react/package.json b/plugins/permission-react/package.json index 288b4a7857..4055e5f2ae 100644 --- a/plugins/permission-react/package.json +++ b/plugins/permission-react/package.json @@ -30,12 +30,12 @@ "@backstage/config": "^0.1.11", "@backstage/core-plugin-api": "^0.2.2", "@backstage/plugin-permission-common": "^0.2.0", - "@types/react": "*", "cross-fetch": "^3.0.6", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index f28f44ab14..4d265b05b3 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -60,7 +60,7 @@ "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "@types/react": "*", + "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.0.6", "msw": "^0.35.0" }, diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 817621c558..35b14dffa1 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -47,7 +47,6 @@ "@material-ui/lab": "4.0.0-alpha.57", "@rjsf/core": "^3.2.1", "@rjsf/material-ui": "^3.2.1", - "@types/react": "*", "classnames": "^2.2.6", "git-url-parse": "^11.6.0", "humanize-duration": "^3.25.1", @@ -64,6 +63,7 @@ "zen-observable": "^0.8.15" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { diff --git a/plugins/search/package.json b/plugins/search/package.json index 211c454573..e301a2e452 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -42,13 +42,13 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@types/react": "*", "qs": "^6.9.4", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 7b45c2c981..a82cb205b6 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -59,7 +59,7 @@ "@types/jest": "^26.0.7", "@types/luxon": "^2.0.4", "@types/node": "^14.14.32", - "@types/react": "*", + "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.0.6", "msw": "^0.35.0" }, diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index e8a75644f8..6746e2188d 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -57,7 +57,7 @@ "@types/d3-force": "^2.1.1", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "@types/react": "*", + "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.0.6", "msw": "^0.35.0" }, diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 840cb2cea1..b048761e1c 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -47,7 +47,6 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@material-ui/styles": "^4.10.0", - "@types/react": "*", "dompurify": "^2.2.9", "event-source-polyfill": "^1.0.25", "git-url-parse": "^11.6.0", @@ -59,6 +58,7 @@ "react-use": "^17.2.4" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0", "react-dom": "^16.13.1 || ^17.0.0" }, From 2d3af35de5786ab1e67ba77d1c7a8a4f37163cb2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Dec 2021 14:24:15 +0100 Subject: [PATCH 061/116] packages: bump last few react deps to ^16.31.1 Signed-off-by: Patrik Oldsberg --- packages/app/package.json | 4 ++-- packages/cli/package.json | 1 - packages/storybook/package.json | 4 ++-- yarn.lock | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 36b811c27d..4d7e7d9a62 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -56,8 +56,8 @@ "@roadiehq/backstage-plugin-travis-ci": "^1.0.11", "history": "^5.0.0", "prop-types": "^15.7.2", - "react": "^16.12.0", - "react-dom": "^16.12.0", + "react": "^16.13.1", + "react-dom": "^16.13.1", "react-hot-loader": "^4.12.21", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index fe8a529de1..0a6b2b934a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -89,7 +89,6 @@ "ora": "^5.3.0", "postcss": "^8.1.0", "process": "^0.11.10", - "react": "^16.0.0", "react-dev-utils": "^12.0.0-next.47", "react-hot-loader": "^4.12.21", "recursive-readdir": "^2.2.2", diff --git a/packages/storybook/package.json b/packages/storybook/package.json index 736afbb8d8..5df0c06632 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -9,8 +9,8 @@ }, "dependencies": { "@backstage/theme": "^0.2.0", - "react": "^16.12.0", - "react-dom": "^16.12.0" + "react": "^16.13.1", + "react-dom": "^16.13.1" }, "devDependencies": { "@storybook/addon-a11y": "^6.3.4", diff --git a/yarn.lock b/yarn.lock index 35059f4b70..a71567ffe8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8096,7 +8096,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@>=16.9.0": +"@types/react@*", "@types/react@>=16.9.0", "@types/react@^16.13.1 || ^17.0.0": version "16.14.18" resolved "https://registry.npmjs.org/@types/react/-/react-16.14.18.tgz#b2bcea05ee244fde92d409f91bd888ca8e54b20f" integrity sha512-eeyqd1mqoG43mI0TvNKy9QNf1Tjz3DEOsRP3rlPo35OeMIt05I+v9RR8ZvL2GuYZeF2WAcLXJZMzu6zdz3VbtQ== @@ -24819,7 +24819,7 @@ react-virtualized@^9.21.0: prop-types "^15.6.0" react-lifecycles-compat "^3.0.4" -react@^16.0.0, react@^16.12.0, react@^16.13.1: +react@^16.12.0, react@^16.13.1: version "16.13.1" resolved "https://registry.npmjs.org/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w== From 34490b75a699ff005478c2f5aa1530ec5c2dedad Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Dec 2021 14:40:09 +0100 Subject: [PATCH 062/116] cli: update plugin template to use react peer dep Signed-off-by: Patrik Oldsberg --- packages/cli/src/lib/diff/handlers.ts | 24 ++++++++++++++++--- packages/cli/src/lib/version.ts | 4 ++-- .../templates/default-plugin/package.json.hbs | 5 ++-- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/lib/diff/handlers.ts b/packages/cli/src/lib/diff/handlers.ts index f8bbfa09ce..5bd90e3d22 100644 --- a/packages/cli/src/lib/diff/handlers.ts +++ b/packages/cli/src/lib/diff/handlers.ts @@ -75,7 +75,9 @@ class PackageJsonHandler { await this.syncScripts(); await this.syncPublishConfig(); await this.syncDependencies('dependencies'); + await this.syncDependencies('peerDependencies', true); await this.syncDependencies('devDependencies'); + await this.syncReactDeps(); } // Make sure a field inside package.json is in sync. This mutates the targetObj and writes package.json on change. @@ -207,12 +209,12 @@ class PackageJsonHandler { } } - private async syncDependencies(fieldName: string) { + private async syncDependencies(fieldName: string, required: boolean = false) { const pkgDeps = this.pkg[fieldName]; const targetDeps = (this.targetPkg[fieldName] = this.targetPkg[fieldName] || {}); - if (!pkgDeps) { + if (!pkgDeps && !required) { return; } @@ -231,10 +233,26 @@ class PackageJsonHandler { continue; } - await this.syncField(key, pkgDeps, targetDeps, fieldName, true, true); + await this.syncField( + key, + pkgDeps, + targetDeps, + fieldName, + true, + !required, + ); } } + private async syncReactDeps() { + const targetDeps = (this.targetPkg.dependencies = + this.targetPkg.dependencies || {}); + + // Remove these from from deps since they're now in peerDeps + await this.syncField('react', {}, targetDeps, 'dependencies'); + await this.syncField('react-dom', {}, targetDeps, 'dependencies'); + } + private async write() { await this.writeFunc(`${JSON.stringify(this.targetPkg, null, 2)}\n`); } diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/lib/version.ts index 3b512b22c3..d65e1e06d6 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/lib/version.ts @@ -66,7 +66,7 @@ export const version = findVersion(); export const isDev = fs.pathExistsSync(paths.resolveOwn('src')); export function createPackageVersionProvider(lockfile?: Lockfile) { - return (name: string, versionHint?: string) => { + return (name: string, versionHint?: string): string => { const packageVersion = packageVersions[name]; const targetVersion = versionHint || packageVersion; if (!targetVersion) { @@ -94,6 +94,6 @@ export function createPackageVersionProvider(lockfile?: Lockfile) { if (semver.parse(versionHint)?.prerelease.length) { return versionHint!; } - return `^${versionHint}`; + return versionHint?.match(/^[\d\.]+$/) ? `^${versionHint}` : versionHint!; }; } diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index 624302da93..7461b0a57b 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -32,10 +32,11 @@ "@material-ui/core": "{{versionQuery '@material-ui/core' '4.12.2'}}", "@material-ui/icons": "{{versionQuery '@material-ui/icons' '4.9.1'}}", "@material-ui/lab": "{{versionQuery '@material-ui/lab' '4.0.0-alpha.57'}}", - "react": "{{versionQuery 'react' '16.13.1'}}", - "react-dom": "{{versionQuery 'react-dom' '16.13.1'}}", "react-use": "{{versionQuery 'react-use' '17.2.4'}}" }, + "peerDependencies": { + "react": "{{versionQuery 'react' '^16.13.1 || ^17.0.0'}}" + }, "devDependencies": { "@backstage/cli": "{{versionQuery '@backstage/cli'}}", "@backstage/core-app-api": "{{versionQuery '@backstage/core-app-api'}}", From 777126a193fc8fcc07fd464b6c4491d5acdfa4f9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Dec 2021 14:48:10 +0100 Subject: [PATCH 063/116] scripts: update type dependency checker to include peerDependencies Signed-off-by: Patrik Oldsberg --- scripts/check-type-dependencies.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/check-type-dependencies.js b/scripts/check-type-dependencies.js index bb5f3b0967..4db1109145 100755 --- a/scripts/check-type-dependencies.js +++ b/scripts/check-type-dependencies.js @@ -143,7 +143,10 @@ function findTypesPackage(dep, pkg) { */ function findTypeDepErrors(typeDeps, pkg) { const devDeps = mkTypeDepSet(pkg.get('devDependencies')); - const deps = mkTypeDepSet(pkg.get('dependencies')); + const deps = mkTypeDepSet({ + ...pkg.get('dependencies'), + ...pkg.get('peerDependencies'), + }); const errors = []; for (const typeDep of typeDeps) { From cd450844f6471e7564d6514d1813f587eae4b21c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Dec 2021 14:54:55 +0100 Subject: [PATCH 064/116] changesets: added changesets for React 17 bump Signed-off-by: Patrik Oldsberg --- .changeset/fresh-walls-impress.md | 60 +++++++++++++++++++++++++++++++ .changeset/serious-buckets-lay.md | 5 +++ 2 files changed, 65 insertions(+) create mode 100644 .changeset/fresh-walls-impress.md create mode 100644 .changeset/serious-buckets-lay.md diff --git a/.changeset/fresh-walls-impress.md b/.changeset/fresh-walls-impress.md new file mode 100644 index 0000000000..08ccded75c --- /dev/null +++ b/.changeset/fresh-walls-impress.md @@ -0,0 +1,60 @@ +--- +'@backstage/app-defaults': patch +'@backstage/core-app-api': patch +'@backstage/core-components': patch +'@backstage/core-plugin-api': patch +'@backstage/dev-utils': patch +'@backstage/integration-react': patch +'@backstage/test-utils': patch +'@backstage/version-bridge': patch +'@backstage/plugin-allure': patch +'@backstage/plugin-analytics-module-ga': patch +'@backstage/plugin-api-docs': patch +'@backstage/plugin-azure-devops': patch +'@backstage/plugin-badges': patch +'@backstage/plugin-bazaar': patch +'@backstage/plugin-bitrise': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-graph': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-circleci': patch +'@backstage/plugin-cloudbuild': patch +'@backstage/plugin-code-coverage': patch +'@backstage/plugin-config-schema': patch +'@backstage/plugin-cost-insights': patch +'@backstage/plugin-explore': patch +'@backstage/plugin-firehydrant': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-gcp-projects': patch +'@backstage/plugin-git-release-manager': patch +'@backstage/plugin-github-actions': patch +'@backstage/plugin-github-deployments': patch +'@backstage/plugin-gitops-profiles': patch +'@backstage/plugin-graphiql': patch +'@backstage/plugin-home': patch +'@backstage/plugin-ilert': patch +'@backstage/plugin-jenkins': patch +'@backstage/plugin-kafka': patch +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-lighthouse': patch +'@backstage/plugin-newrelic': patch +'@backstage/plugin-org': patch +'@backstage/plugin-pagerduty': patch +'@backstage/plugin-permission-react': patch +'@backstage/plugin-rollbar': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-search': patch +'@backstage/plugin-sentry': patch +'@backstage/plugin-shortcuts': patch +'@backstage/plugin-sonarqube': patch +'@backstage/plugin-splunk-on-call': patch +'@backstage/plugin-tech-insights': patch +'@backstage/plugin-tech-radar': patch +'@backstage/plugin-techdocs': patch +'@backstage/plugin-todo': patch +'@backstage/plugin-user-settings': patch +'@backstage/plugin-xcmetrics': patch +--- + +Moved React dependencies to `peerDependencies` and allow both React v16 and v17 to be used. diff --git a/.changeset/serious-buckets-lay.md b/.changeset/serious-buckets-lay.md new file mode 100644 index 0000000000..70afc7ae76 --- /dev/null +++ b/.changeset/serious-buckets-lay.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated the frontend plugin template to put React dependencies in `peerDependencies` by default, as well as allowing both React v16 and v17. This change can be applied to existing plugins by running `yarn backstage-cli plugin:diff` within the plugin package directory. From b191e1737ae56f7ab4ea9fadef40a53b05024977 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Dec 2021 15:11:49 +0100 Subject: [PATCH 065/116] packages,plugins: avoid usage of react-dom in tests Signed-off-by: Patrik Oldsberg --- .../src/components/CopyTextButton/CopyTextButton.test.tsx | 3 +-- .../core-components/src/components/Progress/Progress.test.tsx | 2 +- .../src/components/TabbedLayout/RoutedTabs.test.tsx | 3 +-- .../src/components/TabbedLayout/TabbedLayout.test.tsx | 3 +-- packages/test-utils/src/testUtils/testingLibrary.ts | 3 +-- .../catalog/src/components/EntityLayout/EntityLayout.test.tsx | 3 +-- .../src/components/EntityPageLayout/Tabbed/Tabbed.test.tsx | 3 +-- .../graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx | 2 +- .../src/components/TemplatePage/TemplatePage.test.tsx | 3 +-- plugins/search/src/components/util.test.tsx | 3 +-- plugins/tech-radar/src/components/RadarComponent.test.tsx | 3 +-- plugins/tech-radar/src/components/RadarPage.test.tsx | 3 +-- 12 files changed, 12 insertions(+), 22 deletions(-) diff --git a/packages/core-components/src/components/CopyTextButton/CopyTextButton.test.tsx b/packages/core-components/src/components/CopyTextButton/CopyTextButton.test.tsx index a9532dd79b..ecb784f951 100644 --- a/packages/core-components/src/components/CopyTextButton/CopyTextButton.test.tsx +++ b/packages/core-components/src/components/CopyTextButton/CopyTextButton.test.tsx @@ -15,8 +15,7 @@ */ import React from 'react'; -import { fireEvent } from '@testing-library/react'; -import { act } from 'react-dom/test-utils'; +import { act, fireEvent } from '@testing-library/react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { CopyTextButton } from './CopyTextButton'; import { errorApiRef } from '@backstage/core-plugin-api'; diff --git a/packages/core-components/src/components/Progress/Progress.test.tsx b/packages/core-components/src/components/Progress/Progress.test.tsx index 4f4e74ce08..d4057c1675 100644 --- a/packages/core-components/src/components/Progress/Progress.test.tsx +++ b/packages/core-components/src/components/Progress/Progress.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { renderInTestApp } from '@backstage/test-utils'; -import { act } from 'react-dom/test-utils'; +import { act } from '@testing-library/react'; import { Progress } from './Progress'; diff --git a/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx b/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx index bbd48c127c..f76524fe50 100644 --- a/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx +++ b/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx @@ -14,9 +14,8 @@ * limitations under the License. */ import { renderInTestApp } from '@backstage/test-utils'; -import { fireEvent } from '@testing-library/react'; +import { act, fireEvent } from '@testing-library/react'; import React from 'react'; -import { act } from 'react-dom/test-utils'; import { Route, Routes } from 'react-router'; import { RoutedTabs } from './RoutedTabs'; diff --git a/packages/core-components/src/components/TabbedLayout/TabbedLayout.test.tsx b/packages/core-components/src/components/TabbedLayout/TabbedLayout.test.tsx index 2bc60783d7..55422faffc 100644 --- a/packages/core-components/src/components/TabbedLayout/TabbedLayout.test.tsx +++ b/packages/core-components/src/components/TabbedLayout/TabbedLayout.test.tsx @@ -14,9 +14,8 @@ * limitations under the License. */ import { renderInTestApp, withLogCollector } from '@backstage/test-utils'; -import { fireEvent } from '@testing-library/react'; +import { act, fireEvent } from '@testing-library/react'; import React from 'react'; -import { act } from 'react-dom/test-utils'; import { Route, Routes } from 'react-router'; import { TabbedLayout } from './TabbedLayout'; diff --git a/packages/test-utils/src/testUtils/testingLibrary.ts b/packages/test-utils/src/testUtils/testingLibrary.ts index 44ea20a4cc..9fb47cbc90 100644 --- a/packages/test-utils/src/testUtils/testingLibrary.ts +++ b/packages/test-utils/src/testUtils/testingLibrary.ts @@ -15,8 +15,7 @@ */ import { ReactElement } from 'react'; -import { act } from 'react-dom/test-utils'; -import { render, RenderResult } from '@testing-library/react'; +import { act, render, RenderResult } from '@testing-library/react'; /** * @public diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx index 3f3fc804e1..a45afe6e02 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx @@ -31,9 +31,8 @@ import { renderInTestApp, TestApiRegistry, } from '@backstage/test-utils'; -import { fireEvent } from '@testing-library/react'; +import { act, fireEvent } from '@testing-library/react'; import React from 'react'; -import { act } from 'react-dom/test-utils'; import { Route, Routes } from 'react-router'; import { EntityLayout } from './EntityLayout'; diff --git a/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.test.tsx b/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.test.tsx index 9681420e1c..ad111380f8 100644 --- a/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.test.tsx +++ b/plugins/catalog/src/components/EntityPageLayout/Tabbed/Tabbed.test.tsx @@ -16,8 +16,7 @@ import React from 'react'; import { Tabbed } from './Tabbed'; import { renderInTestApp } from '@backstage/test-utils'; -import { fireEvent } from '@testing-library/react'; -import { act } from 'react-dom/test-utils'; +import { act, fireEvent } from '@testing-library/react'; import { Routes, Route } from 'react-router'; describe('Tabbed layout', () => { diff --git a/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx b/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx index ae84b549ec..90f8edd4ad 100644 --- a/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx +++ b/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { GraphiQLPage } from './GraphiQLPage'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; -import { act } from 'react-dom/test-utils'; +import { act } from '@testing-library/react'; import { renderWithEffects, TestApiProvider } from '@backstage/test-utils'; import { GraphQLBrowseApi, graphQlBrowseApiRef } from '../../lib/api'; import { configApiRef } from '@backstage/core-plugin-api'; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx index 60e9ee8840..7ca65fff25 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx @@ -20,9 +20,8 @@ import { } from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; -import { fireEvent, within } from '@testing-library/react'; +import { act, fireEvent, within } from '@testing-library/react'; import React from 'react'; -import { act } from 'react-dom/test-utils'; import { MemoryRouter, Route } from 'react-router'; import { ScaffolderApi, scaffolderApiRef } from '../../api'; import { rootRouteRef } from '../../routes'; diff --git a/plugins/search/src/components/util.test.tsx b/plugins/search/src/components/util.test.tsx index a5b82370e3..1b452a205e 100644 --- a/plugins/search/src/components/util.test.tsx +++ b/plugins/search/src/components/util.test.tsx @@ -16,11 +16,10 @@ import React from 'react'; import { wrapInTestApp } from '@backstage/test-utils'; -import { render } from '@testing-library/react'; +import { act, render } from '@testing-library/react'; import { useNavigateToQuery } from './util'; import { Routes, Route } from 'react-router-dom'; import { rootRouteRef } from '../plugin'; -import { act } from 'react-dom/test-utils'; const navigate = jest.fn(); jest.mock('react-router-dom', () => ({ diff --git a/plugins/tech-radar/src/components/RadarComponent.test.tsx b/plugins/tech-radar/src/components/RadarComponent.test.tsx index 2b237c8d83..aeced1a2da 100644 --- a/plugins/tech-radar/src/components/RadarComponent.test.tsx +++ b/plugins/tech-radar/src/components/RadarComponent.test.tsx @@ -15,10 +15,9 @@ */ import React from 'react'; -import { render, waitForElement } from '@testing-library/react'; +import { act, render, waitForElement } from '@testing-library/react'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; -import { act } from 'react-dom/test-utils'; import { TestApiProvider, withLogCollector } from '@backstage/test-utils'; import GetBBoxPolyfill from '../utils/polyfills/getBBox'; diff --git a/plugins/tech-radar/src/components/RadarPage.test.tsx b/plugins/tech-radar/src/components/RadarPage.test.tsx index 7f4e69e388..e9bcb3d314 100644 --- a/plugins/tech-radar/src/components/RadarPage.test.tsx +++ b/plugins/tech-radar/src/components/RadarPage.test.tsx @@ -22,9 +22,8 @@ import { } from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; -import { render, waitForElement } from '@testing-library/react'; +import { act, render, waitForElement } from '@testing-library/react'; import React from 'react'; -import { act } from 'react-dom/test-utils'; import GetBBoxPolyfill from '../utils/polyfills/getBBox'; import { RadarPage } from './RadarPage'; import { TechRadarLoaderResponse, techRadarApiRef, TechRadarApi } from '../api'; From 2976a2b8e10c9555caf706038713de67221b1c63 Mon Sep 17 00:00:00 2001 From: Joon Park Date: Tue, 7 Dec 2021 16:24:54 +0000 Subject: [PATCH 066/116] Fix directory typo in catalog-react Signed-off-by: Joon Park --- plugins/catalog-react/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index c65f9144e5..799c6d0ce6 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -15,7 +15,7 @@ "repository": { "type": "git", "url": "https://github.com/backstage/backstage", - "directory": "plugins/plugin-catalog-common-react" + "directory": "plugins/catalog-react" }, "keywords": [ "backstage" From 69034b44191f47c10ee7b27b457c8eb2fc4b5dda Mon Sep 17 00:00:00 2001 From: Joon Park Date: Tue, 7 Dec 2021 16:53:38 +0000 Subject: [PATCH 067/116] Add changeset Signed-off-by: Joon Park --- .changeset/three-frogs-teach.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/three-frogs-teach.md diff --git a/.changeset/three-frogs-teach.md b/.changeset/three-frogs-teach.md new file mode 100644 index 0000000000..836ec8abf2 --- /dev/null +++ b/.changeset/three-frogs-teach.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Fix typo in catalog-react package.json From dcd1a0c3f4d50bde823b098af4a68adf601c62cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 7 Dec 2021 19:42:10 +0100 Subject: [PATCH 068/116] Do not unpack arguments directly on exported items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/gentle-masks-lie.md | 25 +++++ packages/backend-common/api-report.md | 89 ++++----------- .../backend-common/src/reading/UrlReaders.ts | 6 +- packages/backend-common/src/scm/git.ts | 106 ++++++------------ .../src/util/DockerContainerRunner.ts | 26 +++-- packages/core-app-api/api-report.md | 87 +++----------- .../auth/atlassian/AtlassianAuth.ts | 14 ++- .../implementations/auth/auth0/Auth0Auth.ts | 16 +-- .../auth/bitbucket/BitbucketAuth.ts | 16 +-- .../implementations/auth/github/GithubAuth.ts | 16 +-- .../implementations/auth/gitlab/GitlabAuth.ts | 16 +-- .../implementations/auth/google/GoogleAuth.ts | 24 ++-- .../auth/microsoft/MicrosoftAuth.ts | 28 ++--- .../implementations/auth/oauth2/OAuth2.ts | 18 +-- .../implementations/auth/okta/OktaAuth.ts | 16 +-- .../auth/onelogin/OneLoginAuth.ts | 16 ++- .../implementations/auth/saml/SamlAuth.ts | 12 +- packages/core-components/api-report.md | 5 +- .../src/layout/Sidebar/SidebarSubmenu.tsx | 14 +-- packages/core-plugin-api/api-report.md | 5 +- .../src/analytics/AnalyticsContext.tsx | 7 +- packages/dev-utils/api-report.md | 12 +- .../EntityGridItem/EntityGridItem.tsx | 9 +- packages/techdocs-common/api-report.md | 54 ++------- .../src/stages/generate/generators.ts | 10 +- .../src/stages/generate/techdocs.ts | 39 +++---- .../src/stages/generate/types.ts | 13 ++- .../src/stages/publish/index.ts | 7 +- .../src/stages/publish/types.ts | 10 +- packages/test-utils/api-report.md | 21 +--- .../src/testUtils/TestApiProvider.tsx | 12 +- .../apis/AnalyticsApi/MockAnalyticsApi.ts | 11 +- .../src/testUtils/mockBreakpoint.ts | 8 +- plugins/analytics-module-ga/api-report.md | 8 +- .../AnalyticsApi/GoogleAnalytics.ts | 19 +--- plugins/auth-backend/api-report.md | 13 +-- .../src/lib/catalog/CatalogIdentityClient.ts | 6 +- plugins/auth-backend/src/service/router.ts | 11 +- plugins/catalog-backend/api-report.md | 8 +- .../src/search/DefaultCatalogCollator.ts | 11 +- plugins/config-schema/api-report.md | 4 +- .../src/api/StaticSchemaLoader.ts | 6 +- plugins/github-deployments/api-report.md | 17 +-- .../src/components/GithubDeploymentsCard.tsx | 7 +- .../GithubDeploymentsTable.tsx | 8 +- .../GithubDeploymentsTable/columns.tsx | 4 +- plugins/pagerduty/api-report.md | 13 +-- plugins/pagerduty/src/api/client.ts | 9 +- .../src/components/TriggerButton/index.tsx | 12 +- plugins/permission-node/api-report.md | 6 +- .../createPermissionIntegrationRouter.ts | 8 +- plugins/permission-react/api-report.md | 31 ++--- .../src/apis/IdentityPermissionApi.ts | 7 +- .../src/components/PermissionedRoute.tsx | 23 ++-- .../api-report.md | 18 +-- .../src/engines/ElasticSearchSearchEngine.ts | 30 ++--- .../search-backend-module-pg/api-report.md | 4 +- .../src/PgSearchEngine/PgSearchEngine.ts | 6 +- plugins/search-backend/api-report.md | 14 ++- plugins/search-backend/src/service/router.ts | 10 +- plugins/techdocs-backend/api-report.md | 10 +- .../src/search/DefaultTechDocsCollator.ts | 25 ++--- plugins/todo-backend/api-report.md | 2 +- .../src/lib/TodoReader/TodoScmReader.ts | 6 +- plugins/todo/api-report.md | 8 +- plugins/todo/src/api/TodoClient.ts | 9 +- 66 files changed, 433 insertions(+), 708 deletions(-) create mode 100644 .changeset/gentle-masks-lie.md diff --git a/.changeset/gentle-masks-lie.md b/.changeset/gentle-masks-lie.md new file mode 100644 index 0000000000..2f61ef656d --- /dev/null +++ b/.changeset/gentle-masks-lie.md @@ -0,0 +1,25 @@ +--- +'@backstage/backend-common': patch +'@backstage/core-app-api': patch +'@backstage/core-components': patch +'@backstage/core-plugin-api': patch +'@backstage/dev-utils': patch +'@backstage/techdocs-common': patch +'@backstage/test-utils': patch +'@backstage/plugin-analytics-module-ga': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-config-schema': patch +'@backstage/plugin-github-deployments': patch +'@backstage/plugin-pagerduty': patch +'@backstage/plugin-permission-node': patch +'@backstage/plugin-permission-react': patch +'@backstage/plugin-search-backend': patch +'@backstage/plugin-search-backend-module-elasticsearch': patch +'@backstage/plugin-search-backend-module-pg': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-todo': patch +'@backstage/plugin-todo-backend': patch +--- + +Minor improvement to the API reports, by not unpacking arguments directly diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 9c66c0573e..376cdc8b64 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -180,18 +180,9 @@ export class DatabaseManager { // @public (undocumented) export class DockerContainerRunner implements ContainerRunner { - constructor({ dockerClient }: { dockerClient: Docker }); + constructor(options: { dockerClient: Docker }); // (undocumented) - runContainer({ - imageName, - command, - args, - logStream, - mountDirs, - workingDir, - envVars, - pullImage, - }: RunContainerOptions): Promise; + runContainer(options: RunContainerOptions): Promise; } // @public @@ -227,34 +218,17 @@ export function getVoidLogger(): winston.Logger; // @public (undocumented) export class Git { // (undocumented) - add({ dir, filepath }: { dir: string; filepath: string }): Promise; + add(options: { dir: string; filepath: string }): Promise; // (undocumented) - addRemote({ - dir, - url, - remote, - }: { + addRemote(options: { dir: string; remote: string; url: string; }): Promise; // (undocumented) - clone({ - url, - dir, - ref, - }: { - url: string; - dir: string; - ref?: string; - }): Promise; + clone(options: { url: string; dir: string; ref?: string }): Promise; // (undocumented) - commit({ - dir, - message, - author, - committer, - }: { + commit(options: { dir: string; message: string; author: { @@ -267,41 +241,22 @@ export class Git { }; }): Promise; // (undocumented) - currentBranch({ - dir, - fullName, - }: { + currentBranch(options: { dir: string; fullName?: boolean; }): Promise; // (undocumented) - fetch({ dir, remote }: { dir: string; remote?: string }): Promise; + fetch(options: { dir: string; remote?: string }): Promise; // (undocumented) - static fromAuth: ({ - username, - password, - logger, - }: { - username?: string | undefined; - password?: string | undefined; - logger?: Logger_2 | undefined; + static fromAuth: (options: { + username?: string; + password?: string; + logger?: Logger_2; }) => Git; // (undocumented) - init({ - dir, - defaultBranch, - }: { - dir: string; - defaultBranch?: string; - }): Promise; + init(options: { dir: string; defaultBranch?: string }): Promise; // (undocumented) - merge({ - dir, - theirs, - ours, - author, - committer, - }: { + merge(options: { dir: string; theirs: string; ours?: string; @@ -315,17 +270,11 @@ export class Git { }; }): Promise; // (undocumented) - push({ dir, remote }: { dir: string; remote: string }): Promise; + push(options: { dir: string; remote: string }): Promise; // (undocumented) - readCommit({ - dir, - sha, - }: { - dir: string; - sha: string; - }): Promise; + readCommit(options: { dir: string; sha: string }): Promise; // (undocumented) - resolveRef({ dir, ref }: { dir: string; ref: string }): Promise; + resolveRef(options: { dir: string; ref: string }): Promise; } // @public @@ -623,8 +572,8 @@ export type UrlReaderPredicateTuple = { // @public export class UrlReaders { - static create({ logger, config, factories }: UrlReadersOptions): UrlReader; - static default({ logger, config, factories }: UrlReadersOptions): UrlReader; + static create(options: UrlReadersOptions): UrlReader; + static default(options: UrlReadersOptions): UrlReader; } // @public (undocumented) diff --git a/packages/backend-common/src/reading/UrlReaders.ts b/packages/backend-common/src/reading/UrlReaders.ts index 7120a9570f..a920ec080b 100644 --- a/packages/backend-common/src/reading/UrlReaders.ts +++ b/packages/backend-common/src/reading/UrlReaders.ts @@ -46,7 +46,8 @@ export class UrlReaders { /** * Creates a UrlReader without any known types. */ - static create({ logger, config, factories }: UrlReadersOptions): UrlReader { + static create(options: UrlReadersOptions): UrlReader { + const { logger, config, factories } = options; const mux = new UrlReaderPredicateMux(logger); const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config, @@ -68,7 +69,8 @@ export class UrlReaders { * * Any additional factories passed will be loaded before the default ones. */ - static default({ logger, config, factories = [] }: UrlReadersOptions) { + static default(options: UrlReadersOptions) { + const { logger, config, factories = [] } = options; return UrlReaders.create({ logger, config, diff --git a/packages/backend-common/src/scm/git.ts b/packages/backend-common/src/scm/git.ts index 00b103b240..372ecb0260 100644 --- a/packages/backend-common/src/scm/git.ts +++ b/packages/backend-common/src/scm/git.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import git, { ProgressCallback, MergeResult, @@ -42,44 +43,32 @@ export class Git { }, ) {} - async add({ - dir, - filepath, - }: { - dir: string; - filepath: string; - }): Promise { + async add(options: { dir: string; filepath: string }): Promise { + const { dir, filepath } = options; this.config.logger?.info(`Adding file {dir=${dir},filepath=${filepath}}`); return git.add({ fs, dir, filepath }); } - async addRemote({ - dir, - url, - remote, - }: { + async addRemote(options: { dir: string; remote: string; url: string; }): Promise { + const { dir, url, remote } = options; this.config.logger?.info( `Creating new remote {dir=${dir},remote=${remote},url=${url}}`, ); return git.addRemote({ fs, dir, remote, url }); } - async commit({ - dir, - message, - author, - committer, - }: { + async commit(options: { dir: string; message: string; author: { name: string; email: string }; committer: { name: string; email: string }; }): Promise { + const { dir, message, author, committer } = options; this.config.logger?.info( `Committing file to repo {dir=${dir},message=${message}}`, ); @@ -87,15 +76,12 @@ export class Git { return git.commit({ fs, dir, message, author, committer }); } - async clone({ - url, - dir, - ref, - }: { + async clone(options: { url: string; dir: string; ref?: string; }): Promise { + const { url, dir, ref } = options; this.config.logger?.info(`Cloning repo {dir=${dir},url=${url}}`); return git.clone({ fs, @@ -114,51 +100,35 @@ export class Git { } // https://isomorphic-git.org/docs/en/currentBranch - async currentBranch({ - dir, - fullName, - }: { + async currentBranch(options: { dir: string; fullName?: boolean; }): Promise { - const fullname = fullName ?? false; - return git.currentBranch({ fs, dir, fullname }) as Promise< + const { dir, fullName = false } = options; + return git.currentBranch({ fs, dir, fullname: fullName }) as Promise< string | undefined >; } // https://isomorphic-git.org/docs/en/fetch - async fetch({ - dir, - remote, - }: { - dir: string; - remote?: string; - }): Promise { - const remoteValue = remote ?? 'origin'; + async fetch(options: { dir: string; remote?: string }): Promise { + const { dir, remote = 'origin' } = options; this.config.logger?.info( - `Fetching remote=${remoteValue} for repository {dir=${dir}}`, + `Fetching remote=${remote} for repository {dir=${dir}}`, ); await git.fetch({ fs, http, dir, - remote: remoteValue, + remote, onProgress: this.onProgressHandler(), - headers: { - 'user-agent': 'git/@isomorphic-git', - }, + headers: { 'user-agent': 'git/@isomorphic-git' }, onAuth: this.onAuth, }); } - async init({ - dir, - defaultBranch = 'master', - }: { - dir: string; - defaultBranch?: string; - }): Promise { + async init(options: { dir: string; defaultBranch?: string }): Promise { + const { dir, defaultBranch = 'master' } = options; this.config.logger?.info(`Init git repository {dir=${dir}}`); return git.init({ @@ -169,19 +139,14 @@ export class Git { } // https://isomorphic-git.org/docs/en/merge - async merge({ - dir, - theirs, - ours, - author, - committer, - }: { + async merge(options: { dir: string; theirs: string; ours?: string; author: { name: string; email: string }; committer: { name: string; email: string }; }): Promise { + const { dir, theirs, ours, author, committer } = options; this.config.logger?.info( `Merging branch '${theirs}' into '${ours}' for repository {dir=${dir}}`, ); @@ -197,7 +162,8 @@ export class Git { }); } - async push({ dir, remote }: { dir: string; remote: string }) { + async push(options: { dir: string; remote: string }) { + const { dir, remote } = options; this.config.logger?.info( `Pushing directory to remote {dir=${dir},remote=${remote}}`, ); @@ -215,24 +181,17 @@ export class Git { } // https://isomorphic-git.org/docs/en/readCommit - async readCommit({ - dir, - sha, - }: { + async readCommit(options: { dir: string; sha: string; }): Promise { + const { dir, sha } = options; return git.readCommit({ fs, dir, oid: sha }); } // https://isomorphic-git.org/docs/en/resolveRef - async resolveRef({ - dir, - ref, - }: { - dir: string; - ref: string; - }): Promise { + async resolveRef(options: { dir: string; ref: string }): Promise { + const { dir, ref } = options; return git.resolveRef({ fs, dir, ref }); } @@ -256,13 +215,12 @@ export class Git { }; }; - static fromAuth = ({ - username, - password, - logger, - }: { + static fromAuth = (options: { username?: string; password?: string; logger?: Logger; - }) => new Git({ username, password, logger }); + }) => { + const { username, password, logger } = options; + return new Git({ username, password, logger }); + }; } diff --git a/packages/backend-common/src/util/DockerContainerRunner.ts b/packages/backend-common/src/util/DockerContainerRunner.ts index b81bd995ca..424913f316 100644 --- a/packages/backend-common/src/util/DockerContainerRunner.ts +++ b/packages/backend-common/src/util/DockerContainerRunner.ts @@ -28,20 +28,22 @@ export type UserOptions = { export class DockerContainerRunner implements ContainerRunner { private readonly dockerClient: Docker; - constructor({ dockerClient }: { dockerClient: Docker }) { - this.dockerClient = dockerClient; + constructor(options: { dockerClient: Docker }) { + this.dockerClient = options.dockerClient; } - async runContainer({ - imageName, - command, - args, - logStream = new PassThrough(), - mountDirs = {}, - workingDir, - envVars = {}, - pullImage = true, - }: RunContainerOptions) { + async runContainer(options: RunContainerOptions) { + const { + imageName, + command, + args, + logStream = new PassThrough(), + mountDirs = {}, + workingDir, + envVars = {}, + pullImage = true, + } = options; + // Show a better error message when Docker is unavailable. try { await this.dockerClient.ping(); diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index e69ee4f325..339e0a7c8b 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -250,24 +250,13 @@ export class AppThemeSelector implements AppThemeApi { // @public export class AtlassianAuth { // (undocumented) - static create({ - discoveryApi, - environment, - provider, - oauthRequestApi, - }: OAuthApiCreateOptions): typeof atlassianAuthApiRef.T; + static create(options: OAuthApiCreateOptions): typeof atlassianAuthApiRef.T; } // @public export class Auth0Auth { // (undocumented) - static create({ - discoveryApi, - environment, - provider, - oauthRequestApi, - defaultScopes, - }: OAuthApiCreateOptions): typeof auth0AuthApiRef.T; + static create(options: OAuthApiCreateOptions): typeof auth0AuthApiRef.T; } // @public @@ -303,13 +292,7 @@ export type BackstagePluginWithAnyOutput = Omit< // @public export class BitbucketAuth { // (undocumented) - static create({ - discoveryApi, - environment, - provider, - oauthRequestApi, - defaultScopes, - }: OAuthApiCreateOptions): typeof bitbucketAuthApiRef.T; + static create(options: OAuthApiCreateOptions): typeof bitbucketAuthApiRef.T; } // @public @@ -402,13 +385,7 @@ export class GithubAuth implements OAuthApi, SessionApi { // @deprecated constructor(sessionManager: SessionManager); // (undocumented) - static create({ - discoveryApi, - environment, - provider, - oauthRequestApi, - defaultScopes, - }: OAuthApiCreateOptions): GithubAuth; + static create(options: OAuthApiCreateOptions): GithubAuth; // (undocumented) getAccessToken(scope?: string, options?: AuthRequestOptions): Promise; // (undocumented) @@ -441,25 +418,13 @@ export type GithubSession = { // @public export class GitlabAuth { // (undocumented) - static create({ - discoveryApi, - environment, - provider, - oauthRequestApi, - defaultScopes, - }: OAuthApiCreateOptions): typeof gitlabAuthApiRef.T; + static create(options: OAuthApiCreateOptions): typeof gitlabAuthApiRef.T; } // @public export class GoogleAuth { // (undocumented) - static create({ - discoveryApi, - oauthRequestApi, - environment, - provider, - defaultScopes, - }: OAuthApiCreateOptions): typeof googleAuthApiRef.T; + static create(options: OAuthApiCreateOptions): typeof googleAuthApiRef.T; } // @public @@ -477,13 +442,7 @@ export class LocalStorageFeatureFlags implements FeatureFlagsApi { // @public export class MicrosoftAuth { // (undocumented) - static create({ - environment, - provider, - oauthRequestApi, - discoveryApi, - defaultScopes, - }: OAuthApiCreateOptions): typeof microsoftAuthApiRef.T; + static create(options: OAuthApiCreateOptions): typeof microsoftAuthApiRef.T; } // @public @@ -507,14 +466,7 @@ export class OAuth2 scopeTransform: (scopes: string[]) => string[]; }); // (undocumented) - static create({ - discoveryApi, - environment, - provider, - oauthRequestApi, - defaultScopes, - scopeTransform, - }: OAuth2CreateOptions): OAuth2; + static create(options: OAuth2CreateOptions): OAuth2; // (undocumented) getAccessToken( scope?: string | string[], @@ -570,24 +522,15 @@ export class OAuthRequestManager implements OAuthRequestApi { // @public export class OktaAuth { // (undocumented) - static create({ - discoveryApi, - environment, - provider, - oauthRequestApi, - defaultScopes, - }: OAuthApiCreateOptions): typeof oktaAuthApiRef.T; + static create(options: OAuthApiCreateOptions): typeof oktaAuthApiRef.T; } // @public export class OneLoginAuth { // (undocumented) - static create({ - discoveryApi, - environment, - provider, - oauthRequestApi, - }: OneLoginAuthCreateOptions): typeof oneloginAuthApiRef.T; + static create( + options: OneLoginAuthCreateOptions, + ): typeof oneloginAuthApiRef.T; } // @public @@ -607,11 +550,7 @@ export class SamlAuth // @deprecated constructor(sessionManager: SessionManager); // (undocumented) - static create({ - discoveryApi, - environment, - provider, - }: AuthApiCreateOptions): SamlAuth; + static create(options: AuthApiCreateOptions): SamlAuth; // (undocumented) getBackstageIdentity( options?: AuthRequestOptions, diff --git a/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts b/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts index caad40ddbf..07ebefca28 100644 --- a/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts @@ -30,12 +30,14 @@ const DEFAULT_PROVIDER = { * @public */ export default class AtlassianAuth { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - }: OAuthApiCreateOptions): typeof atlassianAuthApiRef.T { + static create(options: OAuthApiCreateOptions): typeof atlassianAuthApiRef.T { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + } = options; + return OAuth2.create({ discoveryApi, oauthRequestApi, 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 index 0a158ccb9e..d0a9dc5d99 100644 --- a/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts @@ -30,13 +30,15 @@ const DEFAULT_PROVIDER = { * @public */ export default class Auth0Auth { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = ['openid', `email`, `profile`], - }: OAuthApiCreateOptions): typeof auth0AuthApiRef.T { + 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, diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts index e488580c4d..c97cccc6ed 100644 --- a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts @@ -45,13 +45,15 @@ const DEFAULT_PROVIDER = { * @public */ export default class BitbucketAuth { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = ['team'], - }: OAuthApiCreateOptions): typeof bitbucketAuthApiRef.T { + static create(options: OAuthApiCreateOptions): typeof bitbucketAuthApiRef.T { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + defaultScopes = ['team'], + } = options; + return OAuth2.create({ discoveryApi, oauthRequestApi, 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 3e9c899346..4da92efbdf 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 @@ -56,13 +56,15 @@ const DEFAULT_PROVIDER = { * @public */ export default class GithubAuth implements OAuthApi, SessionApi { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = ['read:user'], - }: OAuthApiCreateOptions) { + static create(options: OAuthApiCreateOptions) { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + defaultScopes = ['read:user'], + } = options; + const connector = new DefaultAuthConnector({ discoveryApi, environment, diff --git a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts index f46f2ef72e..00085de8ac 100644 --- a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts @@ -30,13 +30,15 @@ const DEFAULT_PROVIDER = { * @public */ export default class GitlabAuth { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = ['read_user'], - }: OAuthApiCreateOptions): typeof gitlabAuthApiRef.T { + static create(options: OAuthApiCreateOptions): typeof gitlabAuthApiRef.T { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + defaultScopes = ['read_user'], + } = options; + return OAuth2.create({ discoveryApi, oauthRequestApi, diff --git a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts index a6f37b250f..8a528f4511 100644 --- a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -32,17 +32,19 @@ const SCOPE_PREFIX = 'https://www.googleapis.com/auth/'; * @public */ export default class GoogleAuth { - static create({ - discoveryApi, - oauthRequestApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - defaultScopes = [ - 'openid', - `${SCOPE_PREFIX}userinfo.email`, - `${SCOPE_PREFIX}userinfo.profile`, - ], - }: OAuthApiCreateOptions): typeof googleAuthApiRef.T { + static create(options: OAuthApiCreateOptions): typeof googleAuthApiRef.T { + const { + discoveryApi, + oauthRequestApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + defaultScopes = [ + 'openid', + `${SCOPE_PREFIX}userinfo.email`, + `${SCOPE_PREFIX}userinfo.profile`, + ], + } = options; + return OAuth2.create({ discoveryApi, oauthRequestApi, diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts index 148be66873..be5776609d 100644 --- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts @@ -30,19 +30,21 @@ const DEFAULT_PROVIDER = { * @public */ export default class MicrosoftAuth { - static create({ - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - discoveryApi, - defaultScopes = [ - 'openid', - 'offline_access', - 'profile', - 'email', - 'User.Read', - ], - }: OAuthApiCreateOptions): typeof microsoftAuthApiRef.T { + static create(options: OAuthApiCreateOptions): typeof microsoftAuthApiRef.T { + const { + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + discoveryApi, + defaultScopes = [ + 'openid', + 'offline_access', + 'profile', + 'email', + 'User.Read', + ], + } = options; + return OAuth2.create({ discoveryApi, oauthRequestApi, diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts index 403e8445d8..582526083b 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts @@ -70,14 +70,16 @@ export default class OAuth2 BackstageIdentityApi, SessionApi { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = [], - scopeTransform = x => x, - }: OAuth2CreateOptions) { + static create(options: OAuth2CreateOptions) { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + defaultScopes = [], + scopeTransform = x => x, + } = options; + const connector = new DefaultAuthConnector({ discoveryApi, environment, diff --git a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts index 465a124051..42c0ddd0ed 100644 --- a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts @@ -42,13 +42,15 @@ const OKTA_SCOPE_PREFIX: string = 'okta.'; * @public */ export default class OktaAuth { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - defaultScopes = ['openid', 'email', 'profile', 'offline_access'], - }: OAuthApiCreateOptions): typeof oktaAuthApiRef.T { + static create(options: OAuthApiCreateOptions): typeof oktaAuthApiRef.T { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + defaultScopes = ['openid', 'email', 'profile', 'offline_access'], + } = options; + return OAuth2.create({ discoveryApi, oauthRequestApi, diff --git a/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts b/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts index 93b9f6634c..9493d0809e 100644 --- a/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts @@ -57,12 +57,16 @@ const SCOPE_PREFIX: string = 'onelogin.'; * @public */ export default class OneLoginAuth { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - oauthRequestApi, - }: OneLoginAuthCreateOptions): typeof oneloginAuthApiRef.T { + static create( + options: OneLoginAuthCreateOptions, + ): typeof oneloginAuthApiRef.T { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + } = options; + return OAuth2.create({ discoveryApi, oauthRequestApi, diff --git a/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts index c1b70e963d..5988e81c48 100644 --- a/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -52,11 +52,13 @@ const DEFAULT_PROVIDER = { export default class SamlAuth implements ProfileInfoApi, BackstageIdentityApi, SessionApi { - static create({ - discoveryApi, - environment = 'development', - provider = DEFAULT_PROVIDER, - }: AuthApiCreateOptions) { + static create(options: AuthApiCreateOptions) { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + } = options; + const connector = new DirectAuthConnector({ discoveryApi, environment, diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index c1e021391e..cc7db32586 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -1982,10 +1982,7 @@ export const SidebarSpacer: React_2.ComponentType< >; // @public -export const SidebarSubmenu: ({ - title, - children, -}: PropsWithChildren) => JSX.Element; +export const SidebarSubmenu: (props: SidebarSubmenuProps) => JSX.Element; // @public export const SidebarSubmenuItem: ( diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx index 51324980fe..df671c7514 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx @@ -16,7 +16,7 @@ import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import clsx from 'clsx'; -import React, { PropsWithChildren, ReactNode, useContext } from 'react'; +import React, { ReactNode, useContext } from 'react'; import { SidebarItemWithSubmenuContext, sidebarConfig, @@ -83,16 +83,12 @@ export type SidebarSubmenuProps = { * * @public */ -export const SidebarSubmenu = ({ - title, - children, -}: PropsWithChildren) => { +export const SidebarSubmenu = (props: SidebarSubmenuProps) => { const { isOpen } = useContext(SidebarContext); const left = isOpen ? sidebarConfig.drawerWidthOpen : sidebarConfig.drawerWidthClosed; - const props = { left: left }; - const classes = useStyles(props)(); + const classes = useStyles({ left: left })(); const { isHoveredOn } = useContext(SidebarItemWithSubmenuContext); return ( @@ -102,9 +98,9 @@ export const SidebarSubmenu = ({ })} > - {title} + {props.title} - {children} + {props.children} ); }; diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 560071e1fa..f403594e34 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -43,10 +43,7 @@ export type AnalyticsApi = { export const analyticsApiRef: ApiRef; // @public -export const AnalyticsContext: ({ - attributes, - children, -}: { +export const AnalyticsContext: (options: { attributes: Partial; children: ReactNode; }) => JSX.Element; diff --git a/packages/core-plugin-api/src/analytics/AnalyticsContext.tsx b/packages/core-plugin-api/src/analytics/AnalyticsContext.tsx index bb2141d47f..075cdff3ff 100644 --- a/packages/core-plugin-api/src/analytics/AnalyticsContext.tsx +++ b/packages/core-plugin-api/src/analytics/AnalyticsContext.tsx @@ -62,13 +62,12 @@ export const useAnalyticsContext = (): AnalyticsContextValue => { * * @public */ -export const AnalyticsContext = ({ - attributes, - children, -}: { +export const AnalyticsContext = (options: { attributes: Partial; children: ReactNode; }) => { + const { attributes, children } = options; + const parentValues = useAnalyticsContext(); const combinedValue = { ...parentValues, diff --git a/packages/dev-utils/api-report.md b/packages/dev-utils/api-report.md index c10de7d529..e1cb93313f 100644 --- a/packages/dev-utils/api-report.md +++ b/packages/dev-utils/api-report.md @@ -44,11 +44,9 @@ export type DevAppPageOptions = { }; // @public (undocumented) -export const EntityGridItem: ({ - entity, - classes, - ...rest -}: Omit, 'container' | 'item'> & { - entity: Entity; -}) => JSX.Element; +export const EntityGridItem: ( + props: Omit & { + entity: Entity; + }, +) => JSX.Element; ``` diff --git a/packages/dev-utils/src/components/EntityGridItem/EntityGridItem.tsx b/packages/dev-utils/src/components/EntityGridItem/EntityGridItem.tsx index 55327f35e5..a66550a4d2 100644 --- a/packages/dev-utils/src/components/EntityGridItem/EntityGridItem.tsx +++ b/packages/dev-utils/src/components/EntityGridItem/EntityGridItem.tsx @@ -35,11 +35,10 @@ const useStyles = makeStyles(theme => ({ })); /** @public */ -export const EntityGridItem = ({ - entity, - classes, - ...rest -}: Omit & { entity: Entity }): JSX.Element => { +export const EntityGridItem = ( + props: Omit & { entity: Entity }, +): JSX.Element => { + const { entity, classes, ...rest } = props; const itemClasses = useStyles({ entity }); return ( diff --git a/packages/techdocs-common/api-report.md b/packages/techdocs-common/api-report.md index b2f9e40d0a..5c0a9f87f6 100644 --- a/packages/techdocs-common/api-report.md +++ b/packages/techdocs-common/api-report.md @@ -49,22 +49,6 @@ export type GeneratorBuilder = { get(entity: Entity): GeneratorBase; }; -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-with-invalid-optional-name) The @param should not include a JSDoc-style optional name; it must not be enclosed in '[ ]' brackets. -// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// Warning: (tsdoc-param-tag-with-invalid-optional-name) The @param should not include a JSDoc-style optional name; it must not be enclosed in '[ ]' brackets. -// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' -// Warning: (ae-missing-release-tag) "GeneratorRunOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type GeneratorRunOptions = { inputDir: string; @@ -82,10 +66,7 @@ export class Generators implements GeneratorBuilder { // (undocumented) static fromConfig( config: Config, - { - logger, - containerRunner, - }: { + options: { logger: Logger_2; containerRunner: ContainerRunner; }, @@ -185,13 +166,10 @@ export class Publisher { ): Promise; } -// Warning: (ae-missing-release-tag) "PublisherBase" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export interface PublisherBase { docsRouter(): express.Handler; fetchTechDocsMetadata(entityName: EntityName): Promise; - // Warning: (ae-forgotten-export) The symbol "ReadinessResponse" needs to be exported by the entry point index.d.ts getReadiness(): Promise; hasDocsBeenGenerated(entityName: Entity): Promise; // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag @@ -202,7 +180,6 @@ export interface PublisherBase { // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" // Warning: (ae-forgotten-export) The symbol "MigrateRequest" needs to be exported by the entry point index.d.ts migrateDocsCase?(migrateRequest: MigrateRequest): Promise; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (ae-forgotten-export) The symbol "PublishRequest" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "PublishResponse" needs to be exported by the entry point index.d.ts publish(request: PublishRequest): Promise; @@ -218,6 +195,11 @@ export type PublisherType = | 'azureBlobStorage' | 'openStackSwift'; +// @public +export type ReadinessResponse = { + isAvailable: boolean; +}; + // Warning: (ae-missing-release-tag) "RemoteProtocol" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -245,12 +227,7 @@ export interface TechDocsDocument extends IndexableDocument { // // @public (undocumented) export class TechdocsGenerator implements GeneratorBase { - constructor({ - logger, - containerRunner, - config, - scmIntegrations, - }: { + constructor(options: { logger: Logger_2; containerRunner: ContainerRunner; config: Config; @@ -260,26 +237,15 @@ export class TechdocsGenerator implements GeneratorBase { // (undocumented) static fromConfig( config: Config, - { - containerRunner, - logger, - }: { + options: { containerRunner: ContainerRunner; logger: Logger_2; }, ): TechdocsGenerator; // (undocumented) - run({ - inputDir, - outputDir, - parsedLocationAnnotation, - etag, - logger: childLogger, - logStream, - }: GeneratorRunOptions): Promise; + run(options: GeneratorRunOptions): Promise; } -// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (ae-missing-release-tag) "TechDocsMetadata" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -319,7 +285,7 @@ export class UrlPreparer implements PreparerBase { // Warnings were encountered during analysis: // -// src/stages/generate/types.d.ts:44:5 - (ae-forgotten-export) The symbol "SupportedGeneratorKey" needs to be exported by the entry point index.d.ts +// src/stages/generate/types.d.ts:45:5 - (ae-forgotten-export) The symbol "SupportedGeneratorKey" needs to be exported by the entry point index.d.ts // src/stages/prepare/types.d.ts:18:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/stages/prepare/types.d.ts:19:8 - (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters // src/stages/prepare/types.d.ts:21:33 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag diff --git a/packages/techdocs-common/src/stages/generate/generators.ts b/packages/techdocs-common/src/stages/generate/generators.ts index 3d1ad2b73b..de39d4e7c3 100644 --- a/packages/techdocs-common/src/stages/generate/generators.ts +++ b/packages/techdocs-common/src/stages/generate/generators.ts @@ -31,17 +31,11 @@ export class Generators implements GeneratorBuilder { static async fromConfig( config: Config, - { - logger, - containerRunner, - }: { logger: Logger; containerRunner: ContainerRunner }, + options: { logger: Logger; containerRunner: ContainerRunner }, ): Promise { const generators = new Generators(); - const techdocsGenerator = TechdocsGenerator.fromConfig(config, { - logger, - containerRunner, - }); + const techdocsGenerator = TechdocsGenerator.fromConfig(config, options); generators.register('techdocs', techdocsGenerator); return generators; diff --git a/packages/techdocs-common/src/stages/generate/techdocs.ts b/packages/techdocs-common/src/stages/generate/techdocs.ts index 8681736815..a6eb8d38d2 100644 --- a/packages/techdocs-common/src/stages/generate/techdocs.ts +++ b/packages/techdocs-common/src/stages/generate/techdocs.ts @@ -52,11 +52,9 @@ export class TechdocsGenerator implements GeneratorBase { static fromConfig( config: Config, - { - containerRunner, - logger, - }: { containerRunner: ContainerRunner; logger: Logger }, + options: { containerRunner: ContainerRunner; logger: Logger }, ) { + const { containerRunner, logger } = options; const scmIntegrations = ScmIntegrations.fromConfig(config); return new TechdocsGenerator({ logger, @@ -66,31 +64,28 @@ export class TechdocsGenerator implements GeneratorBase { }); } - constructor({ - logger, - containerRunner, - config, - scmIntegrations, - }: { + constructor(options: { logger: Logger; containerRunner: ContainerRunner; config: Config; scmIntegrations: ScmIntegrationRegistry; }) { - this.logger = logger; - this.options = readGeneratorConfig(config, logger); - this.containerRunner = containerRunner; - this.scmIntegrations = scmIntegrations; + this.logger = options.logger; + this.options = readGeneratorConfig(options.config, options.logger); + this.containerRunner = options.containerRunner; + this.scmIntegrations = options.scmIntegrations; } - public async run({ - inputDir, - outputDir, - parsedLocationAnnotation, - etag, - logger: childLogger, - logStream, - }: GeneratorRunOptions): Promise { + public async run(options: GeneratorRunOptions): Promise { + const { + inputDir, + outputDir, + parsedLocationAnnotation, + etag, + logger: childLogger, + logStream, + } = options; + // Do some updates to mkdocs.yml before generating docs e.g. adding repo_url const { path: mkdocsYmlPath, content } = await getMkdocsYml(inputDir); diff --git a/packages/techdocs-common/src/stages/generate/types.ts b/packages/techdocs-common/src/stages/generate/types.ts index 3559716be0..2a090d5584 100644 --- a/packages/techdocs-common/src/stages/generate/types.ts +++ b/packages/techdocs-common/src/stages/generate/types.ts @@ -34,12 +34,13 @@ export type GeneratorConfig = { /** * The values that the generator will receive. * - * @param {string} inputDir The directory of the uncompiled documentation, with the values from the frontend - * @param {string} outputDir Directory to store generated docs in. Usually - a newly created temporary directory. - * @param {ParsedLocationAnnotation} parsedLocationAnnotation backstage.io/techdocs-ref annotation of an entity - * @param {string} etag A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json. - * @param {Logger} [logger] A logger that forwards the messages to the caller to be displayed outside of the backend. - * @param {Writable} [logStream] A log stream that can send raw log messages to the caller to be displayed outside of the backend.. + * @public + * @param inputDir - The directory of the uncompiled documentation, with the values from the frontend + * @param outputDir - Directory to store generated docs in. Usually - a newly created temporary directory. + * @param parsedLocationAnnotation - backstage.io/techdocs-ref annotation of an entity + * @param etag - A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json. + * @param logger - A logger that forwards the messages to the caller to be displayed outside of the backend. + * @param logStream - A log stream that can send raw log messages to the caller to be displayed outside of the backend. */ export type GeneratorRunOptions = { inputDir: string; diff --git a/packages/techdocs-common/src/stages/publish/index.ts b/packages/techdocs-common/src/stages/publish/index.ts index b342f33287..083cf2b8ff 100644 --- a/packages/techdocs-common/src/stages/publish/index.ts +++ b/packages/techdocs-common/src/stages/publish/index.ts @@ -14,4 +14,9 @@ * limitations under the License. */ export { Publisher } from './publish'; -export type { PublisherBase, PublisherType, TechDocsMetadata } from './types'; +export type { + PublisherBase, + PublisherType, + TechDocsMetadata, + ReadinessResponse, +} from './types'; diff --git a/packages/techdocs-common/src/stages/publish/types.ts b/packages/techdocs-common/src/stages/publish/types.ts index 1fb301340b..b7415722d6 100644 --- a/packages/techdocs-common/src/stages/publish/types.ts +++ b/packages/techdocs-common/src/stages/publish/types.ts @@ -51,6 +51,8 @@ export type PublishResponse = { /** * Result for the validation check. + * + * @public */ export type ReadinessResponse = { /** If true, the publisher is able to interact with the backing storage. */ @@ -59,7 +61,7 @@ export type ReadinessResponse = { /** * Type to hold metadata found in techdocs_metadata.json and associated with each site - * @param etag ETag of the resource used to generate the site. Usually the latest commit sha of the source repository. + * @param etag - ETag of the resource used to generate the site. Usually the latest commit sha of the source repository. */ export type TechDocsMetadata = { site_name: string; @@ -86,6 +88,8 @@ export type MigrateRequest = { * Base class for a TechDocs publisher (e.g. Local, Google GCS Bucket, AWS S3, etc.) * The publisher handles publishing of the generated static files after the prepare and generate steps of TechDocs. * It also provides APIs to communicate with the storage service. + * + * @public */ export interface PublisherBase { /** @@ -99,8 +103,8 @@ export interface PublisherBase { /** * Store the generated static files onto a storage service (either local filesystem or external service). * - * @param request Object containing the entity from the service - * catalog, and the directory that contains the generated static files from TechDocs. + * @param request - Object containing the entity from the service + * catalog, and the directory that contains the generated static files from TechDocs. */ publish(request: PublishRequest): Promise; diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index 83c209122d..a01f306fab 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -89,23 +89,13 @@ export type LogFuncs = 'log' | 'warn' | 'error'; // @public export class MockAnalyticsApi implements AnalyticsApi { // (undocumented) - captureEvent({ - action, - subject, - value, - attributes, - context, - }: AnalyticsEvent): void; + captureEvent(event: AnalyticsEvent): void; // (undocumented) getEvents(): AnalyticsEvent[]; } // @public -export function mockBreakpoint({ - matches, -}: { - matches?: boolean | undefined; -}): void; +export function mockBreakpoint(options: { matches: boolean }): void; // @public export class MockErrorApi implements ErrorApi { @@ -178,10 +168,9 @@ export function setupRequestMockHandlers(worker: { export type SyncLogCollector = () => void; // @public -export const TestApiProvider: ({ - apis, - children, -}: TestApiProviderProps) => JSX.Element; +export const TestApiProvider: ( + props: TestApiProviderProps, +) => JSX.Element; // @public export type TestApiProviderProps = { diff --git a/packages/test-utils/src/testUtils/TestApiProvider.tsx b/packages/test-utils/src/testUtils/TestApiProvider.tsx index b1499b0cde..6e0be81d53 100644 --- a/packages/test-utils/src/testUtils/TestApiProvider.tsx +++ b/packages/test-utils/src/testUtils/TestApiProvider.tsx @@ -120,11 +120,13 @@ export class TestApiRegistry implements ApiHolder { * * @public **/ -export const TestApiProvider = ({ - apis, - children, -}: TestApiProviderProps) => { +export const TestApiProvider = ( + props: TestApiProviderProps, +) => { return ( - + ); }; diff --git a/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts b/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts index 3e2fc2a01c..6da225df1c 100644 --- a/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts +++ b/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts @@ -19,18 +19,15 @@ import { AnalyticsApi, AnalyticsEvent } from '@backstage/core-plugin-api'; /** * Mock implementation of {@link core-plugin-api#AnalyticsApi} with helpers to ensure that events are sent correctly. * Use getEvents in tests to verify captured events. + * * @public */ export class MockAnalyticsApi implements AnalyticsApi { private events: AnalyticsEvent[] = []; - captureEvent({ - action, - subject, - value, - attributes, - context, - }: AnalyticsEvent) { + captureEvent(event: AnalyticsEvent) { + const { action, subject, value, attributes, context } = event; + this.events.push({ action, subject, diff --git a/packages/test-utils/src/testUtils/mockBreakpoint.ts b/packages/test-utils/src/testUtils/mockBreakpoint.ts index 3bc285a81d..ee759c07a5 100644 --- a/packages/test-utils/src/testUtils/mockBreakpoint.ts +++ b/packages/test-utils/src/testUtils/mockBreakpoint.ts @@ -15,8 +15,8 @@ */ /** - * This is a mocking method suggested in the Jest Doc's, as it is not implemented in JSDOM yet. - * It can be used to mock values when the MUI `useMediaQuery` hook if it is used in a tested component. + * This is a mocking method suggested in the Jest docs, as it is not implemented in JSDOM yet. + * It can be used to mock values for the MUI `useMediaQuery` hook if it is used in a tested component. * * For issues checkout the documentation: * https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom @@ -26,11 +26,11 @@ * * @public */ -export default function mockBreakpoint({ matches = false }) { +export default function mockBreakpoint(options: { matches: boolean }) { Object.defineProperty(window, 'matchMedia', { writable: true, value: jest.fn().mockImplementation(query => ({ - matches: matches, + matches: options.matches ?? false, media: query, onchange: null, addListener: jest.fn(), // deprecated diff --git a/plugins/analytics-module-ga/api-report.md b/plugins/analytics-module-ga/api-report.md index 7ca3b1646f..b0f3b736c3 100644 --- a/plugins/analytics-module-ga/api-report.md +++ b/plugins/analytics-module-ga/api-report.md @@ -17,13 +17,7 @@ export const analyticsModuleGA: BackstagePlugin<{}, {}>; // // @public export class GoogleAnalytics implements AnalyticsApi { - captureEvent({ - context, - action, - subject, - value, - attributes, - }: AnalyticsEvent): void; + captureEvent(event: AnalyticsEvent): void; static fromConfig(config: Config): GoogleAnalytics; } diff --git a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts index 23da7c8589..28202607be 100644 --- a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts +++ b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts @@ -39,19 +39,15 @@ export class GoogleAnalytics implements AnalyticsApi { /** * Instantiate the implementation and initialize ReactGA. */ - private constructor({ - cdmConfig, - trackingId, - scriptSrc, - testMode, - debug, - }: { + private constructor(options: { cdmConfig: CustomDimensionOrMetricConfig[]; trackingId: string; scriptSrc?: string; testMode: boolean; debug: boolean; }) { + const { cdmConfig, trackingId, scriptSrc, testMode, debug } = options; + this.cdmConfig = cdmConfig; // Initialize Google Analytics. @@ -102,13 +98,8 @@ export class GoogleAnalytics implements AnalyticsApi { * pageview and the rest as custom events. All custom dimensions/metrics are * applied as they should be (set on pageview, merged object on events). */ - captureEvent({ - context, - action, - subject, - value, - attributes, - }: AnalyticsEvent) { + captureEvent(event: AnalyticsEvent) { + const { context, action, subject, value, attributes } = event; const customMetadata = this.getCustomDimensionMetrics(context, attributes); if (action === 'navigate' && context.extension === 'App') { diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 46d8b03cbb..2037bd240a 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -185,10 +185,7 @@ export class CatalogIdentityClient { // Warning: (ae-forgotten-export) The symbol "UserQuery" needs to be exported by the entry point index.d.ts findUser(query: UserQuery): Promise; // Warning: (ae-forgotten-export) The symbol "MemberClaimQuery" needs to be exported by the entry point index.d.ts - resolveCatalogMembership({ - entityRefs, - logger, - }: MemberClaimQuery): Promise; + resolveCatalogMembership(query: MemberClaimQuery): Promise; } // Warning: (ae-missing-release-tag) "createAtlassianProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -270,13 +267,7 @@ export function createOriginFilter(config: Config): (origin: string) => boolean; // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function createRouter({ - logger, - config, - discovery, - database, - providerFactories, -}: RouterOptions): Promise; +export function createRouter(options: RouterOptions): Promise; // @public (undocumented) export const createSamlProvider: ( diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts index 02fe9818cb..a1f915d152 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.ts @@ -84,10 +84,8 @@ export class CatalogIdentityClient { * * Returns a superset of the entity names that can be passed directly to `issueToken` as `ent`. */ - async resolveCatalogMembership({ - entityRefs, - logger, - }: MemberClaimQuery): Promise { + async resolveCatalogMembership(query: MemberClaimQuery): Promise { + const { entityRefs, logger } = query; const resolvedEntityRefs = entityRefs .map((ref: string) => { try { diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 4aa9ed8778..6a96d421da 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -44,13 +44,10 @@ export interface RouterOptions { providerFactories?: ProviderFactories; } -export async function createRouter({ - logger, - config, - discovery, - database, - providerFactories, -}: RouterOptions): Promise { +export async function createRouter( + options: RouterOptions, +): Promise { + const { logger, config, discovery, database, providerFactories } = options; const router = Router(); const appUrl = config.getString('app.baseUrl'); diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 43eb8d61b7..f773905ca0 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -752,13 +752,7 @@ export type DbPageInfo = // // @public (undocumented) export class DefaultCatalogCollator implements DocumentCollator { - constructor({ - discovery, - locationTemplate, - filter, - catalogClient, - tokenManager, - }: { + constructor(options: { discovery: PluginEndpointDiscovery; tokenManager: TokenManager; locationTemplate?: string; diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts index 4c6342c6c6..ebad95e72f 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts @@ -56,19 +56,16 @@ export class DefaultCatalogCollator implements DocumentCollator { }); } - constructor({ - discovery, - locationTemplate, - filter, - catalogClient, - tokenManager, - }: { + constructor(options: { discovery: PluginEndpointDiscovery; tokenManager: TokenManager; locationTemplate?: string; filter?: CatalogEntitiesRequest['filter']; catalogClient?: CatalogApi; }) { + const { discovery, locationTemplate, filter, catalogClient, tokenManager } = + options; + this.discovery = discovery; this.locationTemplate = locationTemplate || '/catalog/:namespace/:kind/:name'; diff --git a/plugins/config-schema/api-report.md b/plugins/config-schema/api-report.md index efeb1646eb..f3b620738f 100644 --- a/plugins/config-schema/api-report.md +++ b/plugins/config-schema/api-report.md @@ -41,11 +41,9 @@ export const configSchemaPlugin: BackstagePlugin< {} >; -// Warning: (ae-missing-release-tag) "StaticSchemaLoader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class StaticSchemaLoader implements ConfigSchemaApi { - constructor({ url }?: { url?: string }); + constructor(options?: { url?: string }); // (undocumented) schema$(): Observable; } diff --git a/plugins/config-schema/src/api/StaticSchemaLoader.ts b/plugins/config-schema/src/api/StaticSchemaLoader.ts index 71eae63376..5fc979b76d 100644 --- a/plugins/config-schema/src/api/StaticSchemaLoader.ts +++ b/plugins/config-schema/src/api/StaticSchemaLoader.ts @@ -24,12 +24,14 @@ const DEFAULT_URL = 'config-schema.json'; /** * A ConfigSchemaApi implementation that loads the configuration from a URL. + * + * @public */ export class StaticSchemaLoader implements ConfigSchemaApi { private readonly url: string; - constructor({ url = DEFAULT_URL }: { url?: string } = {}) { - this.url = url; + constructor(options: { url?: string } = {}) { + this.url = options?.url ?? DEFAULT_URL; } schema$(): Observable { diff --git a/plugins/github-deployments/api-report.md b/plugins/github-deployments/api-report.md index a8fca185e5..36680cb020 100644 --- a/plugins/github-deployments/api-report.md +++ b/plugins/github-deployments/api-report.md @@ -38,11 +38,7 @@ function createStatusColumn(): TableColumn; // Warning: (ae-missing-release-tag) "EntityGithubDeploymentsCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const EntityGithubDeploymentsCard: ({ - last, - lastStatuses, - columns, -}: { +export const EntityGithubDeploymentsCard: (props: { last?: number | undefined; lastStatuses?: number | undefined; columns?: TableColumn[] | undefined; @@ -58,12 +54,9 @@ export const githubDeploymentsPlugin: BackstagePlugin<{}, {}>; // Warning: (ae-missing-release-tag) "GithubDeploymentsTable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function GithubDeploymentsTable({ - deployments, - isLoading, - reload, - columns, -}: GithubDeploymentsTableProps): JSX.Element; +export function GithubDeploymentsTable( + props: GithubDeploymentsTableProps, +): JSX.Element; // @public (undocumented) export namespace GithubDeploymentsTable { @@ -78,7 +71,7 @@ export namespace GithubDeploymentsTable { // Warning: (ae-missing-release-tag) "GithubStateIndicator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const GithubStateIndicator: ({ state }: { state: string }) => JSX.Element; +const GithubStateIndicator: (props: { state: string }) => JSX.Element; // Warning: (ae-missing-release-tag) "isGithubDeploymentsAvailable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx index c01cd98a45..c1c33607a1 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.tsx @@ -80,15 +80,12 @@ const GithubDeploymentsComponent = ({ ); }; -export const GithubDeploymentsCard = ({ - last, - lastStatuses, - columns, -}: { +export const GithubDeploymentsCard = (props: { last?: number; lastStatuses?: number; columns?: TableColumn[]; }) => { + const { last, lastStatuses, columns } = props; const { entity } = useEntity(); const [host] = [ entity?.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION], diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx index 2de7cdf419..2c48f3eef1 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/GithubDeploymentsTable.tsx @@ -36,12 +36,8 @@ type GithubDeploymentsTableProps = { columns: TableColumn[]; }; -export function GithubDeploymentsTable({ - deployments, - isLoading, - reload, - columns, -}: GithubDeploymentsTableProps) { +export function GithubDeploymentsTable(props: GithubDeploymentsTableProps) { + const { deployments, isLoading, reload, columns } = props; const classes = useStyles(); return ( diff --git a/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx b/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx index e892b12427..9a720c0e75 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsTable/columns.tsx @@ -27,8 +27,8 @@ import { Link, } from '@backstage/core-components'; -export const GithubStateIndicator = ({ state }: { state: string }) => { - switch (state) { +export const GithubStateIndicator = (props: { state: string }) => { + switch (props.state) { case 'PENDING': return ; case 'IN_PROGRESS': diff --git a/plugins/pagerduty/api-report.md b/plugins/pagerduty/api-report.md index 689edae1e0..bd26531da6 100644 --- a/plugins/pagerduty/api-report.md +++ b/plugins/pagerduty/api-report.md @@ -10,7 +10,7 @@ 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 { PropsWithChildren } from 'react'; +import { ReactNode } from 'react'; // Warning: (ae-missing-release-tag) "EntityPagerDutyCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -65,12 +65,7 @@ export class PagerDutyClient implements PagerDutyApi { // Warning: (ae-forgotten-export) The symbol "TriggerAlarmRequest" needs to be exported by the entry point index.d.ts // // (undocumented) - triggerAlarm({ - integrationKey, - source, - description, - userName, - }: TriggerAlarmRequest): Promise; + triggerAlarm(request: TriggerAlarmRequest): Promise; } // Warning: (ae-missing-release-tag) "pagerDutyPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -84,9 +79,7 @@ export { pagerDutyPlugin as plugin }; // Warning: (ae-missing-release-tag) "TriggerButton" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function TriggerButton({ - children, -}: PropsWithChildren): JSX.Element; +export function TriggerButton(props: TriggerButtonProps): JSX.Element; // Warning: (ae-missing-release-tag) "UnauthorizedError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index c006403fe7..65ff83e7ba 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -91,12 +91,9 @@ export class PagerDutyClient implements PagerDutyApi { return oncalls; } - triggerAlarm({ - integrationKey, - source, - description, - userName, - }: TriggerAlarmRequest): Promise { + triggerAlarm(request: TriggerAlarmRequest): Promise { + const { integrationKey, source, description, userName } = request; + const body = JSON.stringify({ event_action: 'trigger', routing_key: integrationKey, diff --git a/plugins/pagerduty/src/components/TriggerButton/index.tsx b/plugins/pagerduty/src/components/TriggerButton/index.tsx index 93897c77be..6f83effcf4 100644 --- a/plugins/pagerduty/src/components/TriggerButton/index.tsx +++ b/plugins/pagerduty/src/components/TriggerButton/index.tsx @@ -13,14 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useCallback, PropsWithChildren, useState } from 'react'; +import React, { useCallback, ReactNode, useState } from 'react'; import { makeStyles, Button } from '@material-ui/core'; import { BackstageTheme } from '@backstage/theme'; import { usePagerdutyEntity } from '../../hooks'; import { TriggerDialog } from '../TriggerDialog'; -export type TriggerButtonProps = {}; +export type TriggerButtonProps = { + children?: ReactNode; +}; const useStyles = makeStyles(theme => ({ buttonStyle: { @@ -32,9 +34,7 @@ const useStyles = makeStyles(theme => ({ }, })); -export function TriggerButton({ - children, -}: PropsWithChildren) { +export function TriggerButton(props: TriggerButtonProps) { const { buttonStyle } = useStyles(); const { integrationKey } = usePagerdutyEntity(); const [dialogShown, setDialogShown] = useState(false); @@ -56,7 +56,7 @@ export function TriggerButton({ disabled={disabled} > {integrationKey - ? children ?? 'Create Incident' + ? props.children ?? 'Create Incident' : 'Missing integration key'} {integrationKey && ( diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 75c45a9250..adc1cbc3c6 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -83,11 +83,7 @@ export const createConditionTransformer: < ) => ConditionTransformer; // @public -export const createPermissionIntegrationRouter: ({ - resourceType, - rules, - getResource, -}: { +export const createPermissionIntegrationRouter: (options: { resourceType: string; rules: PermissionRule[]; getResource: (resourceRef: string) => Promise; diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index 70b72fef7e..b8d3e02a81 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -116,17 +116,15 @@ const applyConditions = ( * This is used to construct the `createPermissionIntegrationRouter`, a function to add an * authorization route to your backend plugin. This route will be called by the `permission-backend` * when authorization conditions relating to this plugin need to be evaluated. + * * @public */ -export const createPermissionIntegrationRouter = ({ - resourceType, - rules, - getResource, -}: { +export const createPermissionIntegrationRouter = (options: { resourceType: string; rules: PermissionRule[]; getResource: (resourceRef: string) => Promise; }): Router => { + const { resourceType, rules, getResource } = options; const router = Router(); const getRule = createGetRule(rules); diff --git a/plugins/permission-react/api-report.md b/plugins/permission-react/api-report.md index 65f93de8a0..c10317495a 100644 --- a/plugins/permission-react/api-report.md +++ b/plugins/permission-react/api-report.md @@ -6,12 +6,13 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { AuthorizeRequest } from '@backstage/plugin-permission-common'; import { AuthorizeResponse } from '@backstage/plugin-permission-common'; +import { ComponentProps } from 'react'; import { Config } from '@backstage/config'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { IdentityApi } from '@backstage/core-plugin-api'; import { Permission } from '@backstage/plugin-permission-common'; -import { default as React_2 } from 'react'; -import { RouteProps } from 'react-router'; +import { ReactElement } from 'react'; +import { Route } from 'react-router'; // @public (undocumented) export type AsyncPermissionResult = { @@ -25,11 +26,7 @@ export class IdentityPermissionApi implements PermissionApi { // (undocumented) authorize(request: AuthorizeRequest): Promise; // (undocumented) - static create({ - configApi, - discoveryApi, - identityApi, - }: { + static create(options: { configApi: Config; discoveryApi: DiscoveryApi; identityApi: IdentityApi; @@ -45,19 +42,13 @@ export type PermissionApi = { export const permissionApiRef: ApiRef; // @public -export const PermissionedRoute: ({ - permission, - resourceRef, - errorComponent, - ...props -}: RouteProps & { - permission: Permission; - resourceRef?: string | undefined; - errorComponent?: - | React_2.ReactElement> - | null - | undefined; -}) => JSX.Element; +export const PermissionedRoute: ( + props: ComponentProps & { + permission: Permission; + resourceRef?: string; + errorComponent?: ReactElement | null; + }, +) => JSX.Element; // @public export const usePermission: ( diff --git a/plugins/permission-react/src/apis/IdentityPermissionApi.ts b/plugins/permission-react/src/apis/IdentityPermissionApi.ts index a68060a61d..19de564f84 100644 --- a/plugins/permission-react/src/apis/IdentityPermissionApi.ts +++ b/plugins/permission-react/src/apis/IdentityPermissionApi.ts @@ -34,15 +34,12 @@ export class IdentityPermissionApi implements PermissionApi { private readonly identityApi: IdentityApi, ) {} - static create({ - configApi, - discoveryApi, - identityApi, - }: { + static create(options: { configApi: Config; discoveryApi: DiscoveryApi; identityApi: IdentityApi; }) { + const { configApi, discoveryApi, identityApi } = options; const permissionClient = new PermissionClient({ discoveryApi, configApi }); return new IdentityPermissionApi(permissionClient, identityApi); } diff --git a/plugins/permission-react/src/components/PermissionedRoute.tsx b/plugins/permission-react/src/components/PermissionedRoute.tsx index 1a3668205b..76017308aa 100644 --- a/plugins/permission-react/src/components/PermissionedRoute.tsx +++ b/plugins/permission-react/src/components/PermissionedRoute.tsx @@ -21,20 +21,19 @@ import { usePermission } from '../hooks'; import { Permission } from '@backstage/plugin-permission-common'; /** - * Returns a React Router Route which only renders the element when authorized. If unathorized, the Route will render a + * Returns a React Router Route which only renders the element when authorized. If unauthorized, the Route will render a * NotFoundErrorPage (see {@link @backstage/core-app-api#AppComponents}). + * * @public */ -export const PermissionedRoute = ({ - permission, - resourceRef, - errorComponent, - ...props -}: ComponentProps & { - permission: Permission; - resourceRef?: string; - errorComponent?: ReactElement | null; -}) => { +export const PermissionedRoute = ( + props: ComponentProps & { + permission: Permission; + resourceRef?: string; + errorComponent?: ReactElement | null; + }, +) => { + const { permission, resourceRef, errorComponent, ...otherProps } = props; const permissionResult = usePermission(permission, resourceRef); const app = useApp(); const { NotFoundErrorPage } = app.getComponents(); @@ -48,5 +47,5 @@ export const PermissionedRoute = ({ shownElement = props.element; } - return ; + return ; }; diff --git a/plugins/search-backend-module-elasticsearch/api-report.md b/plugins/search-backend-module-elasticsearch/api-report.md index da1f7d4804..1e798119c3 100644 --- a/plugins/search-backend-module-elasticsearch/api-report.md +++ b/plugins/search-backend-module-elasticsearch/api-report.md @@ -11,8 +11,6 @@ import { SearchEngine } from '@backstage/search-common'; import { SearchQuery } from '@backstage/search-common'; import { SearchResultSet } from '@backstage/search-common'; -// Warning: (ae-missing-release-tag) "ElasticSearchSearchEngine" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export class ElasticSearchSearchEngine implements SearchEngine { constructor( @@ -24,12 +22,9 @@ export class ElasticSearchSearchEngine implements SearchEngine { // Warning: (ae-forgotten-export) The symbol "ElasticSearchOptions" needs to be exported by the entry point index.d.ts // // (undocumented) - static fromConfig({ - logger, - config, - aliasPostfix, - indexPrefix, - }: ElasticSearchOptions): Promise; + static fromConfig( + options: ElasticSearchOptions, + ): Promise; // (undocumented) index(type: string, documents: IndexableDocument[]): Promise; // (undocumented) @@ -41,11 +36,6 @@ export class ElasticSearchSearchEngine implements SearchEngine { // Warning: (ae-forgotten-export) The symbol "ConcreteElasticSearchQuery" needs to be exported by the entry point index.d.ts // // (undocumented) - protected translator({ - term, - filters, - types, - pageCursor, - }: SearchQuery): ConcreteElasticSearchQuery; + protected translator(query: SearchQuery): ConcreteElasticSearchQuery; } ``` diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index 1a41099c9c..7912c691b3 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -64,6 +64,9 @@ function isBlank(str: string) { return (isEmpty(str) && !isNumber(str)) || nan(str); } +/** + * @public + */ export class ElasticSearchSearchEngine implements SearchEngine { constructor( private readonly elasticSearchClient: Client, @@ -72,12 +75,14 @@ export class ElasticSearchSearchEngine implements SearchEngine { private readonly logger: Logger, ) {} - static async fromConfig({ - logger, - config, - aliasPostfix = `search`, - indexPrefix = ``, - }: ElasticSearchOptions) { + static async fromConfig(options: ElasticSearchOptions) { + const { + logger, + config, + aliasPostfix = `search`, + indexPrefix = ``, + } = options; + return new ElasticSearchSearchEngine( await ElasticSearchSearchEngine.constructElasticSearchClient( logger, @@ -164,12 +169,9 @@ export class ElasticSearchSearchEngine implements SearchEngine { }); } - protected translator({ - term, - filters = {}, - types, - pageCursor, - }: SearchQuery): ConcreteElasticSearchQuery { + protected translator(query: SearchQuery): ConcreteElasticSearchQuery { + const { term, filters = {}, types, pageCursor } = query; + const filter = Object.entries(filters) .filter(([_, value]) => Boolean(value)) .map(([key, value]: [key: string, value: any]) => { @@ -190,7 +192,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { 'Failed to add filters to query. Unrecognized filter type', ); }); - const query = isBlank(term) + const esbQuery = isBlank(term) ? esb.matchAllQuery() : esb .multiMatchQuery(['*'], term) @@ -202,7 +204,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { return { elasticSearchQuery: esb .requestBodySearch() - .query(esb.boolQuery().filter(filter).must([query])) + .query(esb.boolQuery().filter(filter).must([esbQuery])) .from(page * pageSize) .size(pageSize) .toJSON(), diff --git a/plugins/search-backend-module-pg/api-report.md b/plugins/search-backend-module-pg/api-report.md index 7a171127a3..e2edfc291a 100644 --- a/plugins/search-backend-module-pg/api-report.md +++ b/plugins/search-backend-module-pg/api-report.md @@ -77,9 +77,7 @@ export interface DatabaseStore { export class PgSearchEngine implements SearchEngine { constructor(databaseStore: DatabaseStore); // (undocumented) - static from({ - database, - }: { + static from(options: { database: PluginDatabaseManager; }): Promise; // (undocumented) diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts index a032dd783e..6fd6c571a8 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts @@ -35,13 +35,11 @@ export type ConcretePgSearchQuery = { export class PgSearchEngine implements SearchEngine { constructor(private readonly databaseStore: DatabaseStore) {} - static async from({ - database, - }: { + static async from(options: { database: PluginDatabaseManager; }): Promise { return new PgSearchEngine( - await DatabaseDocumentStore.create(await database.getClient()), + await DatabaseDocumentStore.create(await options.database.getClient()), ); } diff --git a/plugins/search-backend/api-report.md b/plugins/search-backend/api-report.md index f2479610fe..cac97bd725 100644 --- a/plugins/search-backend/api-report.md +++ b/plugins/search-backend/api-report.md @@ -7,12 +7,16 @@ import express from 'express'; import { Logger as Logger_2 } from 'winston'; import { SearchEngine } from '@backstage/plugin-search-backend-node'; -// 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) // // @public (undocumented) -export function createRouter({ - engine, - logger, -}: RouterOptions): Promise; +export function createRouter(options: RouterOptions): Promise; + +// 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 type RouterOptions = { + engine: SearchEngine; + logger: Logger_2; +}; ``` diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index 6df39ca1eb..5bd99988a7 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -20,15 +20,15 @@ import { Logger } from 'winston'; import { SearchQuery, SearchResultSet } from '@backstage/search-common'; import { SearchEngine } from '@backstage/plugin-search-backend-node'; -type RouterOptions = { +export type RouterOptions = { engine: SearchEngine; logger: Logger; }; -export async function createRouter({ - engine, - logger, -}: RouterOptions): Promise { +export async function createRouter( + options: RouterOptions, +): Promise { + const { engine, logger } = options; const router = Router(); router.get( '/query', diff --git a/plugins/techdocs-backend/api-report.md b/plugins/techdocs-backend/api-report.md index 9736555687..b241917bc0 100644 --- a/plugins/techdocs-backend/api-report.md +++ b/plugins/techdocs-backend/api-report.md @@ -28,15 +28,7 @@ export function createRouter(options: RouterOptions): Promise; // @public (undocumented) export class DefaultTechDocsCollator implements DocumentCollator { // @deprecated - constructor({ - discovery, - locationTemplate, - logger, - catalogClient, - tokenManager, - parallelismLimit, - legacyPathCasing, - }: TechDocsCollatorOptions); + constructor(options: TechDocsCollatorOptions); // (undocumented) protected applyArgsToFormat( format: string, diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts index 283d0264e4..fa8a1156fe 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts @@ -63,24 +63,17 @@ export class DefaultTechDocsCollator implements DocumentCollator { /** * @deprecated use static fromConfig method instead. */ - constructor({ - discovery, - locationTemplate, - logger, - catalogClient, - tokenManager, - parallelismLimit = 10, - legacyPathCasing = false, - }: TechDocsCollatorOptions) { - this.discovery = discovery; + constructor(options: TechDocsCollatorOptions) { + this.discovery = options.discovery; this.locationTemplate = - locationTemplate || '/docs/:namespace/:kind/:name/:path'; - this.logger = logger; + options.locationTemplate || '/docs/:namespace/:kind/:name/:path'; + this.logger = options.logger; this.catalogClient = - catalogClient || new CatalogClient({ discoveryApi: discovery }); - this.parallelismLimit = parallelismLimit; - this.legacyPathCasing = legacyPathCasing; - this.tokenManager = tokenManager; + 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: TechDocsCollatorOptions) { diff --git a/plugins/todo-backend/api-report.md b/plugins/todo-backend/api-report.md index f989755a62..00e243d1a2 100644 --- a/plugins/todo-backend/api-report.md +++ b/plugins/todo-backend/api-report.md @@ -112,7 +112,7 @@ export class TodoScmReader implements TodoReader { options: Omit, ): TodoScmReader; // (undocumented) - readTodos({ url }: ReadTodosOptions): Promise; + readTodos(options: ReadTodosOptions): Promise; } // Warning: (ae-missing-release-tag) "TodoService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index 8442983637..804dd1146d 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -76,7 +76,8 @@ export class TodoScmReader implements TodoReader { this.integrations = options.integrations; } - async readTodos({ url }: ReadTodosOptions): Promise { + async readTodos(options: ReadTodosOptions): Promise { + const { url } = options; const inFlightRead = this.inFlightReads.get(url); if (inFlightRead) { return inFlightRead.then(read => read.result); @@ -101,9 +102,10 @@ export class TodoScmReader implements TodoReader { } private async doReadTodos( - { url }: ReadTodosOptions, + options: ReadTodosOptions, etag?: string, ): Promise { + const { url } = options; const tree = await this.reader.readTree(url, { etag, filter(filePath, info) { diff --git a/plugins/todo/api-report.md b/plugins/todo/api-report.md index e0fdb8ba58..0784f5d56e 100644 --- a/plugins/todo/api-report.md +++ b/plugins/todo/api-report.md @@ -26,13 +26,7 @@ export const todoApiRef: ApiRef; export class TodoClient implements TodoApi { constructor(options: TodoClientOptions); // (undocumented) - listTodos({ - entity, - offset, - limit, - orderBy, - filters, - }: TodoListOptions): Promise; + listTodos(options: TodoListOptions): Promise; } // @public diff --git a/plugins/todo/src/api/TodoClient.ts b/plugins/todo/src/api/TodoClient.ts index f75e4b6954..7e4e4c8f79 100644 --- a/plugins/todo/src/api/TodoClient.ts +++ b/plugins/todo/src/api/TodoClient.ts @@ -43,13 +43,8 @@ export class TodoClient implements TodoApi { this.identityApi = options.identityApi; } - async listTodos({ - entity, - offset, - limit, - orderBy, - filters, - }: TodoListOptions): Promise { + async listTodos(options: TodoListOptions): Promise { + const { entity, offset, limit, orderBy, filters } = options; const baseUrl = await this.discoveryApi.getBaseUrl('todo'); const token = await this.identityApi.getIdToken(); From a07e0f5f06bdf835b0e7139592aa4abaaf3f2026 Mon Sep 17 00:00:00 2001 From: Radoslaw Wielonski Date: Wed, 8 Dec 2021 13:15:25 +0100 Subject: [PATCH 069/116] docs: update documentation of setErrorHandler method Signed-off-by: Radoslaw Wielonski --- packages/backend-common/src/service/types.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/backend-common/src/service/types.ts b/packages/backend-common/src/service/types.ts index 3ec4c7aa0e..07075b4d16 100644 --- a/packages/backend-common/src/service/types.ts +++ b/packages/backend-common/src/service/types.ts @@ -99,9 +99,10 @@ export type ServiceBuilder = { ): ServiceBuilder; /** - * Set the error handler + * Sets an additional errorHandler to run before the defaultErrorHandler. * - * If no handler is given the default one is used + * If we want to use only custom errorHandler without defaultErrorHandler we need to + * disable the defaultErrorHandler by invoking disableDefaultErrorHandler() * * @param errorHandler - an error handler */ From 5632fa9eb362035de013dea3d1e9889fb6d12a43 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Dec 2021 16:58:16 +0100 Subject: [PATCH 070/116] scaffolder: fix dev setup Signed-off-by: Patrik Oldsberg --- plugins/scaffolder/dev/index.tsx | 17 ++++++++++++++++- plugins/scaffolder/package.json | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder/dev/index.tsx b/plugins/scaffolder/dev/index.tsx index 3a749792f1..f7c6aa62fc 100644 --- a/plugins/scaffolder/dev/index.tsx +++ b/plugins/scaffolder/dev/index.tsx @@ -17,7 +17,11 @@ import { CatalogClient } from '@backstage/catalog-client'; import { createDevApp } from '@backstage/dev-utils'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { + catalogApiRef, + starredEntitiesApiRef, + DefaultStarredEntitiesApi, +} from '@backstage/plugin-catalog-react'; import React from 'react'; import { scaffolderApiRef, ScaffolderClient } from '../src'; import { ScaffolderPage } from '../src/plugin'; @@ -25,14 +29,25 @@ import { configApiRef, discoveryApiRef, identityApiRef, + storageApiRef, } from '@backstage/core-plugin-api'; +import { CatalogEntityPage } from '@backstage/plugin-catalog'; createDevApp() + .addPage({ + path: '/catalog/:kind/:namespace/:name', + element: , + }) .registerApi({ api: catalogApiRef, deps: { discoveryApi: discoveryApiRef }, factory: ({ discoveryApi }) => new CatalogClient({ discoveryApi }), }) + .registerApi({ + api: starredEntitiesApiRef, + deps: { storageApi: storageApiRef }, + factory: ({ storageApi }) => new DefaultStarredEntitiesApi({ storageApi }), + }) .registerApi({ api: scaffolderApiRef, deps: { diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 35b14dffa1..ef2fa32974 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -67,6 +67,7 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { + "@backstage/plugin-catalog": "^0.7.3", "@backstage/cli": "^0.10.0", "@backstage/core-app-api": "^0.1.24", "@backstage/dev-utils": "^0.2.13", From d94abcab7d64076941449232e22715e0d5e84d0d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Dec 2021 13:48:39 +0100 Subject: [PATCH 071/116] core-components: added AnsiProcessor for LogViewer Signed-off-by: Patrik Oldsberg --- packages/core-components/package.json | 2 + .../LogViewer/AnsiProcessor.test.ts | 197 ++++++++++++++++++ .../src/components/LogViewer/AnsiProcessor.ts | 177 ++++++++++++++++ yarn.lock | 17 +- 4 files changed, 388 insertions(+), 5 deletions(-) create mode 100644 packages/core-components/src/components/LogViewer/AnsiProcessor.test.ts create mode 100644 packages/core-components/src/components/LogViewer/AnsiProcessor.ts diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 1b04173c40..8d24b419d7 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -39,6 +39,7 @@ "@material-ui/lab": "4.0.0-alpha.57", "@types/react-sparklines": "^1.7.0", "@types/react-text-truncate": "^0.14.0", + "ansi-regex": "^5.0.1", "classnames": "^2.2.6", "clsx": "^1.1.0", "d3-selection": "^3.0.0", @@ -81,6 +82,7 @@ "@types/d3-selection": "^3.0.1", "@types/d3-shape": "^3.0.1", "@types/d3-zoom": "^3.0.1", + "@types/ansi-regex": "^5.0.0", "@types/dagre": "^0.7.44", "@types/google-protobuf": "^3.7.2", "@types/jest": "^26.0.7", diff --git a/packages/core-components/src/components/LogViewer/AnsiProcessor.test.ts b/packages/core-components/src/components/LogViewer/AnsiProcessor.test.ts new file mode 100644 index 0000000000..b2ceee78b0 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/AnsiProcessor.test.ts @@ -0,0 +1,197 @@ +/* + * 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 { AnsiProcessor } from './AnsiProcessor'; + +describe('AnsiProcessor', () => { + it('should process a single line', () => { + const processor = new AnsiProcessor(); + expect(processor.process('foo\x1b[31mbar\x1b[39mbaz')).toEqual([ + [ + { + text: 'foo', + modifiers: {}, + }, + { + text: 'bar', + modifiers: { foreground: 'red' }, + }, + { + text: 'baz', + modifiers: {}, + }, + ], + ]); + + expect(processor.process(`foo bar: baz`)).toEqual([ + [ + { + text: 'foo ', + modifiers: {}, + }, + { + text: 'bar', + modifiers: { foreground: 'green' }, + }, + { + text: ': baz', + modifiers: {}, + }, + ], + ]); + }); + + it('should process multiple lines', () => { + const processor = new AnsiProcessor(); + expect( + processor.process(` +a\x1b[34mb\x1b[39mc +x\x1b[44my\x1b[49mz +`), + ).toEqual([ + [{ text: '', modifiers: {} }], + [ + { + text: 'a', + modifiers: {}, + }, + { + text: 'b', + modifiers: { foreground: 'blue' }, + }, + { + text: 'c', + modifiers: {}, + }, + ], + [ + { + text: 'x', + modifiers: {}, + }, + { + text: 'y', + modifiers: { background: 'blue' }, + }, + { + text: 'z', + modifiers: {}, + }, + ], + [{ text: '', modifiers: {} }], + ]); + }); + + it('should carry state across lines', () => { + const processor = new AnsiProcessor(); + expect( + processor.process(` +a\x1b[45mb\x1b[35mc +x\x1b[39my\x1b[49mz`), + ).toEqual([ + [{ text: '', modifiers: {} }], + [ + { + text: 'a', + modifiers: {}, + }, + { + text: 'b', + modifiers: { background: 'magenta' }, + }, + { + text: 'c', + modifiers: { foreground: 'magenta', background: 'magenta' }, + }, + ], + [ + { + text: 'x', + modifiers: { foreground: 'magenta', background: 'magenta' }, + }, + { + text: 'y', + modifiers: { background: 'magenta' }, + }, + { + text: 'z', + modifiers: {}, + }, + ], + ]); + }); + + it('should carry forward state when appending lines', () => { + const processor = new AnsiProcessor(); + const out1 = processor.process(` +a\x1b[36mb\x1b[3mc`); + expect(out1).toEqual([ + [{ text: '', modifiers: {} }], + [ + { + text: 'a', + modifiers: {}, + }, + { + text: 'b', + modifiers: { foreground: 'cyan' }, + }, + { + text: 'c', + modifiers: { foreground: 'cyan', italic: true }, + }, + ], + ]); + + const out2 = processor.process(` +a\x1b[36mb\x1b[3mc +x\x1b[39my\x1b[23mz`); + expect(out2).toEqual([ + [{ text: '', modifiers: {} }], + [ + { + text: 'a', + modifiers: {}, + }, + { + text: 'b', + modifiers: { foreground: 'cyan' }, + }, + { + text: 'c', + modifiers: { foreground: 'cyan', italic: true }, + }, + ], + [ + { + text: 'x', + modifiers: { foreground: 'cyan', italic: true }, + }, + { + text: 'y', + modifiers: { italic: true }, + }, + { + text: 'z', + modifiers: {}, + }, + ], + ]); + + // Verifies that we appended rather than reprocessed + expect(out1[0]).toBe(out2[0]); + }); +}); diff --git a/packages/core-components/src/components/LogViewer/AnsiProcessor.ts b/packages/core-components/src/components/LogViewer/AnsiProcessor.ts new file mode 100644 index 0000000000..f148005355 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/AnsiProcessor.ts @@ -0,0 +1,177 @@ +/* + * 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 ansiRegexMaker from 'ansi-regex'; + +const ansiRegex = ansiRegexMaker(); +const newlineRegex = /\n\r?/g; + +// A mapping of how each escape code changes the modifiers +const codeModifiers = Object.fromEntries( + Object.entries({ + 1: m => ({ ...m, bold: true }), + 3: m => ({ ...m, italic: true }), + 4: m => ({ ...m, underline: true }), + 22: ({ bold: _, ...m }) => m, + 23: ({ italic: _, ...m }) => m, + 24: ({ underline: _, ...m }) => m, + 30: m => ({ ...m, foreground: 'black' }), + 31: m => ({ ...m, foreground: 'red' }), + 32: m => ({ ...m, foreground: 'green' }), + 33: m => ({ ...m, foreground: 'yellow' }), + 34: m => ({ ...m, foreground: 'blue' }), + 35: m => ({ ...m, foreground: 'magenta' }), + 36: m => ({ ...m, foreground: 'cyan' }), + 37: m => ({ ...m, foreground: 'white' }), + 39: ({ foreground: _, ...m }) => m, + 90: m => ({ ...m, foreground: 'grey' }), + 40: m => ({ ...m, background: 'black' }), + 41: m => ({ ...m, background: 'red' }), + 42: m => ({ ...m, background: 'green' }), + 43: m => ({ ...m, background: 'yellow' }), + 44: m => ({ ...m, background: 'blue' }), + 45: m => ({ ...m, background: 'magenta' }), + 46: m => ({ ...m, background: 'cyan' }), + 47: m => ({ ...m, background: 'white' }), + 49: ({ background: _, ...m }) => m, + } as Record ChunkModifiers>).map( + ([code, modifier]) => [`\x1b[${code}m`, modifier], + ), +); + +export type AnsiColor = + | 'black' + | 'red' + | 'green' + | 'yellow' + | 'blue' + | 'magenta' + | 'cyan' + | 'white' + | 'grey'; + +export interface ChunkModifiers { + foreground?: AnsiColor; + background?: AnsiColor; + bold?: boolean; + italic?: boolean; + underline?: boolean; +} + +export interface Chunk { + text: string; + modifiers: ChunkModifiers; +} + +export class AnsiProcessor { + private text: string = ''; + private lines: Chunk[][] = []; + + /** + * Processes a chunk of text while keeping internal state that optimizes + * subsequent processing that appends to the text. + */ + process(text: string): Chunk[][] { + if (this.text === text) { + return this.lines; + } + + if (text.startsWith(this.text)) { + const lastLineIndex = this.lines.length > 0 ? this.lines.length - 1 : 0; + const lastLine = this.lines[lastLineIndex] ?? []; + const lastChunk = lastLine[lastLine.length - 1] as Chunk | undefined; + const newLines = this.processLines( + (lastChunk?.text ?? '') + text.slice(this.text.length), + lastChunk?.modifiers, + ); + this.text = text; + lastLine.splice(lastLine.length - 1, 1, ...newLines[0]); + this.lines[lastLineIndex] = lastLine; + this.lines.push(...newLines.slice(1)); + } else { + this.lines = this.processLines(text); + this.text = text; + } + + return this.lines; + } + + // Split a chunk of text up into lines and process each line individually + private processLines = ( + text: string, + modifiers: ChunkModifiers = {}, + ): Chunk[][] => { + const lines: Chunk[][] = []; + + let prevIndex = 0; + let currentModifiers = modifiers; + newlineRegex.lastIndex = 0; + for (;;) { + const match = newlineRegex.exec(text); + if (!match) { + lines.push(this.processText(text.slice(prevIndex), currentModifiers)); + return lines; + } + + const line = text.slice(prevIndex, match.index); + prevIndex = match.index + match[0].length; + + const chunks = this.processText(line, currentModifiers); + lines.push(chunks); + + // Modifiers that are active in the last chunk are carried over to the next line + currentModifiers = + chunks[chunks.length - 1].modifiers ?? currentModifiers; + } + }; + + // Processing of a one individual text chunk + private processText = ( + fullText: string, + modifiers: ChunkModifiers, + ): Chunk[] => { + const chunks: Chunk[] = []; + + let prevIndex = 0; + let currentModifiers = modifiers; + ansiRegex.lastIndex = 0; + for (;;) { + const match = ansiRegex.exec(fullText); + if (!match) { + chunks.push({ + text: fullText.slice(prevIndex), + modifiers: currentModifiers, + }); + return chunks; + } + + const text = fullText.slice(prevIndex, match.index); + chunks.push({ text, modifiers: currentModifiers }); + + // For every escape code that we encounter we keep track of where the + // next chunk of text starts, and what modifiers it has + prevIndex = match.index + match[0].length; + currentModifiers = this.processCode(match[0], currentModifiers); + } + }; + + private processCode = ( + code: string, + modifiers: ChunkModifiers, + ): ChunkModifiers => { + return codeModifiers[code]?.(modifiers) ?? modifiers; + }; +} diff --git a/yarn.lock b/yarn.lock index a71567ffe8..5c43e126d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6974,6 +6974,13 @@ dependencies: "@types/node" "*" +"@types/ansi-regex@^5.0.0": + version "5.0.0" + resolved "https://registry.npmjs.org/@types/ansi-regex/-/ansi-regex-5.0.0.tgz#569a5189a92cc46d63fb2ad91e6b130f33d999c1" + integrity sha512-SQafVL3pXFh/5qq/nN6p5858g//zSVzcb8JzCLtoVxm8YNPggMQfEIm7aaTNysxpw1S+lFTaW8kv+aR0/CEhCA== + dependencies: + ansi-regex "*" + "@types/archiver@^5.1.0": version "5.3.0" resolved "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.0.tgz#2b34ba56d4d7102d256b922c7e91e09eab79db6f" @@ -9235,6 +9242,11 @@ ansi-html@0.0.7, ansi-html@^0.0.7: resolved "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= +ansi-regex@*, ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -9255,11 +9267,6 @@ ansi-regex@^5.0.0, ansi-regex@^5.0.1: resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== - ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" From cef1f124f6e90b00b5f75f6946af678edca63863 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Dec 2021 15:56:47 +0100 Subject: [PATCH 072/116] core-components: added new LogViewer component Signed-off-by: Patrik Oldsberg --- packages/core-components/package.json | 4 + .../LogViewer/LogViewer.stories.tsx | 81 ++++++++ .../src/components/LogViewer/LogViewer.tsx | 196 ++++++++++++++++++ .../src/components/LogViewer/index.ts | 18 ++ .../core-components/src/components/index.ts | 1 + yarn.lock | 17 +- 6 files changed, 305 insertions(+), 12 deletions(-) create mode 100644 packages/core-components/src/components/LogViewer/LogViewer.stories.tsx create mode 100644 packages/core-components/src/components/LogViewer/LogViewer.tsx create mode 100644 packages/core-components/src/components/LogViewer/index.ts diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 8d24b419d7..0d17d30461 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -62,6 +62,8 @@ "react-syntax-highlighter": "^15.4.3", "react-text-truncate": "^0.16.0", "react-use": "^17.2.4", + "react-virtualized-auto-sizer": "^1.0.6", + "react-window": "^1.8.6", "remark-gfm": "^2.0.0", "zen-observable": "^0.8.15" }, @@ -89,6 +91,8 @@ "@types/node": "^14.14.32", "@types/react-helmet": "^6.1.0", "@types/react-syntax-highlighter": "^13.5.2", + "@types/react-virtualized-auto-sizer": "^1.0.1", + "@types/react-window": "^1.8.5", "@types/zen-observable": "^0.8.0" }, "files": [ diff --git a/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx b/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx new file mode 100644 index 0000000000..b7a56e1a7b --- /dev/null +++ b/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx @@ -0,0 +1,81 @@ +/* + * 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 { LogViewer } from './LogViewer'; + +export default { + title: 'Data Display/LogViewer', + component: LogViewer, +}; + +const exampleLog = `Starting up task with 3 steps +Beginning step Fetch Skeleton + Template +info: Fetching template content from remote URL {"timestamp":"2021-12-03T15:47:11.625Z"} +info: Listing files and directories in template {"timestamp":"2021-12-03T15:47:12.797Z"} +info: Processing 33 template files/directories with input values {"component_id":"srnthsrthntrhsn","description":"rnthsrtnhssrthnrsthn","destination":{"host":"github.com","owner":"rtshnsrtmhrstmh","repo":"srtmhsrtmhrsthms"},"owner":"rstnhrstnhsrthn","timestamp":"2021-12-03T15:47:12.801Z"} +info: Writing file .editorconfig to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.816Z"} +info: Writing file .eslintignore to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.818Z"} +info: Writing file .eslintrc.js to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.820Z"} +info: Writing directory .github/ to template output path. {"timestamp":"2021-12-03T15:47:12.823Z"} +info: Writing file .gitignore to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.824Z"} +info: Writing file README.md to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.827Z"} +info: Writing file babel.config.js to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.829Z"} +info: Writing file catalog-info.yaml to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.831Z"} +info: Writing directory docs/ to template output path. {"timestamp":"2021-12-03T15:47:12.834Z"} +info: Writing file jest.config.js to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.836Z"} +info: Writing file mkdocs.yml to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.838Z"} +info: Writing file next-env.d.ts to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.841Z"} +info: Writing file next.config.js to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.844Z"} +info: Writing file package.json to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.845Z"} +info: Writing file prettier.config.js to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.848Z"} +info: Writing directory public/ to template output path. {"timestamp":"2021-12-03T15:47:12.849Z"} +info: Writing directory src/ to template output path. {"timestamp":"2021-12-03T15:47:12.850Z"} +info: Writing file tsconfig.json to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.851Z"} +info: Writing directory .github/workflows/ to template output path. {"timestamp":"2021-12-03T15:47:12.853Z"} +info: Writing file docs/index.md to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.854Z"} +info: Writing directory public/static/ to template output path. {"timestamp":"2021-12-03T15:47:12.857Z"} +info: Writing directory src/__tests__/ to template output path. {"timestamp":"2021-12-03T15:47:12.858Z"} +info: Writing directory src/components/ to template output path. {"timestamp":"2021-12-03T15:47:12.858Z"} +info: Writing directory src/pages/ to template output path. {"timestamp":"2021-12-03T15:47:12.859Z"} +info: Copying file/directory .github/workflows/build.yml without processing. {"timestamp":"2021-12-03T15:47:12.859Z"} +info: Writing file .github/workflows/build.yml to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.860Z"} +info: Writing file public/static/fonts.css to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.861Z"} +info: Writing file src/components/Header.tsx to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.863Z"} +info: Writing file src/__tests__/index.test.tsx to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.865Z"} +info: Writing file src/pages/_app.tsx to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.868Z"} +info: Writing file src/pages/_document.tsx to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.871Z"} +info: Writing directory src/pages/api/ to template output path. {"timestamp":"2021-12-03T15:47:12.873Z"} +info: Writing file src/pages/index.tsx to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.874Z"} +info: Writing file src/pages/api/ping.ts to template output path with mode 33188. {"timestamp":"2021-12-03T15:47:12.877Z"} +info: Template result written to /var/folders/k6/9s7hd6w17115xlgwnsp0wsbr0000gn/T/5c9f8584-fded-4741-b6ef-46d94ff2cbdb {"timestamp":"2021-12-03T15:47:12.878Z"} +Finished step Fetch Skeleton + Template +Beginning step Publish +HttpError: Not Found + at /Users/patriko/dev/backstage/node_modules/@octokit/request/dist-node/index.js:86:21 + at runMicrotasks () + at processTicksAndRejections (internal/process/task_queues.js:95:5) + at async Object.handler (webpack-internal:///../../plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts:156:20) + at async HandlebarsWorkflowRunner.execute (webpack-internal:///../../plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts:254:11) + at async TaskWorker.runOneTask (webpack-internal:///../../plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts:110:13) + at async eval (webpack-internal:///../../plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts:100:9) +Run completed with status: failed`; + +export const ExampleLogViewer = () => ( +
+ +
+); diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx new file mode 100644 index 0000000000..21d42a1862 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -0,0 +1,196 @@ +/* + * 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 { makeStyles } from '@material-ui/core/styles'; +import React, { useMemo } from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { FixedSizeList } from 'react-window'; +import { AnsiProcessor } from './AnsiProcessor'; +import startCase from 'lodash/startCase'; +import * as colors from '@material-ui/core/colors'; + +export interface LogViewerProps { + text: string; + noLineNumbers?: boolean; +} + +export type AnsiColor = + | 'black' + | 'red' + | 'green' + | 'yellow' + | 'blue' + | 'magenta' + | 'cyan' + | 'white' + | 'grey'; + +export interface ChunkModifiers { + foreground?: AnsiColor; + background?: AnsiColor; + bold?: boolean; + italic?: boolean; + underline?: boolean; +} + +const useStyles = makeStyles(theme => ({ + root: { + fontFamily: '"Monaco", monospace', + fontSize: theme.typography.fontSize, + background: theme.palette.background.paper, + }, + line: { + whiteSpace: 'pre', + }, + lineNumber: { + display: 'inline-block', + textAlign: 'end', + width: 60, + marginRight: theme.spacing(1), + }, + modifierBold: { + fontWeight: theme.typography.fontWeightBold, + }, + modifierItalic: { + fontStyle: 'italic', + }, + modifierUnderline: { + textDecoration: 'underline', + }, + modifierForegroundBlack: { + color: colors.common.black, + }, + modifierForegroundRed: { + color: colors.red[500], + }, + modifierForegroundGreen: { + color: colors.green[500], + }, + modifierForegroundYellow: { + color: colors.yellow[500], + }, + modifierForegroundBlue: { + color: colors.blue[500], + }, + modifierForegroundMagenta: { + color: colors.purple[500], + }, + modifierForegroundCyan: { + color: colors.cyan[500], + }, + modifierForegroundWhite: { + color: colors.common.white, + }, + modifierForegroundGrey: { + color: colors.grey[500], + }, + modifierBackgroundBlack: { + color: colors.common.black, + }, + modifierBackgroundRed: { + color: colors.red[500], + }, + modifierBackgroundGreen: { + color: colors.green[500], + }, + modifierBackgroundYellow: { + color: colors.yellow[500], + }, + modifierBackgroundBlue: { + color: colors.blue[500], + }, + modifierBackgroundMagenta: { + color: colors.purple[500], + }, + modifierBackgroundCyan: { + color: colors.cyan[500], + }, + modifierBackgroundWhite: { + color: colors.common.white, + }, + modifierBackgroundGrey: { + color: colors.grey[500], + }, +})); + +function getModifierClasses( + classes: ReturnType, + modifiers: ChunkModifiers, +) { + const classNames = new Array(); + if (modifiers.bold) { + classNames.push(classes.modifierBold); + } + if (modifiers.italic) { + classNames.push(classes.modifierItalic); + } + if (modifiers.underline) { + classNames.push(classes.modifierUnderline); + } + if (modifiers.foreground) { + const key = `modifierForeground${startCase( + modifiers.foreground, + )}` as keyof typeof classes; + classNames.push(classes[key]); + } + if (modifiers.background) { + const key = `modifierBackground${startCase( + modifiers.background, + )}` as keyof typeof classes; + classNames.push(classes[key]); + } + return classNames.join(' '); +} + +export function LogViewer(props: LogViewerProps) { + const { noLineNumbers } = props; + const classes = useStyles(); + + // The processor keeps state that optimizes appending to the text + const processor = useMemo(() => new AnsiProcessor(), []); + const lines = processor.process(props.text); + + return ( + + {({ height, width }) => ( + + {({ index, style, data }) => ( +
+ {!noLineNumbers && ( + {index + 1} + )} + {data[index].map(({ text, modifiers }, i) => ( + + {text} + + ))} +
+ )} +
+ )} +
+ ); +} diff --git a/packages/core-components/src/components/LogViewer/index.ts b/packages/core-components/src/components/LogViewer/index.ts new file mode 100644 index 0000000000..839f34f81e --- /dev/null +++ b/packages/core-components/src/components/LogViewer/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { LogViewer } from './LogViewer'; +export type { LogViewerProps } from './LogViewer'; diff --git a/packages/core-components/src/components/index.ts b/packages/core-components/src/components/index.ts index 3c9376a894..473fab8444 100644 --- a/packages/core-components/src/components/index.ts +++ b/packages/core-components/src/components/index.ts @@ -30,6 +30,7 @@ export * from './HeaderIconLinkRow'; export * from './HorizontalScrollGrid'; export * from './Lifecycle'; export * from './Link'; +export * from './LogViewer'; export * from './MarkdownContent'; export * from './OAuthRequestDialog'; export * from './OverflowTooltip'; diff --git a/yarn.lock b/yarn.lock index 5c43e126d4..a71567ffe8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6974,13 +6974,6 @@ dependencies: "@types/node" "*" -"@types/ansi-regex@^5.0.0": - version "5.0.0" - resolved "https://registry.npmjs.org/@types/ansi-regex/-/ansi-regex-5.0.0.tgz#569a5189a92cc46d63fb2ad91e6b130f33d999c1" - integrity sha512-SQafVL3pXFh/5qq/nN6p5858g//zSVzcb8JzCLtoVxm8YNPggMQfEIm7aaTNysxpw1S+lFTaW8kv+aR0/CEhCA== - dependencies: - ansi-regex "*" - "@types/archiver@^5.1.0": version "5.3.0" resolved "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.0.tgz#2b34ba56d4d7102d256b922c7e91e09eab79db6f" @@ -9242,11 +9235,6 @@ ansi-html@0.0.7, ansi-html@^0.0.7: resolved "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= -ansi-regex@*, ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== - ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -9267,6 +9255,11 @@ ansi-regex@^5.0.0, ansi-regex@^5.0.1: resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" From 8af6baa2d1df591a5113f41635f78faec77dd4b5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Dec 2021 17:12:38 +0100 Subject: [PATCH 073/116] core-components: refactor AnsiProcessor to keep track of line numbers Signed-off-by: Patrik Oldsberg --- .../LogViewer/AnsiProcessor.test.ts | 289 ++++++++++-------- .../src/components/LogViewer/AnsiProcessor.ts | 59 +++- .../src/components/LogViewer/LogViewer.tsx | 2 +- 3 files changed, 204 insertions(+), 146 deletions(-) diff --git a/packages/core-components/src/components/LogViewer/AnsiProcessor.test.ts b/packages/core-components/src/components/LogViewer/AnsiProcessor.test.ts index b2ceee78b0..9ef0353eca 100644 --- a/packages/core-components/src/components/LogViewer/AnsiProcessor.test.ts +++ b/packages/core-components/src/components/LogViewer/AnsiProcessor.test.ts @@ -20,37 +20,43 @@ describe('AnsiProcessor', () => { it('should process a single line', () => { const processor = new AnsiProcessor(); expect(processor.process('foo\x1b[31mbar\x1b[39mbaz')).toEqual([ - [ - { - text: 'foo', - modifiers: {}, - }, - { - text: 'bar', - modifiers: { foreground: 'red' }, - }, - { - text: 'baz', - modifiers: {}, - }, - ], + { + chunks: [ + { + text: 'foo', + modifiers: {}, + }, + { + text: 'bar', + modifiers: { foreground: 'red' }, + }, + { + text: 'baz', + modifiers: {}, + }, + ], + lineNumber: 1, + }, ]); expect(processor.process(`foo bar: baz`)).toEqual([ - [ - { - text: 'foo ', - modifiers: {}, - }, - { - text: 'bar', - modifiers: { foreground: 'green' }, - }, - { - text: ': baz', - modifiers: {}, - }, - ], + { + chunks: [ + { + text: 'foo ', + modifiers: {}, + }, + { + text: 'bar', + modifiers: { foreground: 'green' }, + }, + { + text: ': baz', + modifiers: {}, + }, + ], + lineNumber: 1, + }, ]); }); @@ -62,36 +68,42 @@ a\x1b[34mb\x1b[39mc x\x1b[44my\x1b[49mz `), ).toEqual([ - [{ text: '', modifiers: {} }], - [ - { - text: 'a', - modifiers: {}, - }, - { - text: 'b', - modifiers: { foreground: 'blue' }, - }, - { - text: 'c', - modifiers: {}, - }, - ], - [ - { - text: 'x', - modifiers: {}, - }, - { - text: 'y', - modifiers: { background: 'blue' }, - }, - { - text: 'z', - modifiers: {}, - }, - ], - [{ text: '', modifiers: {} }], + { chunks: [{ text: '', modifiers: {} }], lineNumber: 1 }, + { + chunks: [ + { + text: 'a', + modifiers: {}, + }, + { + text: 'b', + modifiers: { foreground: 'blue' }, + }, + { + text: 'c', + modifiers: {}, + }, + ], + lineNumber: 2, + }, + { + chunks: [ + { + text: 'x', + modifiers: {}, + }, + { + text: 'y', + modifiers: { background: 'blue' }, + }, + { + text: 'z', + modifiers: {}, + }, + ], + lineNumber: 3, + }, + { chunks: [{ text: '', modifiers: {} }], lineNumber: 4 }, ]); }); @@ -102,35 +114,41 @@ x\x1b[44my\x1b[49mz a\x1b[45mb\x1b[35mc x\x1b[39my\x1b[49mz`), ).toEqual([ - [{ text: '', modifiers: {} }], - [ - { - text: 'a', - modifiers: {}, - }, - { - text: 'b', - modifiers: { background: 'magenta' }, - }, - { - text: 'c', - modifiers: { foreground: 'magenta', background: 'magenta' }, - }, - ], - [ - { - text: 'x', - modifiers: { foreground: 'magenta', background: 'magenta' }, - }, - { - text: 'y', - modifiers: { background: 'magenta' }, - }, - { - text: 'z', - modifiers: {}, - }, - ], + { chunks: [{ text: '', modifiers: {} }], lineNumber: 1 }, + { + chunks: [ + { + text: 'a', + modifiers: {}, + }, + { + text: 'b', + modifiers: { background: 'magenta' }, + }, + { + text: 'c', + modifiers: { foreground: 'magenta', background: 'magenta' }, + }, + ], + lineNumber: 2, + }, + { + chunks: [ + { + text: 'x', + modifiers: { foreground: 'magenta', background: 'magenta' }, + }, + { + text: 'y', + modifiers: { background: 'magenta' }, + }, + { + text: 'z', + modifiers: {}, + }, + ], + lineNumber: 3, + }, ]); }); @@ -139,56 +157,65 @@ x\x1b[39my\x1b[49mz`), const out1 = processor.process(` a\x1b[36mb\x1b[3mc`); expect(out1).toEqual([ - [{ text: '', modifiers: {} }], - [ - { - text: 'a', - modifiers: {}, - }, - { - text: 'b', - modifiers: { foreground: 'cyan' }, - }, - { - text: 'c', - modifiers: { foreground: 'cyan', italic: true }, - }, - ], + { chunks: [{ text: '', modifiers: {} }], lineNumber: 1 }, + { + chunks: [ + { + text: 'a', + modifiers: {}, + }, + { + text: 'b', + modifiers: { foreground: 'cyan' }, + }, + { + text: 'c', + modifiers: { foreground: 'cyan', italic: true }, + }, + ], + lineNumber: 2, + }, ]); const out2 = processor.process(` a\x1b[36mb\x1b[3mc x\x1b[39my\x1b[23mz`); expect(out2).toEqual([ - [{ text: '', modifiers: {} }], - [ - { - text: 'a', - modifiers: {}, - }, - { - text: 'b', - modifiers: { foreground: 'cyan' }, - }, - { - text: 'c', - modifiers: { foreground: 'cyan', italic: true }, - }, - ], - [ - { - text: 'x', - modifiers: { foreground: 'cyan', italic: true }, - }, - { - text: 'y', - modifiers: { italic: true }, - }, - { - text: 'z', - modifiers: {}, - }, - ], + { chunks: [{ text: '', modifiers: {} }], lineNumber: 1 }, + { + chunks: [ + { + text: 'a', + modifiers: {}, + }, + { + text: 'b', + modifiers: { foreground: 'cyan' }, + }, + { + text: 'c', + modifiers: { foreground: 'cyan', italic: true }, + }, + ], + lineNumber: 2, + }, + { + chunks: [ + { + text: 'x', + modifiers: { foreground: 'cyan', italic: true }, + }, + { + text: 'y', + modifiers: { italic: true }, + }, + { + text: 'z', + modifiers: {}, + }, + ], + lineNumber: 3, + }, ]); // Verifies that we appended rather than reprocessed diff --git a/packages/core-components/src/components/LogViewer/AnsiProcessor.ts b/packages/core-components/src/components/LogViewer/AnsiProcessor.ts index f148005355..f3ea37615c 100644 --- a/packages/core-components/src/components/LogViewer/AnsiProcessor.ts +++ b/packages/core-components/src/components/LogViewer/AnsiProcessor.ts @@ -71,34 +71,56 @@ export interface ChunkModifiers { underline?: boolean; } -export interface Chunk { +// export interface AnsiLine { +// lineNumber: number; +// chunks: AnsiChunk[]; +// } + +export interface AnsiChunk { text: string; modifiers: ChunkModifiers; } +export class AnsiLine { + constructor( + readonly lineNumber: number = 1, + readonly chunks: AnsiChunk[] = [], + ) {} + + lastChunk(): AnsiChunk | undefined { + return this.chunks[this.chunks.length - 1]; + } +} + export class AnsiProcessor { private text: string = ''; - private lines: Chunk[][] = []; + private lines: AnsiLine[] = []; /** * Processes a chunk of text while keeping internal state that optimizes * subsequent processing that appends to the text. */ - process(text: string): Chunk[][] { + process(text: string): AnsiLine[] { if (this.text === text) { return this.lines; } if (text.startsWith(this.text)) { const lastLineIndex = this.lines.length > 0 ? this.lines.length - 1 : 0; - const lastLine = this.lines[lastLineIndex] ?? []; - const lastChunk = lastLine[lastLine.length - 1] as Chunk | undefined; + const lastLine = this.lines[lastLineIndex] ?? new AnsiLine(); + const lastChunk = lastLine.lastChunk(); + const newLines = this.processLines( (lastChunk?.text ?? '') + text.slice(this.text.length), lastChunk?.modifiers, + lastLine?.lineNumber, ); this.text = text; - lastLine.splice(lastLine.length - 1, 1, ...newLines[0]); + lastLine.chunks.splice( + lastLine.chunks.length - 1, + 1, + ...newLines[0]?.chunks, + ); this.lines[lastLineIndex] = lastLine; this.lines.push(...newLines.slice(1)); } else { @@ -113,16 +135,23 @@ export class AnsiProcessor { private processLines = ( text: string, modifiers: ChunkModifiers = {}, - ): Chunk[][] => { - const lines: Chunk[][] = []; + startingLineNumber: number = 1, + ): AnsiLine[] => { + const lines: AnsiLine[] = []; + + let currentModifiers = modifiers; + let currentLineNumber = startingLineNumber; let prevIndex = 0; - let currentModifiers = modifiers; newlineRegex.lastIndex = 0; for (;;) { const match = newlineRegex.exec(text); if (!match) { - lines.push(this.processText(text.slice(prevIndex), currentModifiers)); + const chunks = this.processText( + text.slice(prevIndex), + currentModifiers, + ); + lines.push(new AnsiLine(currentLineNumber, chunks)); return lines; } @@ -130,11 +159,12 @@ export class AnsiProcessor { prevIndex = match.index + match[0].length; const chunks = this.processText(line, currentModifiers); - lines.push(chunks); + lines.push(new AnsiLine(currentLineNumber, chunks)); // Modifiers that are active in the last chunk are carried over to the next line currentModifiers = chunks[chunks.length - 1].modifiers ?? currentModifiers; + currentLineNumber += 1; } }; @@ -142,11 +172,12 @@ export class AnsiProcessor { private processText = ( fullText: string, modifiers: ChunkModifiers, - ): Chunk[] => { - const chunks: Chunk[] = []; + ): AnsiChunk[] => { + const chunks: AnsiChunk[] = []; + + let currentModifiers = modifiers; let prevIndex = 0; - let currentModifiers = modifiers; ansiRegex.lastIndex = 0; for (;;) { const match = ansiRegex.exec(fullText); diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index 21d42a1862..a77990f734 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -179,7 +179,7 @@ export function LogViewer(props: LogViewerProps) { {!noLineNumbers && ( {index + 1} )} - {data[index].map(({ text, modifiers }, i) => ( + {data[index].chunks.map(({ text, modifiers }, i) => ( Date: Sat, 4 Dec 2021 17:43:17 +0100 Subject: [PATCH 074/116] core-components: basic line selection and filtering for LogViewer Signed-off-by: Patrik Oldsberg --- .../LogViewer/AnsiProcessor.test.ts | 19 ++- .../src/components/LogViewer/AnsiProcessor.ts | 23 ++-- .../src/components/LogViewer/LogViewer.tsx | 113 ++++++++++++++---- 3 files changed, 116 insertions(+), 39 deletions(-) diff --git a/packages/core-components/src/components/LogViewer/AnsiProcessor.test.ts b/packages/core-components/src/components/LogViewer/AnsiProcessor.test.ts index 9ef0353eca..e957b28b7c 100644 --- a/packages/core-components/src/components/LogViewer/AnsiProcessor.test.ts +++ b/packages/core-components/src/components/LogViewer/AnsiProcessor.test.ts @@ -35,6 +35,7 @@ describe('AnsiProcessor', () => { modifiers: {}, }, ], + text: 'foobarbaz', lineNumber: 1, }, ]); @@ -55,6 +56,7 @@ describe('AnsiProcessor', () => { modifiers: {}, }, ], + text: 'foo bar: baz', lineNumber: 1, }, ]); @@ -68,7 +70,7 @@ a\x1b[34mb\x1b[39mc x\x1b[44my\x1b[49mz `), ).toEqual([ - { chunks: [{ text: '', modifiers: {} }], lineNumber: 1 }, + { chunks: [{ text: '', modifiers: {} }], text: '', lineNumber: 1 }, { chunks: [ { @@ -84,6 +86,7 @@ x\x1b[44my\x1b[49mz modifiers: {}, }, ], + text: 'abc', lineNumber: 2, }, { @@ -101,9 +104,10 @@ x\x1b[44my\x1b[49mz modifiers: {}, }, ], + text: 'xyz', lineNumber: 3, }, - { chunks: [{ text: '', modifiers: {} }], lineNumber: 4 }, + { chunks: [{ text: '', modifiers: {} }], text: '', lineNumber: 4 }, ]); }); @@ -114,7 +118,7 @@ x\x1b[44my\x1b[49mz a\x1b[45mb\x1b[35mc x\x1b[39my\x1b[49mz`), ).toEqual([ - { chunks: [{ text: '', modifiers: {} }], lineNumber: 1 }, + { chunks: [{ text: '', modifiers: {} }], text: '', lineNumber: 1 }, { chunks: [ { @@ -130,6 +134,7 @@ x\x1b[39my\x1b[49mz`), modifiers: { foreground: 'magenta', background: 'magenta' }, }, ], + text: 'abc', lineNumber: 2, }, { @@ -147,6 +152,7 @@ x\x1b[39my\x1b[49mz`), modifiers: {}, }, ], + text: 'xyz', lineNumber: 3, }, ]); @@ -157,7 +163,7 @@ x\x1b[39my\x1b[49mz`), const out1 = processor.process(` a\x1b[36mb\x1b[3mc`); expect(out1).toEqual([ - { chunks: [{ text: '', modifiers: {} }], lineNumber: 1 }, + { chunks: [{ text: '', modifiers: {} }], text: '', lineNumber: 1 }, { chunks: [ { @@ -173,6 +179,7 @@ a\x1b[36mb\x1b[3mc`); modifiers: { foreground: 'cyan', italic: true }, }, ], + text: 'abc', lineNumber: 2, }, ]); @@ -181,7 +188,7 @@ a\x1b[36mb\x1b[3mc`); a\x1b[36mb\x1b[3mc x\x1b[39my\x1b[23mz`); expect(out2).toEqual([ - { chunks: [{ text: '', modifiers: {} }], lineNumber: 1 }, + { chunks: [{ text: '', modifiers: {} }], text: '', lineNumber: 1 }, { chunks: [ { @@ -197,6 +204,7 @@ x\x1b[39my\x1b[23mz`); modifiers: { foreground: 'cyan', italic: true }, }, ], + text: 'abc', lineNumber: 2, }, { @@ -214,6 +222,7 @@ x\x1b[39my\x1b[23mz`); modifiers: {}, }, ], + text: 'xyz', lineNumber: 3, }, ]); diff --git a/packages/core-components/src/components/LogViewer/AnsiProcessor.ts b/packages/core-components/src/components/LogViewer/AnsiProcessor.ts index f3ea37615c..a33d0db450 100644 --- a/packages/core-components/src/components/LogViewer/AnsiProcessor.ts +++ b/packages/core-components/src/components/LogViewer/AnsiProcessor.ts @@ -82,14 +82,25 @@ export interface AnsiChunk { } export class AnsiLine { + text: string; + constructor( readonly lineNumber: number = 1, readonly chunks: AnsiChunk[] = [], - ) {} + ) { + this.text = chunks.map(c => c.text).join(''); + } lastChunk(): AnsiChunk | undefined { return this.chunks[this.chunks.length - 1]; } + + replaceLastChunk(newChunks?: AnsiChunk[]) { + if (newChunks) { + this.chunks.splice(this.chunks.length - 1, 1, ...newChunks); + this.text = this.chunks.map(c => c.text).join(''); + } + } } export class AnsiProcessor { @@ -115,18 +126,14 @@ export class AnsiProcessor { lastChunk?.modifiers, lastLine?.lineNumber, ); - this.text = text; - lastLine.chunks.splice( - lastLine.chunks.length - 1, - 1, - ...newLines[0]?.chunks, - ); + lastLine.replaceLastChunk(newLines[0]?.chunks); + this.lines[lastLineIndex] = lastLine; this.lines.push(...newLines.slice(1)); } else { this.lines = this.processLines(text); - this.text = text; } + this.text = text; return this.lines; } diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index a77990f734..1adcb6df7e 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -15,12 +15,16 @@ */ import { makeStyles } from '@material-ui/core/styles'; -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList } from 'react-window'; import { AnsiProcessor } from './AnsiProcessor'; import startCase from 'lodash/startCase'; import * as colors from '@material-ui/core/colors'; +import clsx from 'clsx'; +import TextField from '@material-ui/core/TextField'; + +const HEADER_SIZE = 40; export interface LogViewerProps { text: string; @@ -48,18 +52,38 @@ export interface ChunkModifiers { const useStyles = makeStyles(theme => ({ root: { + background: theme.palette.background.paper, + }, + header: { + height: HEADER_SIZE, + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + }, + log: { fontFamily: '"Monaco", monospace', fontSize: theme.typography.fontSize, - background: theme.palette.background.paper, }, line: { whiteSpace: 'pre', + + '&:hover': { + background: theme.palette.action.hover, + }, + }, + lineSelected: { + background: theme.palette.action.selected, + + '&:hover': { + background: theme.palette.action.selected, + }, }, lineNumber: { display: 'inline-block', textAlign: 'end', width: 60, marginRight: theme.spacing(1), + cursor: 'pointer', }, modifierBold: { fontWeight: theme.typography.fontWeightBold, @@ -152,44 +176,81 @@ function getModifierClasses( )}` as keyof typeof classes; classNames.push(classes[key]); } - return classNames.join(' '); + return classNames.length > 0 ? classNames.join(' ') : undefined; } export function LogViewer(props: LogViewerProps) { const { noLineNumbers } = props; const classes = useStyles(); + const [selectedLine, setSelectedLine] = useState(); + const [filter, setFilter] = useState(''); // The processor keeps state that optimizes appending to the text const processor = useMemo(() => new AnsiProcessor(), []); const lines = processor.process(props.text); + const filteredLines = useMemo(() => { + if (!filter) { + return lines; + } + return lines.filter(line => line.text.includes(filter)); + }, [lines, filter]); + return ( {({ height, width }) => ( - - {({ index, style, data }) => ( -
- {!noLineNumbers && ( - {index + 1} - )} - {data[index].chunks.map(({ text, modifiers }, i) => ( - +
+ setFilter(e.target.value)} + /> +
+ + {({ index, style, data }) => { + const { chunks, lineNumber } = data[index]; + return ( +
- {text} - - ))} -
- )} -
+ {!noLineNumbers && ( + setSelectedLine(lineNumber)} + onKeyPress={() => setSelectedLine(lineNumber)} + > + {lineNumber} + + )} + {chunks.map(({ text, modifiers }, i) => ( + + {text} + + ))} +
+ ); + }} +
+ )}
); From 8884af06e8f0b4e7b077610aef0c0ade6116b71e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Dec 2021 18:58:51 +0100 Subject: [PATCH 075/116] core-components: highlight search matches in LogViewer Signed-off-by: Patrik Oldsberg --- .../src/components/LogViewer/AnsiProcessor.ts | 10 +- .../src/components/LogViewer/LogViewer.tsx | 166 +++++++++++++++--- 2 files changed, 148 insertions(+), 28 deletions(-) diff --git a/packages/core-components/src/components/LogViewer/AnsiProcessor.ts b/packages/core-components/src/components/LogViewer/AnsiProcessor.ts index a33d0db450..5c4bf58865 100644 --- a/packages/core-components/src/components/LogViewer/AnsiProcessor.ts +++ b/packages/core-components/src/components/LogViewer/AnsiProcessor.ts @@ -88,7 +88,10 @@ export class AnsiLine { readonly lineNumber: number = 1, readonly chunks: AnsiChunk[] = [], ) { - this.text = chunks.map(c => c.text).join(''); + this.text = chunks + .map(c => c.text) + .join('') + .toLocaleLowerCase('en-US'); } lastChunk(): AnsiChunk | undefined { @@ -98,7 +101,10 @@ export class AnsiLine { replaceLastChunk(newChunks?: AnsiChunk[]) { if (newChunks) { this.chunks.splice(this.chunks.length - 1, 1, ...newChunks); - this.text = this.chunks.map(c => c.text).join(''); + this.text = this.chunks + .map(c => c.text) + .join('') + .toLocaleLowerCase('en-US'); } } } diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index 1adcb6df7e..c1a5d06b34 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -14,11 +14,11 @@ * limitations under the License. */ -import { makeStyles } from '@material-ui/core/styles'; +import { alpha, makeStyles } from '@material-ui/core/styles'; import React, { useMemo, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList } from 'react-window'; -import { AnsiProcessor } from './AnsiProcessor'; +import { AnsiChunk, AnsiLine, AnsiProcessor } from './AnsiProcessor'; import startCase from 'lodash/startCase'; import * as colors from '@material-ui/core/colors'; import clsx from 'clsx'; @@ -85,6 +85,9 @@ const useStyles = makeStyles(theme => ({ marginRight: theme.spacing(1), cursor: 'pointer', }, + textHighlight: { + background: alpha(theme.palette.primary.main, 0.3), + }, modifierBold: { fontWeight: theme.typography.fontWeightBold, }, @@ -122,31 +125,31 @@ const useStyles = makeStyles(theme => ({ color: colors.grey[500], }, modifierBackgroundBlack: { - color: colors.common.black, + background: colors.common.black, }, modifierBackgroundRed: { - color: colors.red[500], + background: colors.red[500], }, modifierBackgroundGreen: { - color: colors.green[500], + background: colors.green[500], }, modifierBackgroundYellow: { - color: colors.yellow[500], + background: colors.yellow[500], }, modifierBackgroundBlue: { - color: colors.blue[500], + background: colors.blue[500], }, modifierBackgroundMagenta: { - color: colors.purple[500], + background: colors.purple[500], }, modifierBackgroundCyan: { - color: colors.cyan[500], + background: colors.cyan[500], }, modifierBackgroundWhite: { - color: colors.common.white, + background: colors.common.white, }, modifierBackgroundGrey: { - color: colors.grey[500], + background: colors.grey[500], }, })); @@ -179,22 +182,135 @@ function getModifierClasses( return classNames.length > 0 ? classNames.join(' ') : undefined; } +export function LogLine({ + line, + classes, + searchText, +}: { + line: AnsiLine; + classes: ReturnType; + searchText: string; +}) { + let searchResults: Array<{ start: number; end: number }> | undefined = + undefined; + if (searchText && line.text.includes(searchText)) { + searchResults = []; + let offset = 0; + for (;;) { + const start = line.text.indexOf(searchText, offset); + if (start === -1) { + break; + } + const end = start + searchText.length; + searchResults.push({ start, end }); + offset = end; + } + } + + const output = new Array(line.chunks.length); + + let key = 0; + let chunkOffset = 0; + let nextResult = searchResults?.shift(); + for (const { text, modifiers } of line.chunks) { + if (!nextResult || chunkOffset + text.length < nextResult.start) { + output.push( + + {text} + , + ); + chunkOffset += text.length; + continue; + } + + let localOffset = 0; + while (nextResult) { + let localStart = nextResult.start - chunkOffset; + if (localStart < 0) { + localStart = 0; + } + const localEnd = nextResult.end - chunkOffset; + const beforeMatch = text.slice(localOffset, localStart); + const match = text.slice(localStart, localEnd); + + if (beforeMatch) { + output.push( + + {beforeMatch} + , + ); + } + output.push( + + {match} + , + ); + + localOffset = localStart + match.length; + + if (match.length === searchText.length) { + nextResult = searchResults?.shift(); + } else { + break; + } + } + + if (localOffset < text.length) { + output.push( + + {text.slice(localOffset)} + , + ); + } + + chunkOffset += text.length; + } + return <>{output}; +} + export function LogViewer(props: LogViewerProps) { const { noLineNumbers } = props; const classes = useStyles(); const [selectedLine, setSelectedLine] = useState(); - const [filter, setFilter] = useState(''); + const [searchInput, setSearchInput] = useState(''); + const searchText = searchInput.toLocaleLowerCase('en-US'); // The processor keeps state that optimizes appending to the text const processor = useMemo(() => new AnsiProcessor(), []); const lines = processor.process(props.text); const filteredLines = useMemo(() => { - if (!filter) { + if (!searchText) { return lines; } - return lines.filter(line => line.text.includes(filter)); - }, [lines, filter]); + const matchingLines = []; + const searchResults = []; + for (const line of lines) { + if (line.text.includes(searchText)) { + matchingLines.push(line); + + const lineResults = []; + let offset = 0; + for (;;) { + const start = line.text.indexOf(searchText, offset); + if (start === -1) { + break; + } + const end = start + searchText.length; + lineResults.push({ start, end }); + offset = end; + } + searchResults.push(lineResults); + } + } + return lines.filter(line => line.text.includes(searchText)); + }, [lines, searchText]); return ( @@ -205,8 +321,8 @@ export function LogViewer(props: LogViewerProps) { size="small" variant="standard" placeholder="Search" - value={filter} - onChange={e => setFilter(e.target.value)} + value={searchInput} + onChange={e => setSearchInput(e.target.value)} /> {({ index, style, data }) => { - const { chunks, lineNumber } = data[index]; + const line = data[index]; + const { lineNumber } = line; return (
)} - {chunks.map(({ text, modifiers }, i) => ( - - {text} - - ))} +
); }} From 439833d300c03dfa1e7d50ac715133e6969173db Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 11:29:09 +0100 Subject: [PATCH 076/116] core-components: split up LogViewer Signed-off-by: Patrik Oldsberg --- .../src/components/LogViewer/LogLine.tsx | 140 +++++++++++ .../src/components/LogViewer/LogViewer.tsx | 233 +----------------- .../src/components/LogViewer/styles.ts | 123 +++++++++ 3 files changed, 266 insertions(+), 230 deletions(-) create mode 100644 packages/core-components/src/components/LogViewer/LogLine.tsx create mode 100644 packages/core-components/src/components/LogViewer/styles.ts diff --git a/packages/core-components/src/components/LogViewer/LogLine.tsx b/packages/core-components/src/components/LogViewer/LogLine.tsx new file mode 100644 index 0000000000..7e5366eb77 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/LogLine.tsx @@ -0,0 +1,140 @@ +/* + * 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 from 'react'; +import { AnsiLine, ChunkModifiers } from './AnsiProcessor'; +import startCase from 'lodash/startCase'; +import clsx from 'clsx'; +import { useStyles } from './useStyles'; + +function getModifierClasses( + classes: ReturnType, + modifiers: ChunkModifiers, +) { + const classNames = new Array(); + if (modifiers.bold) { + classNames.push(classes.modifierBold); + } + if (modifiers.italic) { + classNames.push(classes.modifierItalic); + } + if (modifiers.underline) { + classNames.push(classes.modifierUnderline); + } + if (modifiers.foreground) { + const key = `modifierForeground${startCase( + modifiers.foreground, + )}` as keyof typeof classes; + classNames.push(classes[key]); + } + if (modifiers.background) { + const key = `modifierBackground${startCase( + modifiers.background, + )}` as keyof typeof classes; + classNames.push(classes[key]); + } + return classNames.length > 0 ? classNames.join(' ') : undefined; +} + +export interface LogLineProps { + line: AnsiLine; + classes: ReturnType; + searchText: string; +} + +export function LogLine({ line, classes, searchText }: LogLineProps) { + let searchResults: Array<{ start: number; end: number }> | undefined = + undefined; + if (searchText && line.text.includes(searchText)) { + searchResults = []; + let offset = 0; + for (;;) { + const start = line.text.indexOf(searchText, offset); + if (start === -1) { + break; + } + const end = start + searchText.length; + searchResults.push({ start, end }); + offset = end; + } + } + + const output = new Array(line.chunks.length); + + let key = 0; + let chunkOffset = 0; + let nextResult = searchResults?.shift(); + for (const { text, modifiers } of line.chunks) { + if (!nextResult || chunkOffset + text.length < nextResult.start) { + output.push( + + {text} + , + ); + chunkOffset += text.length; + continue; + } + + let localOffset = 0; + while (nextResult) { + let localStart = nextResult.start - chunkOffset; + if (localStart < 0) { + localStart = 0; + } + const localEnd = nextResult.end - chunkOffset; + const beforeMatch = text.slice(localOffset, localStart); + const match = text.slice(localStart, localEnd); + + if (beforeMatch) { + output.push( + + {beforeMatch} + , + ); + } + output.push( + + {match} + , + ); + + localOffset = localStart + match.length; + + if (match.length === searchText.length) { + nextResult = searchResults?.shift(); + } else { + break; + } + } + + if (localOffset < text.length) { + output.push( + + {text.slice(localOffset)} + , + ); + } + + chunkOffset += text.length; + } + return <>{output}; +} diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index c1a5d06b34..d6722edbcd 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -14,17 +14,14 @@ * limitations under the License. */ -import { alpha, makeStyles } from '@material-ui/core/styles'; import React, { useMemo, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList } from 'react-window'; -import { AnsiChunk, AnsiLine, AnsiProcessor } from './AnsiProcessor'; -import startCase from 'lodash/startCase'; -import * as colors from '@material-ui/core/colors'; +import { AnsiProcessor } from './AnsiProcessor'; +import { HEADER_SIZE, useStyles } from './styles'; import clsx from 'clsx'; import TextField from '@material-ui/core/TextField'; - -const HEADER_SIZE = 40; +import { LogLine } from './LogLine'; export interface LogViewerProps { text: string; @@ -50,230 +47,6 @@ export interface ChunkModifiers { underline?: boolean; } -const useStyles = makeStyles(theme => ({ - root: { - background: theme.palette.background.paper, - }, - header: { - height: HEADER_SIZE, - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-end', - }, - log: { - fontFamily: '"Monaco", monospace', - fontSize: theme.typography.fontSize, - }, - line: { - whiteSpace: 'pre', - - '&:hover': { - background: theme.palette.action.hover, - }, - }, - lineSelected: { - background: theme.palette.action.selected, - - '&:hover': { - background: theme.palette.action.selected, - }, - }, - lineNumber: { - display: 'inline-block', - textAlign: 'end', - width: 60, - marginRight: theme.spacing(1), - cursor: 'pointer', - }, - textHighlight: { - background: alpha(theme.palette.primary.main, 0.3), - }, - modifierBold: { - fontWeight: theme.typography.fontWeightBold, - }, - modifierItalic: { - fontStyle: 'italic', - }, - modifierUnderline: { - textDecoration: 'underline', - }, - modifierForegroundBlack: { - color: colors.common.black, - }, - modifierForegroundRed: { - color: colors.red[500], - }, - modifierForegroundGreen: { - color: colors.green[500], - }, - modifierForegroundYellow: { - color: colors.yellow[500], - }, - modifierForegroundBlue: { - color: colors.blue[500], - }, - modifierForegroundMagenta: { - color: colors.purple[500], - }, - modifierForegroundCyan: { - color: colors.cyan[500], - }, - modifierForegroundWhite: { - color: colors.common.white, - }, - modifierForegroundGrey: { - color: colors.grey[500], - }, - modifierBackgroundBlack: { - background: colors.common.black, - }, - modifierBackgroundRed: { - background: colors.red[500], - }, - modifierBackgroundGreen: { - background: colors.green[500], - }, - modifierBackgroundYellow: { - background: colors.yellow[500], - }, - modifierBackgroundBlue: { - background: colors.blue[500], - }, - modifierBackgroundMagenta: { - background: colors.purple[500], - }, - modifierBackgroundCyan: { - background: colors.cyan[500], - }, - modifierBackgroundWhite: { - background: colors.common.white, - }, - modifierBackgroundGrey: { - background: colors.grey[500], - }, -})); - -function getModifierClasses( - classes: ReturnType, - modifiers: ChunkModifiers, -) { - const classNames = new Array(); - if (modifiers.bold) { - classNames.push(classes.modifierBold); - } - if (modifiers.italic) { - classNames.push(classes.modifierItalic); - } - if (modifiers.underline) { - classNames.push(classes.modifierUnderline); - } - if (modifiers.foreground) { - const key = `modifierForeground${startCase( - modifiers.foreground, - )}` as keyof typeof classes; - classNames.push(classes[key]); - } - if (modifiers.background) { - const key = `modifierBackground${startCase( - modifiers.background, - )}` as keyof typeof classes; - classNames.push(classes[key]); - } - return classNames.length > 0 ? classNames.join(' ') : undefined; -} - -export function LogLine({ - line, - classes, - searchText, -}: { - line: AnsiLine; - classes: ReturnType; - searchText: string; -}) { - let searchResults: Array<{ start: number; end: number }> | undefined = - undefined; - if (searchText && line.text.includes(searchText)) { - searchResults = []; - let offset = 0; - for (;;) { - const start = line.text.indexOf(searchText, offset); - if (start === -1) { - break; - } - const end = start + searchText.length; - searchResults.push({ start, end }); - offset = end; - } - } - - const output = new Array(line.chunks.length); - - let key = 0; - let chunkOffset = 0; - let nextResult = searchResults?.shift(); - for (const { text, modifiers } of line.chunks) { - if (!nextResult || chunkOffset + text.length < nextResult.start) { - output.push( - - {text} - , - ); - chunkOffset += text.length; - continue; - } - - let localOffset = 0; - while (nextResult) { - let localStart = nextResult.start - chunkOffset; - if (localStart < 0) { - localStart = 0; - } - const localEnd = nextResult.end - chunkOffset; - const beforeMatch = text.slice(localOffset, localStart); - const match = text.slice(localStart, localEnd); - - if (beforeMatch) { - output.push( - - {beforeMatch} - , - ); - } - output.push( - - {match} - , - ); - - localOffset = localStart + match.length; - - if (match.length === searchText.length) { - nextResult = searchResults?.shift(); - } else { - break; - } - } - - if (localOffset < text.length) { - output.push( - - {text.slice(localOffset)} - , - ); - } - - chunkOffset += text.length; - } - return <>{output}; -} - export function LogViewer(props: LogViewerProps) { const { noLineNumbers } = props; const classes = useStyles(); diff --git a/packages/core-components/src/components/LogViewer/styles.ts b/packages/core-components/src/components/LogViewer/styles.ts new file mode 100644 index 0000000000..fd66571915 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/styles.ts @@ -0,0 +1,123 @@ +/* + * 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 { alpha, makeStyles } from '@material-ui/core/styles'; +import * as colors from '@material-ui/core/colors'; + +export const HEADER_SIZE = 40; + +export const useStyles = makeStyles(theme => ({ + root: { + background: theme.palette.background.paper, + }, + header: { + height: HEADER_SIZE, + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + }, + log: { + fontFamily: '"Monaco", monospace', + fontSize: theme.typography.fontSize, + }, + line: { + whiteSpace: 'pre', + + '&:hover': { + background: theme.palette.action.hover, + }, + }, + lineSelected: { + background: theme.palette.action.selected, + + '&:hover': { + background: theme.palette.action.selected, + }, + }, + lineNumber: { + display: 'inline-block', + textAlign: 'end', + width: 60, + marginRight: theme.spacing(1), + cursor: 'pointer', + }, + textHighlight: { + background: alpha(theme.palette.primary.main, 0.3), + }, + modifierBold: { + fontWeight: theme.typography.fontWeightBold, + }, + modifierItalic: { + fontStyle: 'italic', + }, + modifierUnderline: { + textDecoration: 'underline', + }, + modifierForegroundBlack: { + color: colors.common.black, + }, + modifierForegroundRed: { + color: colors.red[500], + }, + modifierForegroundGreen: { + color: colors.green[500], + }, + modifierForegroundYellow: { + color: colors.yellow[500], + }, + modifierForegroundBlue: { + color: colors.blue[500], + }, + modifierForegroundMagenta: { + color: colors.purple[500], + }, + modifierForegroundCyan: { + color: colors.cyan[500], + }, + modifierForegroundWhite: { + color: colors.common.white, + }, + modifierForegroundGrey: { + color: colors.grey[500], + }, + modifierBackgroundBlack: { + background: colors.common.black, + }, + modifierBackgroundRed: { + background: colors.red[500], + }, + modifierBackgroundGreen: { + background: colors.green[500], + }, + modifierBackgroundYellow: { + background: colors.yellow[500], + }, + modifierBackgroundBlue: { + background: colors.blue[500], + }, + modifierBackgroundMagenta: { + background: colors.purple[500], + }, + modifierBackgroundCyan: { + background: colors.cyan[500], + }, + modifierBackgroundWhite: { + background: colors.common.white, + }, + modifierBackgroundGrey: { + background: colors.grey[500], + }, +})); From 1faae751b683aa1ea21598d4b864f749d5514443 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 12:14:42 +0100 Subject: [PATCH 077/116] core-components: refactor LogViewer LogLine into pieces of logic Signed-off-by: Patrik Oldsberg --- .../src/components/LogViewer/LogLine.tsx | 119 ++++++++++-------- 1 file changed, 65 insertions(+), 54 deletions(-) diff --git a/packages/core-components/src/components/LogViewer/LogLine.tsx b/packages/core-components/src/components/LogViewer/LogLine.tsx index 7e5366eb77..66197e9865 100644 --- a/packages/core-components/src/components/LogViewer/LogLine.tsx +++ b/packages/core-components/src/components/LogViewer/LogLine.tsx @@ -15,12 +15,12 @@ */ import React from 'react'; -import { AnsiLine, ChunkModifiers } from './AnsiProcessor'; +import { AnsiChunk, AnsiLine, ChunkModifiers } from './AnsiProcessor'; import startCase from 'lodash/startCase'; import clsx from 'clsx'; -import { useStyles } from './useStyles'; +import { useStyles } from './styles'; -function getModifierClasses( +export function getModifierClasses( classes: ReturnType, modifiers: ChunkModifiers, ) { @@ -49,41 +49,45 @@ function getModifierClasses( return classNames.length > 0 ? classNames.join(' ') : undefined; } -export interface LogLineProps { - line: AnsiLine; - classes: ReturnType; - searchText: string; +export function findSearchResults(text: string, searchText: string) { + if (!searchText || !text.includes(searchText)) { + return undefined; + } + const searchResults = new Array<{ start: number; end: number }>(); + let offset = 0; + for (;;) { + const start = text.indexOf(searchText, offset); + if (start === -1) { + break; + } + const end = start + searchText.length; + searchResults.push({ start, end }); + offset = end; + } + return searchResults; } -export function LogLine({ line, classes, searchText }: LogLineProps) { - let searchResults: Array<{ start: number; end: number }> | undefined = - undefined; - if (searchText && line.text.includes(searchText)) { - searchResults = []; - let offset = 0; - for (;;) { - const start = line.text.indexOf(searchText, offset); - if (start === -1) { - break; - } - const end = start + searchText.length; - searchResults.push({ start, end }); - offset = end; - } +export interface HighlightAnsiChunk extends AnsiChunk { + highlight?: boolean; +} + +export function calculateHighlightedChunks( + line: AnsiLine, + searchText: string, +): HighlightAnsiChunk[] { + const results = findSearchResults(line.text, searchText); + if (!results) { + return line.chunks; } - const output = new Array(line.chunks.length); + const chunks = new Array(); - let key = 0; let chunkOffset = 0; - let nextResult = searchResults?.shift(); - for (const { text, modifiers } of line.chunks) { + let nextResult = results.shift(); + for (const chunk of line.chunks) { + const { text, modifiers } = chunk; if (!nextResult || chunkOffset + text.length < nextResult.start) { - output.push( - - {text} - , - ); + chunks.push(chunk); chunkOffset += text.length; continue; } @@ -99,42 +103,49 @@ export function LogLine({ line, classes, searchText }: LogLineProps) { const match = text.slice(localStart, localEnd); if (beforeMatch) { - output.push( - - {beforeMatch} - , - ); + chunks.push({ text: beforeMatch, modifiers }); } - output.push( - - {match} - , - ); + chunks.push({ text: match, modifiers, highlight: true }); localOffset = localStart + match.length; if (match.length === searchText.length) { - nextResult = searchResults?.shift(); + nextResult = results.shift(); } else { break; } } if (localOffset < text.length) { - output.push( - - {text.slice(localOffset)} - , - ); + chunks.push({ text: text.slice(localOffset), modifiers }); } chunkOffset += text.length; } - return <>{output}; + + return chunks; +} + +export interface LogLineProps { + line: AnsiLine; + classes: ReturnType; + searchText: string; +} + +export function LogLine({ line, classes, searchText }: LogLineProps) { + const chunks = calculateHighlightedChunks(line, searchText); + + const elements = chunks.map(({ text, modifiers, highlight }, index) => ( + + {text} + + )); + + return <>{elements}; } From ef111abb56566f8d1d761d34f2a4935ec525bdd6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 12:32:52 +0100 Subject: [PATCH 078/116] core-components: add tests for LogLine modifiers Signed-off-by: Patrik Oldsberg --- .../src/components/LogViewer/LogLine.test.tsx | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 packages/core-components/src/components/LogViewer/LogLine.test.tsx diff --git a/packages/core-components/src/components/LogViewer/LogLine.test.tsx b/packages/core-components/src/components/LogViewer/LogLine.test.tsx new file mode 100644 index 0000000000..2b4513221a --- /dev/null +++ b/packages/core-components/src/components/LogViewer/LogLine.test.tsx @@ -0,0 +1,67 @@ +/* + * 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 { ChunkModifiers } from './AnsiProcessor'; +import { getModifierClasses } from './LogLine'; + +describe('getModifierClasses', () => { + const classes = { + modifierBold: 'bold', + modifierItalic: 'italic', + modifierUnderline: 'underline', + modifierForegroundBlack: 'black', + modifierForegroundRed: 'red', + modifierForegroundGreen: 'green', + modifierForegroundYellow: 'yellow', + modifierForegroundBlue: 'blue', + modifierForegroundMagenta: 'magenta', + modifierForegroundCyan: 'cyan', + modifierForegroundWhite: 'white', + modifierForegroundGrey: 'grey', + modifierBackgroundBlack: 'bg-black', + modifierBackgroundRed: 'bg-red', + modifierBackgroundGreen: 'bg-green', + modifierBackgroundYellow: 'bg-yellow', + modifierBackgroundBlue: 'bg-blue', + modifierBackgroundMagenta: 'bg-magenta', + modifierBackgroundCyan: 'bg-cyan', + modifierBackgroundWhite: 'bg-white', + modifierBackgroundGrey: 'bg-grey', + }; + const curried = (modifiers: ChunkModifiers) => + getModifierClasses( + classes as Parameters[0], + modifiers, + ); + + it('should transform modifiers to classes', () => { + expect(curried({})).toEqual(undefined); + expect(curried({ bold: true })).toEqual('bold'); + expect(curried({ italic: true })).toEqual('italic'); + expect(curried({ underline: true })).toEqual('underline'); + expect(curried({ foreground: 'black' })).toEqual('black'); + expect(curried({ background: 'black' })).toEqual('bg-black'); + expect( + curried({ + bold: true, + italic: true, + underline: true, + foreground: 'red', + background: 'red', + }), + ).toEqual('bold italic underline red bg-red'); + }); +}); From 4dd0511074566f61ae3bc94bc5c80dd48bf87a3e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 12:42:37 +0100 Subject: [PATCH 079/116] core-components: add tests for LogLine search results Signed-off-by: Patrik Oldsberg --- .../src/components/LogViewer/LogLine.test.tsx | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/core-components/src/components/LogViewer/LogLine.test.tsx b/packages/core-components/src/components/LogViewer/LogLine.test.tsx index 2b4513221a..a4033f9221 100644 --- a/packages/core-components/src/components/LogViewer/LogLine.test.tsx +++ b/packages/core-components/src/components/LogViewer/LogLine.test.tsx @@ -15,7 +15,7 @@ */ import { ChunkModifiers } from './AnsiProcessor'; -import { getModifierClasses } from './LogLine'; +import { findSearchResults, getModifierClasses } from './LogLine'; describe('getModifierClasses', () => { const classes = { @@ -65,3 +65,48 @@ describe('getModifierClasses', () => { ).toEqual('bold italic underline red bg-red'); }); }); + +describe('findSearchResults', () => { + it('should not return results if there is no match', () => { + expect(findSearchResults('Foo', 'Bar')).toEqual(undefined); + expect(findSearchResults('Foo Bar', 'oof')).toEqual(undefined); + expect(findSearchResults('Foo Bar', '')).toEqual(undefined); + expect(findSearchResults('', '')).toEqual(undefined); + expect(findSearchResults('', 'Foo')).toEqual(undefined); + }); + + it('should find result indices', () => { + expect(findSearchResults('Foo', 'Foo')).toEqual([{ start: 0, end: 3 }]); + expect(findSearchResults('Foo', 'o')).toEqual([ + { start: 1, end: 2 }, + { start: 2, end: 3 }, + ]); + expect(findSearchResults('FooBarBaz', 'Bar')).toEqual([ + { start: 3, end: 6 }, + ]); + expect(findSearchResults('Foo Bar Baz', ' ')).toEqual([ + { start: 3, end: 4 }, + { start: 7, end: 8 }, + ]); + expect(findSearchResults('FooBarBazBarFoo', 'Bar')).toEqual([ + { start: 3, end: 6 }, + { start: 9, end: 12 }, + ]); + expect(findSearchResults('FooBarBazBarFoo', 'Foo')).toEqual([ + { start: 0, end: 3 }, + { start: 12, end: 15 }, + ]); + }); + + it('should not overlap search results', () => { + expect(findSearchResults('aaa', 'aa')).toEqual([{ start: 0, end: 2 }]); + expect(findSearchResults('aaaa', 'aa')).toEqual([ + { start: 0, end: 2 }, + { start: 2, end: 4 }, + ]); + expect(findSearchResults('aaaaa', 'aa')).toEqual([ + { start: 0, end: 2 }, + { start: 2, end: 4 }, + ]); + }); +}); From 563635a0f8decf17427ff8d61b6adf6f6562b420 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 14:54:45 +0100 Subject: [PATCH 080/116] core-components: add tests for LogLine highlighting + fix and refactor Signed-off-by: Patrik Oldsberg --- .../src/components/LogViewer/LogLine.test.tsx | 248 +++++++++++++++++- .../src/components/LogViewer/LogLine.tsx | 43 +-- 2 files changed, 272 insertions(+), 19 deletions(-) diff --git a/packages/core-components/src/components/LogViewer/LogLine.test.tsx b/packages/core-components/src/components/LogViewer/LogLine.test.tsx index a4033f9221..ce98746b01 100644 --- a/packages/core-components/src/components/LogViewer/LogLine.test.tsx +++ b/packages/core-components/src/components/LogViewer/LogLine.test.tsx @@ -14,8 +14,12 @@ * limitations under the License. */ -import { ChunkModifiers } from './AnsiProcessor'; -import { findSearchResults, getModifierClasses } from './LogLine'; +import { AnsiLine, ChunkModifiers } from './AnsiProcessor'; +import { + calculateHighlightedChunks, + findSearchResults, + getModifierClasses, +} from './LogLine'; describe('getModifierClasses', () => { const classes = { @@ -110,3 +114,243 @@ describe('findSearchResults', () => { ]); }); }); + +describe('calculateHighlightedChunks', () => { + it('should pass through chunks if there are no results', () => { + const chunks = [{ text: 'Foo', modifiers: {} }]; + const line = new AnsiLine(0, chunks); + expect(calculateHighlightedChunks(line, 'bar')).toBe(chunks); + }); + + it('should highlight one result from plain text', () => { + const line = new AnsiLine(0, [{ text: 'FooBarBaz', modifiers: {} }]); + expect(calculateHighlightedChunks(line, 'foo')).toEqual([ + { + text: 'Foo', + modifiers: {}, + highlight: true, + }, + { + text: 'BarBaz', + modifiers: {}, + }, + ]); + expect(calculateHighlightedChunks(line, 'bar')).toEqual([ + { + text: 'Foo', + modifiers: {}, + }, + { + text: 'Bar', + modifiers: {}, + highlight: true, + }, + { + text: 'Baz', + modifiers: {}, + }, + ]); + expect(calculateHighlightedChunks(line, 'baz')).toEqual([ + { + text: 'FooBar', + modifiers: {}, + }, + { + text: 'Baz', + modifiers: {}, + highlight: true, + }, + ]); + }); + + it('should highlight multiple results from plain text', () => { + const line = new AnsiLine(0, [ + { text: 'FooBarBazBazBarFoo', modifiers: {} }, + ]); + expect(calculateHighlightedChunks(line, 'foo')).toEqual([ + { + text: 'Foo', + modifiers: {}, + highlight: true, + }, + { + text: 'BarBazBazBar', + modifiers: {}, + }, + { + text: 'Foo', + modifiers: {}, + highlight: true, + }, + ]); + expect(calculateHighlightedChunks(line, 'bar')).toEqual([ + { + text: 'Foo', + modifiers: {}, + }, + { + text: 'Bar', + modifiers: {}, + highlight: true, + }, + { + text: 'BazBaz', + modifiers: {}, + }, + { + text: 'Bar', + modifiers: {}, + highlight: true, + }, + { + text: 'Foo', + modifiers: {}, + }, + ]); + expect(calculateHighlightedChunks(line, 'baz')).toEqual([ + { + text: 'FooBar', + modifiers: {}, + }, + { + text: 'Baz', + modifiers: {}, + highlight: true, + }, + { + text: 'Baz', + modifiers: {}, + highlight: true, + }, + { + text: 'BarFoo', + modifiers: {}, + }, + ]); + }); + + it('should forward modifiers to result', () => { + const line = new AnsiLine(0, [ + { text: 'FooBarBazBazBarFoo', modifiers: { bold: true } }, + ]); + expect(calculateHighlightedChunks(line, 'foo')).toEqual([ + { + text: 'Foo', + modifiers: { bold: true }, + highlight: true, + }, + { + text: 'BarBazBazBar', + modifiers: { bold: true }, + }, + { + text: 'Foo', + modifiers: { bold: true }, + highlight: true, + }, + ]); + }); + + it('should highlight full chunks', () => { + const line = new AnsiLine(0, [ + { text: 'Foo', modifiers: { bold: true } }, + { text: 'BarBaz', modifiers: { bold: true } }, + { text: 'BazBar', modifiers: { italic: true } }, + { text: 'Foo', modifiers: { italic: true } }, + ]); + expect(calculateHighlightedChunks(line, 'foo')).toEqual([ + { + text: 'Foo', + modifiers: { bold: true }, + highlight: true, + }, + { + text: 'BarBaz', + modifiers: { bold: true }, + }, + { + text: 'BazBar', + modifiers: { italic: true }, + }, + { + text: 'Foo', + modifiers: { italic: true }, + highlight: true, + }, + ]); + }); + + it('should highlight partial chunks', () => { + const line = new AnsiLine(0, [ + { text: 'Fo', modifiers: { bold: true } }, + { text: 'oFooFo', modifiers: {} }, + { text: 'oBarBaz', modifiers: { italic: true } }, + { text: 'Foo', modifiers: { foreground: 'blue' } }, + { text: 'FooFoo', modifiers: { italic: true } }, + { text: 'F', modifiers: { bold: true } }, + { text: 'o', modifiers: {} }, + { text: 'o', modifiers: { bold: true } }, + ]); + expect(calculateHighlightedChunks(line, 'foo')).toEqual([ + { + text: 'Fo', + modifiers: { bold: true }, + highlight: true, + }, + { + text: 'o', + modifiers: {}, + highlight: true, + }, + { + text: 'Foo', + modifiers: {}, + highlight: true, + }, + { + text: 'Fo', + modifiers: {}, + highlight: true, + }, + { + text: 'o', + modifiers: { italic: true }, + highlight: true, + }, + { + text: 'BarBaz', + modifiers: { italic: true }, + }, + { + text: 'Foo', + modifiers: { foreground: 'blue' }, + highlight: true, + }, + { + text: 'Foo', + modifiers: { italic: true }, + highlight: true, + }, + { + text: 'Foo', + modifiers: { italic: true }, + highlight: true, + }, + { + text: 'F', + modifiers: { bold: true }, + highlight: true, + }, + { + text: 'o', + modifiers: {}, + highlight: true, + }, + { + text: 'o', + modifiers: { bold: true }, + highlight: true, + }, + ]); + }); +}); diff --git a/packages/core-components/src/components/LogViewer/LogLine.tsx b/packages/core-components/src/components/LogViewer/LogLine.tsx index 66197e9865..315ecb0222 100644 --- a/packages/core-components/src/components/LogViewer/LogLine.tsx +++ b/packages/core-components/src/components/LogViewer/LogLine.tsx @@ -82,45 +82,54 @@ export function calculateHighlightedChunks( const chunks = new Array(); - let chunkOffset = 0; + let lineOffset = 0; let nextResult = results.shift(); for (const chunk of line.chunks) { const { text, modifiers } = chunk; - if (!nextResult || chunkOffset + text.length < nextResult.start) { + if (!nextResult || lineOffset + text.length < nextResult.start) { chunks.push(chunk); - chunkOffset += text.length; + lineOffset += text.length; continue; } let localOffset = 0; while (nextResult) { - let localStart = nextResult.start - chunkOffset; - if (localStart < 0) { - localStart = 0; + const localStart = Math.max(nextResult.start - lineOffset, 0); + if (localStart > text.length) { + break; // The next result is not in this chunk } - const localEnd = nextResult.end - chunkOffset; - const beforeMatch = text.slice(localOffset, localStart); - const match = text.slice(localStart, localEnd); - if (beforeMatch) { - chunks.push({ text: beforeMatch, modifiers }); + const localEnd = Math.min(nextResult.end - lineOffset, text.length); + + const hasTextBeforeResult = localStart > localOffset; + if (hasTextBeforeResult) { + chunks.push({ text: text.slice(localOffset, localStart), modifiers }); + } + const hasResultText = localEnd > localStart; + if (hasResultText) { + chunks.push({ + modifiers, + highlight: true, + text: text.slice(localStart, localEnd), + }); } - chunks.push({ text: match, modifiers, highlight: true }); - localOffset = localStart + match.length; + localOffset = localEnd; - if (match.length === searchText.length) { + const foundCompleteResult = nextResult.end - lineOffset === localEnd; + if (foundCompleteResult) { nextResult = results.shift(); } else { - break; + break; // The rest of the result is in the following chunks } } - if (localOffset < text.length) { + const hasTextAfterResult = localOffset < text.length; + if (hasTextAfterResult) { chunks.push({ text: text.slice(localOffset), modifiers }); } - chunkOffset += text.length; + lineOffset += text.length; } return chunks; From 155da5097840d32266ccafea00f4466b9de2923a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 15:59:12 +0100 Subject: [PATCH 081/116] core-components: make LogViewer filter scroll to match instead of filter Signed-off-by: Patrik Oldsberg --- .../src/components/LogViewer/LogViewer.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index d6722edbcd..1b7b7c2c38 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList } from 'react-window'; import { AnsiProcessor } from './AnsiProcessor'; @@ -49,6 +49,7 @@ export interface ChunkModifiers { export function LogViewer(props: LogViewerProps) { const { noLineNumbers } = props; + const listRef = useRef(null); const classes = useStyles(); const [selectedLine, setSelectedLine] = useState(); const [searchInput, setSearchInput] = useState(''); @@ -85,6 +86,15 @@ export function LogViewer(props: LogViewerProps) { return lines.filter(line => line.text.includes(searchText)); }, [lines, searchText]); + const [foundLine] = filteredLines; + const scrollToLineNumber = foundLine?.lineNumber; + + useEffect(() => { + if (scrollToLineNumber !== undefined && listRef.current) { + listRef.current.scrollToItem(scrollToLineNumber - 1, 'center'); + } + }, [scrollToLineNumber]); + return ( {({ height, width }) => ( @@ -99,12 +109,13 @@ export function LogViewer(props: LogViewerProps) { /> {({ index, style, data }) => { const line = data[index]; From 1407c0085a520eac3f68154aef81d58ccd27660f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 17:27:33 +0100 Subject: [PATCH 082/116] core-components: more advanced LogViewer filter controls Signed-off-by: Patrik Oldsberg --- .../src/components/LogViewer/LogViewer.tsx | 199 +++++++++++++----- 1 file changed, 145 insertions(+), 54 deletions(-) diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index 1b7b7c2c38..7ff24acda5 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -17,15 +17,20 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList } from 'react-window'; -import { AnsiProcessor } from './AnsiProcessor'; +import Typography from '@material-ui/core/Typography'; +import IconButton from '@material-ui/core/IconButton'; +import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; +import ChevronRightIcon from '@material-ui/icons/ChevronRight'; +import FilterListIcon from '@material-ui/icons/FilterList'; +import { AnsiLine, AnsiProcessor } from './AnsiProcessor'; import { HEADER_SIZE, useStyles } from './styles'; import clsx from 'clsx'; import TextField from '@material-ui/core/TextField'; import { LogLine } from './LogLine'; +import { useToggle } from 'react-use'; export interface LogViewerProps { text: string; - noLineNumbers?: boolean; } export type AnsiColor = @@ -47,11 +52,117 @@ export interface ChunkModifiers { underline?: boolean; } +function applySearchFilter(lines: AnsiLine[], searchText: string) { + if (!searchText) { + return { lines }; + } + + const matchingLines = []; + const searchResults = []; + for (const line of lines) { + if (line.text.includes(searchText)) { + matchingLines.push(line); + + let offset = 0; + let lineResultIndex = 0; + for (;;) { + const start = line.text.indexOf(searchText, offset); + if (start === -1) { + break; + } + searchResults.push({ + lineNumber: line.lineNumber, + lineIndex: lineResultIndex++, + }); + offset = start + searchText.length; + } + } + } + + return { + lines: matchingLines, + results: searchResults, + }; +} + +function LogViewerControls(props: { + search: string; + onSearchChange: (search: string) => void; + resultIndex: number | undefined; + resultCount: number | undefined; + onResultIndexChange: (index: number) => void; + shouldFilter: boolean; + onToggleShouldFilter: () => void; +}) { + const { resultCount, onResultIndexChange, onToggleShouldFilter } = props; + const resultIndex = props.resultIndex ?? 0; + + const increment = () => { + if (resultCount !== undefined) { + const next = resultIndex + 1; + onResultIndexChange(next >= resultCount ? 0 : next); + } + }; + + const decrement = () => { + if (resultCount !== undefined) { + const next = resultIndex - 1; + onResultIndexChange(next < 0 ? resultCount - 1 : next); + } + }; + + const handleKeyPress = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + if (event.metaKey || event.ctrlKey || event.altKey) { + onToggleShouldFilter(); + } else if (event.shiftKey) { + decrement(); + } else { + increment(); + } + } + }; + + return ( + <> + {resultCount !== undefined && ( + <> + + + + + {Math.min(resultIndex + 1, resultCount)}/{resultCount} + + + + + + )} + props.onSearchChange(e.target.value)} + /> + + {props.shouldFilter ? ( + + ) : ( + + )} + + + ); +} + export function LogViewer(props: LogViewerProps) { - const { noLineNumbers } = props; - const listRef = useRef(null); const classes = useStyles(); + const listRef = useRef(null); const [selectedLine, setSelectedLine] = useState(); + const [resultIndex, setResultIndex] = useState(); + const [shouldFilter, toggleShouldFilter] = useToggle(false); const [searchInput, setSearchInput] = useState(''); const searchText = searchInput.toLocaleLowerCase('en-US'); @@ -59,53 +170,35 @@ export function LogViewer(props: LogViewerProps) { const processor = useMemo(() => new AnsiProcessor(), []); const lines = processor.process(props.text); - const filteredLines = useMemo(() => { - if (!searchText) { - return lines; - } - const matchingLines = []; - const searchResults = []; - for (const line of lines) { - if (line.text.includes(searchText)) { - matchingLines.push(line); + const filter = useMemo( + () => applySearchFilter(lines, searchText), + [lines, searchText], + ); - const lineResults = []; - let offset = 0; - for (;;) { - const start = line.text.indexOf(searchText, offset); - if (start === -1) { - break; - } - const end = start + searchText.length; - lineResults.push({ start, end }); - offset = end; - } - searchResults.push(lineResults); - } - } - return lines.filter(line => line.text.includes(searchText)); - }, [lines, searchText]); + const searchResult = filter.results?.[resultIndex ?? 0]; + const searchResultLine = searchResult?.lineNumber; - const [foundLine] = filteredLines; - const scrollToLineNumber = foundLine?.lineNumber; + const displayLines = shouldFilter ? filter.lines : lines; useEffect(() => { - if (scrollToLineNumber !== undefined && listRef.current) { - listRef.current.scrollToItem(scrollToLineNumber - 1, 'center'); + if (searchResultLine !== undefined && listRef.current) { + listRef.current.scrollToItem(searchResultLine - 1, 'center'); } - }, [scrollToLineNumber]); + }, [searchResultLine]); return ( {({ height, width }) => (
- setSearchInput(e.target.value)} +
{({ index, style, data }) => { const line = data[index]; @@ -127,18 +220,16 @@ export function LogViewer(props: LogViewerProps) { [classes.lineSelected]: selectedLine === lineNumber, })} > - {!noLineNumbers && ( - setSelectedLine(lineNumber)} - onKeyPress={() => setSelectedLine(lineNumber)} - > - {lineNumber} - - )} + setSelectedLine(lineNumber)} + onKeyPress={() => setSelectedLine(lineNumber)} + > + {lineNumber} + Date: Sun, 5 Dec 2021 17:45:06 +0100 Subject: [PATCH 083/116] core-components: highlight individual LogViewer search results Signed-off-by: Patrik Oldsberg --- .../src/components/LogViewer/LogLine.test.tsx | 48 +++++++++---------- .../src/components/LogViewer/LogLine.tsx | 33 ++++++++----- .../src/components/LogViewer/LogViewer.tsx | 5 ++ .../src/components/LogViewer/styles.ts | 5 +- 4 files changed, 55 insertions(+), 36 deletions(-) diff --git a/packages/core-components/src/components/LogViewer/LogLine.test.tsx b/packages/core-components/src/components/LogViewer/LogLine.test.tsx index ce98746b01..c5fd6e39ca 100644 --- a/packages/core-components/src/components/LogViewer/LogLine.test.tsx +++ b/packages/core-components/src/components/LogViewer/LogLine.test.tsx @@ -128,7 +128,7 @@ describe('calculateHighlightedChunks', () => { { text: 'Foo', modifiers: {}, - highlight: true, + highlight: 0, }, { text: 'BarBaz', @@ -143,7 +143,7 @@ describe('calculateHighlightedChunks', () => { { text: 'Bar', modifiers: {}, - highlight: true, + highlight: 0, }, { text: 'Baz', @@ -158,7 +158,7 @@ describe('calculateHighlightedChunks', () => { { text: 'Baz', modifiers: {}, - highlight: true, + highlight: 0, }, ]); }); @@ -171,7 +171,7 @@ describe('calculateHighlightedChunks', () => { { text: 'Foo', modifiers: {}, - highlight: true, + highlight: 0, }, { text: 'BarBazBazBar', @@ -180,7 +180,7 @@ describe('calculateHighlightedChunks', () => { { text: 'Foo', modifiers: {}, - highlight: true, + highlight: 1, }, ]); expect(calculateHighlightedChunks(line, 'bar')).toEqual([ @@ -191,7 +191,7 @@ describe('calculateHighlightedChunks', () => { { text: 'Bar', modifiers: {}, - highlight: true, + highlight: 0, }, { text: 'BazBaz', @@ -200,7 +200,7 @@ describe('calculateHighlightedChunks', () => { { text: 'Bar', modifiers: {}, - highlight: true, + highlight: 1, }, { text: 'Foo', @@ -215,12 +215,12 @@ describe('calculateHighlightedChunks', () => { { text: 'Baz', modifiers: {}, - highlight: true, + highlight: 0, }, { text: 'Baz', modifiers: {}, - highlight: true, + highlight: 1, }, { text: 'BarFoo', @@ -237,7 +237,7 @@ describe('calculateHighlightedChunks', () => { { text: 'Foo', modifiers: { bold: true }, - highlight: true, + highlight: 0, }, { text: 'BarBazBazBar', @@ -246,7 +246,7 @@ describe('calculateHighlightedChunks', () => { { text: 'Foo', modifiers: { bold: true }, - highlight: true, + highlight: 1, }, ]); }); @@ -262,7 +262,7 @@ describe('calculateHighlightedChunks', () => { { text: 'Foo', modifiers: { bold: true }, - highlight: true, + highlight: 0, }, { text: 'BarBaz', @@ -275,7 +275,7 @@ describe('calculateHighlightedChunks', () => { { text: 'Foo', modifiers: { italic: true }, - highlight: true, + highlight: 1, }, ]); }); @@ -295,27 +295,27 @@ describe('calculateHighlightedChunks', () => { { text: 'Fo', modifiers: { bold: true }, - highlight: true, + highlight: 0, }, { text: 'o', modifiers: {}, - highlight: true, + highlight: 0, }, { text: 'Foo', modifiers: {}, - highlight: true, + highlight: 1, }, { text: 'Fo', modifiers: {}, - highlight: true, + highlight: 2, }, { text: 'o', modifiers: { italic: true }, - highlight: true, + highlight: 2, }, { text: 'BarBaz', @@ -324,32 +324,32 @@ describe('calculateHighlightedChunks', () => { { text: 'Foo', modifiers: { foreground: 'blue' }, - highlight: true, + highlight: 3, }, { text: 'Foo', modifiers: { italic: true }, - highlight: true, + highlight: 4, }, { text: 'Foo', modifiers: { italic: true }, - highlight: true, + highlight: 5, }, { text: 'F', modifiers: { bold: true }, - highlight: true, + highlight: 6, }, { text: 'o', modifiers: {}, - highlight: true, + highlight: 6, }, { text: 'o', modifiers: { bold: true }, - highlight: true, + highlight: 6, }, ]); }); diff --git a/packages/core-components/src/components/LogViewer/LogLine.tsx b/packages/core-components/src/components/LogViewer/LogLine.tsx index 315ecb0222..f1296982b0 100644 --- a/packages/core-components/src/components/LogViewer/LogLine.tsx +++ b/packages/core-components/src/components/LogViewer/LogLine.tsx @@ -68,7 +68,7 @@ export function findSearchResults(text: string, searchText: string) { } export interface HighlightAnsiChunk extends AnsiChunk { - highlight?: boolean; + highlight?: number; } export function calculateHighlightedChunks( @@ -83,23 +83,24 @@ export function calculateHighlightedChunks( const chunks = new Array(); let lineOffset = 0; - let nextResult = results.shift(); + let resultIndex = 0; + let result = results[resultIndex]; for (const chunk of line.chunks) { const { text, modifiers } = chunk; - if (!nextResult || lineOffset + text.length < nextResult.start) { + if (!result || lineOffset + text.length < result.start) { chunks.push(chunk); lineOffset += text.length; continue; } let localOffset = 0; - while (nextResult) { - const localStart = Math.max(nextResult.start - lineOffset, 0); + while (result) { + const localStart = Math.max(result.start - lineOffset, 0); if (localStart > text.length) { break; // The next result is not in this chunk } - const localEnd = Math.min(nextResult.end - lineOffset, text.length); + const localEnd = Math.min(result.end - lineOffset, text.length); const hasTextBeforeResult = localStart > localOffset; if (hasTextBeforeResult) { @@ -109,16 +110,17 @@ export function calculateHighlightedChunks( if (hasResultText) { chunks.push({ modifiers, - highlight: true, + highlight: resultIndex, text: text.slice(localStart, localEnd), }); } localOffset = localEnd; - const foundCompleteResult = nextResult.end - lineOffset === localEnd; + const foundCompleteResult = result.end - lineOffset === localEnd; if (foundCompleteResult) { - nextResult = results.shift(); + resultIndex += 1; + result = results[resultIndex]; } else { break; // The rest of the result is in the following chunks } @@ -139,9 +141,15 @@ export interface LogLineProps { line: AnsiLine; classes: ReturnType; searchText: string; + highlightResultIndex?: number; } -export function LogLine({ line, classes, searchText }: LogLineProps) { +export function LogLine({ + line, + classes, + searchText, + highlightResultIndex, +}: LogLineProps) { const chunks = calculateHighlightedChunks(line, searchText); const elements = chunks.map(({ text, modifiers, highlight }, index) => ( @@ -149,7 +157,10 @@ export function LogLine({ line, classes, searchText }: LogLineProps) { key={index} className={clsx( getModifierClasses(classes, modifiers), - highlight && classes.textHighlight, + highlight !== undefined && + (highlight === highlightResultIndex + ? classes.textSelectedHighlight + : classes.textHighlight), )} > {text} diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index 7ff24acda5..f974f9bb90 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -234,6 +234,11 @@ export function LogViewer(props: LogViewerProps) { line={line} classes={classes} searchText={searchText} + highlightResultIndex={ + searchResultLine === lineNumber + ? searchResult!.lineIndex + : undefined + } />
); diff --git a/packages/core-components/src/components/LogViewer/styles.ts b/packages/core-components/src/components/LogViewer/styles.ts index fd66571915..25db095645 100644 --- a/packages/core-components/src/components/LogViewer/styles.ts +++ b/packages/core-components/src/components/LogViewer/styles.ts @@ -55,7 +55,10 @@ export const useStyles = makeStyles(theme => ({ cursor: 'pointer', }, textHighlight: { - background: alpha(theme.palette.primary.main, 0.3), + background: alpha(theme.palette.info.main, 0.15), + }, + textSelectedHighlight: { + background: alpha(theme.palette.info.main, 0.4), }, modifierBold: { fontWeight: theme.typography.fontWeightBold, From 8a71f91bf270a240280bfd15991ea461fba52bcc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 18:00:37 +0100 Subject: [PATCH 084/116] core-components: split out LogViewerControls Signed-off-by: Patrik Oldsberg --- .../src/components/LogViewer/LogViewer.tsx | 79 +-------------- .../LogViewer/LogViewerControls.tsx | 97 +++++++++++++++++++ 2 files changed, 98 insertions(+), 78 deletions(-) create mode 100644 packages/core-components/src/components/LogViewer/LogViewerControls.tsx diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index f974f9bb90..aaf9949a74 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -17,16 +17,11 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList } from 'react-window'; -import Typography from '@material-ui/core/Typography'; -import IconButton from '@material-ui/core/IconButton'; -import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; -import ChevronRightIcon from '@material-ui/icons/ChevronRight'; -import FilterListIcon from '@material-ui/icons/FilterList'; import { AnsiLine, AnsiProcessor } from './AnsiProcessor'; import { HEADER_SIZE, useStyles } from './styles'; import clsx from 'clsx'; -import TextField from '@material-ui/core/TextField'; import { LogLine } from './LogLine'; +import { LogViewerControls } from './LogViewerControls'; import { useToggle } from 'react-use'; export interface LogViewerProps { @@ -85,78 +80,6 @@ function applySearchFilter(lines: AnsiLine[], searchText: string) { }; } -function LogViewerControls(props: { - search: string; - onSearchChange: (search: string) => void; - resultIndex: number | undefined; - resultCount: number | undefined; - onResultIndexChange: (index: number) => void; - shouldFilter: boolean; - onToggleShouldFilter: () => void; -}) { - const { resultCount, onResultIndexChange, onToggleShouldFilter } = props; - const resultIndex = props.resultIndex ?? 0; - - const increment = () => { - if (resultCount !== undefined) { - const next = resultIndex + 1; - onResultIndexChange(next >= resultCount ? 0 : next); - } - }; - - const decrement = () => { - if (resultCount !== undefined) { - const next = resultIndex - 1; - onResultIndexChange(next < 0 ? resultCount - 1 : next); - } - }; - - const handleKeyPress = (event: React.KeyboardEvent) => { - if (event.key === 'Enter') { - if (event.metaKey || event.ctrlKey || event.altKey) { - onToggleShouldFilter(); - } else if (event.shiftKey) { - decrement(); - } else { - increment(); - } - } - }; - - return ( - <> - {resultCount !== undefined && ( - <> - - - - - {Math.min(resultIndex + 1, resultCount)}/{resultCount} - - - - - - )} - props.onSearchChange(e.target.value)} - /> - - {props.shouldFilter ? ( - - ) : ( - - )} - - - ); -} - export function LogViewer(props: LogViewerProps) { const classes = useStyles(); const listRef = useRef(null); diff --git a/packages/core-components/src/components/LogViewer/LogViewerControls.tsx b/packages/core-components/src/components/LogViewer/LogViewerControls.tsx new file mode 100644 index 0000000000..f7c06edd05 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/LogViewerControls.tsx @@ -0,0 +1,97 @@ +/* + * 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 from 'react'; +import IconButton from '@material-ui/core/IconButton'; +import TextField from '@material-ui/core/TextField'; +import Typography from '@material-ui/core/Typography'; +import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; +import ChevronRightIcon from '@material-ui/icons/ChevronRight'; +import FilterListIcon from '@material-ui/icons/FilterList'; + +export interface LogViewerControlsProps { + search: string; + onSearchChange: (search: string) => void; + resultIndex: number | undefined; + resultCount: number | undefined; + onResultIndexChange: (index: number) => void; + shouldFilter: boolean; + onToggleShouldFilter: () => void; +} + +export function LogViewerControls(props: LogViewerControlsProps) { + const { resultCount, onResultIndexChange, onToggleShouldFilter } = props; + const resultIndex = props.resultIndex ?? 0; + + const increment = () => { + if (resultCount !== undefined) { + const next = resultIndex + 1; + onResultIndexChange(next >= resultCount ? 0 : next); + } + }; + + const decrement = () => { + if (resultCount !== undefined) { + const next = resultIndex - 1; + onResultIndexChange(next < 0 ? resultCount - 1 : next); + } + }; + + const handleKeyPress = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + if (event.metaKey || event.ctrlKey || event.altKey) { + onToggleShouldFilter(); + } else if (event.shiftKey) { + decrement(); + } else { + increment(); + } + } + }; + + return ( + <> + {resultCount !== undefined && ( + <> + + + + + {Math.min(resultIndex + 1, resultCount)}/{resultCount} + + + + + + )} + props.onSearchChange(e.target.value)} + /> + + {props.shouldFilter ? ( + + ) : ( + + )} + + + ); +} From b38e4c636beebb0410097c8da6a451ca87b85b0f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 18:01:20 +0100 Subject: [PATCH 085/116] core-components: remove duplicate types from LogViewer Signed-off-by: Patrik Oldsberg --- .../src/components/LogViewer/LogViewer.tsx | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index aaf9949a74..24f10ca5f6 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -28,25 +28,6 @@ export interface LogViewerProps { text: string; } -export type AnsiColor = - | 'black' - | 'red' - | 'green' - | 'yellow' - | 'blue' - | 'magenta' - | 'cyan' - | 'white' - | 'grey'; - -export interface ChunkModifiers { - foreground?: AnsiColor; - background?: AnsiColor; - bold?: boolean; - italic?: boolean; - underline?: boolean; -} - function applySearchFilter(lines: AnsiLine[], searchText: string) { if (!searchText) { return { lines }; From ed042d205692b3327937c0f1fc4de3ac3a6559f5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 18:39:11 +0100 Subject: [PATCH 086/116] core-components: memo LogViewer LogLines Signed-off-by: Patrik Oldsberg --- .../src/components/LogViewer/LogLine.tsx | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/packages/core-components/src/components/LogViewer/LogLine.tsx b/packages/core-components/src/components/LogViewer/LogLine.tsx index f1296982b0..c14df4a45a 100644 --- a/packages/core-components/src/components/LogViewer/LogLine.tsx +++ b/packages/core-components/src/components/LogViewer/LogLine.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { AnsiChunk, AnsiLine, ChunkModifiers } from './AnsiProcessor'; import startCase from 'lodash/startCase'; import clsx from 'clsx'; @@ -150,22 +150,29 @@ export function LogLine({ searchText, highlightResultIndex, }: LogLineProps) { - const chunks = calculateHighlightedChunks(line, searchText); + const chunks = useMemo( + () => calculateHighlightedChunks(line, searchText), + [line, searchText], + ); - const elements = chunks.map(({ text, modifiers, highlight }, index) => ( - - {text} - - )); + const elements = useMemo( + () => + chunks.map(({ text, modifiers, highlight }, index) => ( + + {text} + + )), + [chunks, highlightResultIndex, classes], + ); return <>{elements}; } From e708a0109f23fbc9373880a04dae5e6b05119f9b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 18:38:44 +0100 Subject: [PATCH 087/116] core-components: refactor out LogViewer search Signed-off-by: Patrik Oldsberg --- .../src/components/LogViewer/LogViewer.tsx | 77 +++----------- .../LogViewer/LogViewerControls.tsx | 25 ++--- .../LogViewer/useLogViewerSearch.tsx | 100 ++++++++++++++++++ 3 files changed, 121 insertions(+), 81 deletions(-) create mode 100644 packages/core-components/src/components/LogViewer/useLogViewerSearch.tsx diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index 24f10ca5f6..2ce1e5f04f 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -17,102 +17,49 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList } from 'react-window'; -import { AnsiLine, AnsiProcessor } from './AnsiProcessor'; +import { AnsiProcessor } from './AnsiProcessor'; import { HEADER_SIZE, useStyles } from './styles'; import clsx from 'clsx'; import { LogLine } from './LogLine'; import { LogViewerControls } from './LogViewerControls'; -import { useToggle } from 'react-use'; +import { useLogViewerSearch } from './useLogViewerSearch'; export interface LogViewerProps { text: string; } -function applySearchFilter(lines: AnsiLine[], searchText: string) { - if (!searchText) { - return { lines }; - } - - const matchingLines = []; - const searchResults = []; - for (const line of lines) { - if (line.text.includes(searchText)) { - matchingLines.push(line); - - let offset = 0; - let lineResultIndex = 0; - for (;;) { - const start = line.text.indexOf(searchText, offset); - if (start === -1) { - break; - } - searchResults.push({ - lineNumber: line.lineNumber, - lineIndex: lineResultIndex++, - }); - offset = start + searchText.length; - } - } - } - - return { - lines: matchingLines, - results: searchResults, - }; -} - export function LogViewer(props: LogViewerProps) { const classes = useStyles(); const listRef = useRef(null); const [selectedLine, setSelectedLine] = useState(); - const [resultIndex, setResultIndex] = useState(); - const [shouldFilter, toggleShouldFilter] = useToggle(false); - const [searchInput, setSearchInput] = useState(''); - const searchText = searchInput.toLocaleLowerCase('en-US'); // The processor keeps state that optimizes appending to the text const processor = useMemo(() => new AnsiProcessor(), []); const lines = processor.process(props.text); - const filter = useMemo( - () => applySearchFilter(lines, searchText), - [lines, searchText], - ); - - const searchResult = filter.results?.[resultIndex ?? 0]; - const searchResultLine = searchResult?.lineNumber; - - const displayLines = shouldFilter ? filter.lines : lines; + const search = useLogViewerSearch(lines); useEffect(() => { - if (searchResultLine !== undefined && listRef.current) { - listRef.current.scrollToItem(searchResultLine - 1, 'center'); + if (search.resultLine !== undefined && listRef.current) { + listRef.current.scrollToItem(search.resultLine - 1, 'center'); } - }, [searchResultLine]); + }, [search.resultLine]); return ( {({ height, width }) => (
- +
{({ index, style, data }) => { const line = data[index]; @@ -137,10 +84,10 @@ export function LogViewer(props: LogViewerProps) { diff --git a/packages/core-components/src/components/LogViewer/LogViewerControls.tsx b/packages/core-components/src/components/LogViewer/LogViewerControls.tsx index f7c06edd05..6d55e6f76f 100644 --- a/packages/core-components/src/components/LogViewer/LogViewerControls.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewerControls.tsx @@ -21,39 +21,32 @@ import Typography from '@material-ui/core/Typography'; import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; import ChevronRightIcon from '@material-ui/icons/ChevronRight'; import FilterListIcon from '@material-ui/icons/FilterList'; +import { LogViewerSearch } from './useLogViewerSearch'; -export interface LogViewerControlsProps { - search: string; - onSearchChange: (search: string) => void; - resultIndex: number | undefined; - resultCount: number | undefined; - onResultIndexChange: (index: number) => void; - shouldFilter: boolean; - onToggleShouldFilter: () => void; -} +export interface LogViewerControlsProps extends LogViewerSearch {} export function LogViewerControls(props: LogViewerControlsProps) { - const { resultCount, onResultIndexChange, onToggleShouldFilter } = props; + const { resultCount, setResultIndex, toggleShouldFilter } = props; const resultIndex = props.resultIndex ?? 0; const increment = () => { if (resultCount !== undefined) { const next = resultIndex + 1; - onResultIndexChange(next >= resultCount ? 0 : next); + setResultIndex(next >= resultCount ? 0 : next); } }; const decrement = () => { if (resultCount !== undefined) { const next = resultIndex - 1; - onResultIndexChange(next < 0 ? resultCount - 1 : next); + setResultIndex(next < 0 ? resultCount - 1 : next); } }; const handleKeyPress = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { if (event.metaKey || event.ctrlKey || event.altKey) { - onToggleShouldFilter(); + toggleShouldFilter(); } else if (event.shiftKey) { decrement(); } else { @@ -81,11 +74,11 @@ export function LogViewerControls(props: LogViewerControlsProps) { size="small" variant="standard" placeholder="Search" - value={props.search} + value={props.searchInput} onKeyPress={handleKeyPress} - onChange={e => props.onSearchChange(e.target.value)} + onChange={e => props.setSearchInput(e.target.value)} /> - + {props.shouldFilter ? ( ) : ( diff --git a/packages/core-components/src/components/LogViewer/useLogViewerSearch.tsx b/packages/core-components/src/components/LogViewer/useLogViewerSearch.tsx new file mode 100644 index 0000000000..4e93178c20 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/useLogViewerSearch.tsx @@ -0,0 +1,100 @@ +/* + * 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 { useMemo, useState } from 'react'; +import { useToggle } from 'react-use'; +import { AnsiLine } from './AnsiProcessor'; + +export function applySearchFilter(lines: AnsiLine[], searchText: string) { + if (!searchText) { + return { lines }; + } + + const matchingLines = []; + const searchResults = []; + for (const line of lines) { + if (line.text.includes(searchText)) { + matchingLines.push(line); + + let offset = 0; + let lineResultIndex = 0; + for (;;) { + const start = line.text.indexOf(searchText, offset); + if (start === -1) { + break; + } + searchResults.push({ + lineNumber: line.lineNumber, + lineIndex: lineResultIndex++, + }); + offset = start + searchText.length; + } + } + } + + return { + lines: matchingLines, + results: searchResults, + }; +} + +export interface LogViewerSearch { + lines: AnsiLine[]; + + searchText: string; + searchInput: string; + setSearchInput: (searchInput: string) => void; + + shouldFilter: boolean; + toggleShouldFilter: () => void; + + resultIndex: number | undefined; + resultCount: number | undefined; + setResultIndex: (number: number) => void; + + resultLine: number | undefined; + resultLineIndex: number | undefined; +} + +export function useLogViewerSearch(lines: AnsiLine[]): LogViewerSearch { + const [searchInput, setSearchInput] = useState(''); + const searchText = searchInput.toLocaleLowerCase('en-US'); + + const [resultIndex, setResultIndex] = useState(); + + const [shouldFilter, toggleShouldFilter] = useToggle(false); + + const filter = useMemo( + () => applySearchFilter(lines, searchText), + [lines, searchText], + ); + + const searchResult = filter.results?.[resultIndex ?? 0]; + + return { + lines: shouldFilter ? filter.lines : lines, + searchText, + searchInput, + setSearchInput, + shouldFilter, + toggleShouldFilter, + resultIndex, + resultCount: filter.results?.length, + setResultIndex, + resultLine: searchResult?.lineNumber, + resultLineIndex: searchResult?.lineIndex, + }; +} From cb3246ef89247d51c8d2b35c669da1b2061a7e8b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 18:58:24 +0100 Subject: [PATCH 088/116] core-components: lazy load log viewer Signed-off-by: Patrik Oldsberg --- .../components/LogViewer/LazyLogViewer.tsx | 32 +++++++++++++++++++ .../src/components/LogViewer/index.ts | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 packages/core-components/src/components/LogViewer/LazyLogViewer.tsx diff --git a/packages/core-components/src/components/LogViewer/LazyLogViewer.tsx b/packages/core-components/src/components/LogViewer/LazyLogViewer.tsx new file mode 100644 index 0000000000..fb765bbbc4 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/LazyLogViewer.tsx @@ -0,0 +1,32 @@ +/* + * 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, { lazy, Suspense } from 'react'; +import { useApp } from '@backstage/core-plugin-api'; +import { LogViewerProps } from './LogViewer'; + +const LogViewer = lazy(() => + import('./LogViewer').then(m => ({ default: m.LogViewer })), +); + +export function LazyLogViewer(props: LogViewerProps) { + const { Progress } = useApp().getComponents(); + return ( + }> + + + ); +} diff --git a/packages/core-components/src/components/LogViewer/index.ts b/packages/core-components/src/components/LogViewer/index.ts index 839f34f81e..aba2e8ea16 100644 --- a/packages/core-components/src/components/LogViewer/index.ts +++ b/packages/core-components/src/components/LogViewer/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { LogViewer } from './LogViewer'; +export { LazyLogViewer as LogViewer } from './LazyLogViewer'; export type { LogViewerProps } from './LogViewer'; From d06c6a4cfe58a399c2a69cfe39450b4b773f54a7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 19:45:10 +0100 Subject: [PATCH 089/116] core-components: proper LogViewer selection and copy Signed-off-by: Patrik Oldsberg --- .../src/components/LogViewer/LogViewer.tsx | 30 ++++++-- .../src/components/LogViewer/styles.ts | 6 ++ .../LogViewer/useLogViewerSelection.tsx | 70 +++++++++++++++++++ 3 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 packages/core-components/src/components/LogViewer/useLogViewerSelection.tsx diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index 2ce1e5f04f..b78764e86f 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -14,7 +14,9 @@ * limitations under the License. */ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; +import IconButton from '@material-ui/core/IconButton'; +import CopyIcon from '@material-ui/icons/FileCopy'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList } from 'react-window'; import { AnsiProcessor } from './AnsiProcessor'; @@ -23,6 +25,7 @@ import clsx from 'clsx'; import { LogLine } from './LogLine'; import { LogViewerControls } from './LogViewerControls'; import { useLogViewerSearch } from './useLogViewerSearch'; +import { useLogViewerSelection } from './useLogViewerSelection'; export interface LogViewerProps { text: string; @@ -31,13 +34,13 @@ export interface LogViewerProps { export function LogViewer(props: LogViewerProps) { const classes = useStyles(); const listRef = useRef(null); - const [selectedLine, setSelectedLine] = useState(); // The processor keeps state that optimizes appending to the text const processor = useMemo(() => new AnsiProcessor(), []); const lines = processor.process(props.text); const search = useLogViewerSearch(lines); + const selection = useLogViewerSelection(lines); useEffect(() => { if (search.resultLine !== undefined && listRef.current) { @@ -45,6 +48,14 @@ export function LogViewer(props: LogViewerProps) { } }, [search.resultLine]); + const handleSelectLine = ( + line: number, + event: { shiftKey: boolean; preventDefault: () => void }, + ) => { + event.preventDefault(); + selection.setSelection(line, event.shiftKey); + }; + return ( {({ height, width }) => ( @@ -68,16 +79,25 @@ export function LogViewer(props: LogViewerProps) {
+ {selection.shouldShowButton(lineNumber) && ( + selection.copySelection()} + > + + + )} setSelectedLine(lineNumber)} - onKeyPress={() => setSelectedLine(lineNumber)} + onClick={event => handleSelectLine(lineNumber, event)} + onKeyPress={event => handleSelectLine(lineNumber, event)} > {lineNumber} diff --git a/packages/core-components/src/components/LogViewer/styles.ts b/packages/core-components/src/components/LogViewer/styles.ts index 25db095645..d074e56632 100644 --- a/packages/core-components/src/components/LogViewer/styles.ts +++ b/packages/core-components/src/components/LogViewer/styles.ts @@ -34,6 +34,7 @@ export const useStyles = makeStyles(theme => ({ fontSize: theme.typography.fontSize, }, line: { + position: 'relative', whiteSpace: 'pre', '&:hover': { @@ -47,6 +48,11 @@ export const useStyles = makeStyles(theme => ({ background: theme.palette.action.selected, }, }, + lineCopyButton: { + position: 'absolute', + paddingTop: 0, + paddingBottom: 0, + }, lineNumber: { display: 'inline-block', textAlign: 'end', diff --git a/packages/core-components/src/components/LogViewer/useLogViewerSelection.tsx b/packages/core-components/src/components/LogViewer/useLogViewerSelection.tsx new file mode 100644 index 0000000000..cae56e5e43 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/useLogViewerSelection.tsx @@ -0,0 +1,70 @@ +/* + * 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 { errorApiRef, useApi } from '@backstage/core-plugin-api'; +import { useEffect, useState } from 'react'; +import { useCopyToClipboard } from 'react-use'; +import { AnsiLine } from './AnsiProcessor'; + +export function useLogViewerSelection(lines: AnsiLine[]) { + const errorApi = useApi(errorApiRef); + const [sel, setSelection] = useState<{ start: number; end: number }>(); + const start = sel ? Math.min(sel.start, sel.end) : undefined; + const end = sel ? Math.max(sel.start, sel.end) : undefined; + + const [{ error }, copyToClipboard] = useCopyToClipboard(); + + useEffect(() => { + if (error) { + errorApi.post(error); + } + }, [error, errorApi]); + + return { + shouldShowButton(line: number) { + return start === line || end === line; + }, + isSelected(line: number) { + if (!sel) { + return false; + } + return start! <= line && line <= end!; + }, + setSelection(line: number, add: boolean) { + if (add) { + setSelection(s => + s ? { start: s.start, end: line } : { start: line, end: line }, + ); + } else { + setSelection(s => + s?.start === line && s?.end === line + ? undefined + : { start: line, end: line }, + ); + } + }, + copySelection() { + if (sel) { + const copyText = lines + .slice(sel.start - 1, sel.end) + .map(l => l.text) + .join('\n'); + copyToClipboard(copyText); + setSelection(undefined); + } + }, + }; +} From b291c3176ef66126d94135cef3f0813d1ebd4489 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 19:51:51 +0100 Subject: [PATCH 090/116] scaffolder: switch to using LogViewer Signed-off-by: Patrik Oldsberg --- .changeset/soft-shoes-check.md | 5 ++++ plugins/scaffolder/package.json | 1 - .../src/components/TaskPage/TaskPage.tsx | 26 ++++--------------- 3 files changed, 10 insertions(+), 22 deletions(-) create mode 100644 .changeset/soft-shoes-check.md diff --git a/.changeset/soft-shoes-check.md b/.changeset/soft-shoes-check.md new file mode 100644 index 0000000000..5600344cc1 --- /dev/null +++ b/.changeset/soft-shoes-check.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Switch to using `LogViewer` component from `@backstage/core-components` to display scaffolder logs. diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index ef2fa32974..8a6123b7a9 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -55,7 +55,6 @@ "lodash": "^4.17.21", "luxon": "^2.0.2", "qs": "^6.9.4", - "react-lazylog": "^4.5.2", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4", diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index 11d12282d2..58ec3dfc02 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -20,7 +20,7 @@ import { Header, Lifecycle, Page, - Progress, + LogViewer, } from '@backstage/core-components'; import { BackstageTheme } from '@backstage/theme'; import { @@ -40,15 +40,13 @@ import Check from '@material-ui/icons/Check'; import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'; import classNames from 'classnames'; import { DateTime, Interval } from 'luxon'; -import React, { memo, Suspense, useEffect, useMemo, useState } from 'react'; +import React, { memo, useEffect, useMemo, useState } from 'react'; import { useParams } from 'react-router'; import { useInterval } from 'react-use'; import { Status, TaskOutput } from '../../types'; import { useTaskEventStream } from '../hooks/useEventStream'; import { TaskPageLinks } from './TaskPageLinks'; -const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog')); - // typings are wrong for this library, so fallback to not parsing types. const humanizeDuration = require('humanize-duration'); @@ -213,22 +211,6 @@ export const TaskStatusStepper = memo( }, ); -const TaskLogger = memo(({ log }: { log: string }) => { - return ( - }> -
- -
-
- ); -}); - const hasLinks = ({ entityRef, remoteUrl, links = [] }: TaskOutput): boolean => !!(entityRef || remoteUrl || links.length > 0); @@ -318,7 +300,9 @@ export const TaskPage = () => { - +
+ +
From 37d80d0a9d717b98b81cfc8fe3fe0a3c1b621c47 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 20:40:44 +0100 Subject: [PATCH 091/116] core-components: add className prop for LogViewer Signed-off-by: Patrik Oldsberg --- .../core-components/src/components/LogViewer/LogViewer.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index b78764e86f..36d25153cd 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -29,6 +29,7 @@ import { useLogViewerSelection } from './useLogViewerSelection'; export interface LogViewerProps { text: string; + className?: string; } export function LogViewer(props: LogViewerProps) { @@ -59,7 +60,10 @@ export function LogViewer(props: LogViewerProps) { return ( {({ height, width }) => ( -
+
From d90dad84b0cae18ce88c6706c04915503202e0eb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 20:44:59 +0100 Subject: [PATCH 092/116] techdocs: switch to using LogViewer Signed-off-by: Patrik Oldsberg --- .changeset/kind-ways-nail.md | 5 +++ plugins/techdocs/package.json | 1 - .../components/TechDocsBuildLogs.test.tsx | 36 +++++++++---------- .../reader/components/TechDocsBuildLogs.tsx | 24 +++++-------- 4 files changed, 30 insertions(+), 36 deletions(-) create mode 100644 .changeset/kind-ways-nail.md diff --git a/.changeset/kind-ways-nail.md b/.changeset/kind-ways-nail.md new file mode 100644 index 0000000000..14821e4f44 --- /dev/null +++ b/.changeset/kind-ways-nail.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Switch to using `LogViewer` component from `@backstage/core-components` to display build logs. diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index b048761e1c..50cabace51 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -51,7 +51,6 @@ "event-source-polyfill": "^1.0.25", "git-url-parse": "^11.6.0", "lodash": "^4.17.21", - "react-lazylog": "^4.5.2", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-text-truncate": "^0.16.0", diff --git a/plugins/techdocs/src/reader/components/TechDocsBuildLogs.test.tsx b/plugins/techdocs/src/reader/components/TechDocsBuildLogs.test.tsx index fa82527282..ea24a3627f 100644 --- a/plugins/techdocs/src/reader/components/TechDocsBuildLogs.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsBuildLogs.test.tsx @@ -14,32 +14,30 @@ * limitations under the License. */ -import { render } from '@testing-library/react'; -import React from 'react'; +import React, { ReactNode } from 'react'; +import { renderInTestApp } from '@backstage/test-utils'; import { TechDocsBuildLogs, TechDocsBuildLogsDrawerContent, } from './TechDocsBuildLogs'; -// react-lazylog is based on a react-virtualized component which doesn't -// write the content to the dom, so we mock it. -jest.mock('react-lazylog/build/LazyLog', () => { - return { - default: ({ text }: { text: string }) => { - return

{text}

; - }, - }; -}); +// The inside needs mocking to render in jsdom +jest.mock('react-virtualized-auto-sizer', () => ({ + __esModule: true, + default: (props: { + children: (size: { width: number; height: number }) => ReactNode; + }) => <>{props.children({ width: 400, height: 200 })}, +})); describe('', () => { - it('should render with button', () => { - const rendered = render(); + it('should render with button', async () => { + const rendered = await renderInTestApp(); expect(rendered.getByText(/Show Build Logs/i)).toBeInTheDocument(); expect(rendered.queryByText(/Build Details/i)).not.toBeInTheDocument(); }); - it('should open drawer', () => { - const rendered = render(); + it('should open drawer', async () => { + const rendered = await renderInTestApp(); rendered.getByText(/Show Build Logs/i).click(); expect(rendered.getByText(/Build Details/i)).toBeInTheDocument(); }); @@ -48,7 +46,7 @@ describe('', () => { describe('', () => { it('should render with empty log', async () => { const onClose = jest.fn(); - const rendered = render( + const rendered = await renderInTestApp( , ); expect(rendered.getByText(/Build Details/i)).toBeInTheDocument(); @@ -61,7 +59,7 @@ describe('', () => { it('should render logs', async () => { const onClose = jest.fn(); - const rendered = render( + const rendered = await renderInTestApp( ', () => { expect(onClose).toBeCalledTimes(0); }); - it('should call onClose', () => { + it('should call onClose', async () => { const onClose = jest.fn(); - const rendered = render( + const rendered = await renderInTestApp( , ); rendered.getByTitle('Close the drawer').click(); diff --git a/plugins/techdocs/src/reader/components/TechDocsBuildLogs.tsx b/plugins/techdocs/src/reader/components/TechDocsBuildLogs.tsx index 8c739a9bcf..e49d486ab8 100644 --- a/plugins/techdocs/src/reader/components/TechDocsBuildLogs.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsBuildLogs.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Progress } from '@backstage/core-components'; +import { LogViewer } from '@backstage/core-components'; import { Button, createStyles, @@ -26,9 +26,7 @@ import { Typography, } from '@material-ui/core'; import Close from '@material-ui/icons/Close'; -import React, { Suspense, useState } from 'react'; - -const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog')); +import React, { useState } from 'react'; const useDrawerStyles = makeStyles((theme: Theme) => createStyles({ @@ -46,6 +44,9 @@ const useDrawerStyles = makeStyles((theme: Theme) => height: '100%', overflow: 'hidden', }, + logs: { + background: theme.palette.background.default, + }, }), ); @@ -57,6 +58,8 @@ export const TechDocsBuildLogsDrawerContent = ({ onClose: () => void; }) => { const classes = useDrawerStyles(); + const logText = + buildLog.length === 0 ? 'Waiting for logs...' : buildLog.join('\n'); return ( - - }> - - + ); }; From ea8d73a7ed9b716cbd3545fd40a40f1dc7c0d529 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 20:56:44 +0100 Subject: [PATCH 093/116] core-components: reduce font size in LogViewer Signed-off-by: Patrik Oldsberg --- packages/core-components/src/components/LogViewer/styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-components/src/components/LogViewer/styles.ts b/packages/core-components/src/components/LogViewer/styles.ts index d074e56632..4f4312bdd9 100644 --- a/packages/core-components/src/components/LogViewer/styles.ts +++ b/packages/core-components/src/components/LogViewer/styles.ts @@ -31,7 +31,7 @@ export const useStyles = makeStyles(theme => ({ }, log: { fontFamily: '"Monaco", monospace', - fontSize: theme.typography.fontSize, + fontSize: theme.typography.pxToRem(12), }, line: { position: 'relative', From cbd20c46f14f16ec73724454b1a524e220d9c4f9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 20:57:31 +0100 Subject: [PATCH 094/116] github-actions: switch to using LogViewer Signed-off-by: Patrik Oldsberg --- .changeset/red-chairs-wave.md | 5 ++ plugins/github-actions/package.json | 1 - .../WorkflowRunLogs/WorkflowRunLogs.tsx | 69 +++++-------------- 3 files changed, 24 insertions(+), 51 deletions(-) create mode 100644 .changeset/red-chairs-wave.md diff --git a/.changeset/red-chairs-wave.md b/.changeset/red-chairs-wave.md new file mode 100644 index 0000000000..a052afe4b5 --- /dev/null +++ b/.changeset/red-chairs-wave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-github-actions': patch +--- + +Switch to using `LogViewer` component from `@backstage/core-components` to display build logs. diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 0ea8bbe9f2..ace9527598 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -44,7 +44,6 @@ "@material-ui/lab": "4.0.0-alpha.57", "@octokit/rest": "^18.5.3", "luxon": "^2.0.2", - "react-lazylog": "^4.5.3", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" diff --git a/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx b/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx index 399fed9557..509e404194 100644 --- a/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx +++ b/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -import { Progress } from '@backstage/core-components'; +import { LogViewer } from '@backstage/core-components'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; import { readGitHubIntegrationConfigs } from '@backstage/integration'; import { @@ -32,14 +32,11 @@ import { } from '@material-ui/core'; import DescriptionIcon from '@material-ui/icons/Description'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import React, { Suspense } from 'react'; +import React from 'react'; import { useProjectName } from '../useProjectName'; import { useDownloadWorkflowRunLogs } from './useDownloadWorkflowRunLogs'; -const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog')); -const LinePart = React.lazy(() => import('react-lazylog/build/LinePart')); - -const useStyles = makeStyles(() => ({ +const useStyles = makeStyles(theme => ({ button: { order: -1, marginRight: 0, @@ -53,49 +50,19 @@ const useStyles = makeStyles(() => ({ justifyContent: 'center', margin: 'auto', }, - normalLog: { + normalLogContainer: { height: '75vh', width: '100%', }, - modalLog: { + modalLogContainer: { height: '100%', width: '100%', }, + log: { + background: theme.palette.background.default, + }, })); -const DisplayLog = ({ - jobLogs, - className, -}: { - jobLogs: any; - className: string; -}) => { - return ( - }> -
- { - if ( - line.toLocaleLowerCase().includes('error') || - line.toLocaleLowerCase().includes('failed') || - line.toLocaleLowerCase().includes('failure') - ) { - return ( - - ); - } - return line; - }} - /> -
-
- ); -}; - /** * A component for Run Logs visualization. */ @@ -123,6 +90,7 @@ export const WorkflowRunLogs = ({ repo, id: runId, }); + const logText = jobLogs.value ? String(jobLogs.value) : undefined; const [open, setOpen] = React.useState(false); const handleOpen = () => { @@ -162,18 +130,19 @@ export const WorkflowRunLogs = ({ onClose={handleClose} > - +
+ +
- {jobLogs.value && ( - + {logText && ( +
+ +
)} ); From 56d04330c499ae0f237885c32c999c1e046cc773 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 21:01:00 +0100 Subject: [PATCH 095/116] circleci: switch to use LogViewer Signed-off-by: Patrik Oldsberg --- .changeset/rich-carrots-relax.md | 5 +++++ plugins/circleci/package.json | 2 -- .../lib/ActionOutput/ActionOutput.tsx | 13 +++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 .changeset/rich-carrots-relax.md diff --git a/.changeset/rich-carrots-relax.md b/.changeset/rich-carrots-relax.md new file mode 100644 index 0000000000..9f7c822883 --- /dev/null +++ b/.changeset/rich-carrots-relax.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-circleci': patch +--- + +Switch to using `LogViewer` component from `@backstage/core-components` to display action output. diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 9d94d61dcd..dfcf7b61e1 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -44,7 +44,6 @@ "humanize-duration": "^3.27.0", "lodash": "^4.17.21", "luxon": "^2.0.2", - "react-lazylog": "^4.5.2", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" @@ -63,7 +62,6 @@ "@types/humanize-duration": "^3.25.1", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "@types/react-lazylog": "^4.5.0", "cross-fetch": "^3.0.6", "msw": "^0.35.0" }, diff --git a/plugins/circleci/src/components/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx b/plugins/circleci/src/components/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx index a4c2af241f..4a4a8a45f2 100644 --- a/plugins/circleci/src/components/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx +++ b/plugins/circleci/src/components/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Progress } from '@backstage/core-components'; +import { LogViewer } from '@backstage/core-components'; import { Accordion, AccordionDetails, @@ -24,10 +24,9 @@ import { import { makeStyles } from '@material-ui/core/styles'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { BuildStepAction } from 'circleci-api'; -import React, { Suspense, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { durationHumanized } from '../../../../util'; -const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog')); const useStyles = makeStyles({ accordionDetails: { padding: 0, @@ -85,11 +84,9 @@ export const ActionOutput = ({ {messages.length === 0 ? ( 'Nothing here...' ) : ( - }> -
- -
-
+
+ +
)} From c77def982f3095a209fa2c9e3b2cabd2b348090f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 21:05:59 +0100 Subject: [PATCH 096/116] cloudbuild: remove unnecessary lazylog dependency Signed-off-by: Patrik Oldsberg --- .changeset/cool-starfishes-press.md | 5 +++++ plugins/cloudbuild/package.json | 1 - yarn.lock | 12 ++---------- 3 files changed, 7 insertions(+), 11 deletions(-) create mode 100644 .changeset/cool-starfishes-press.md diff --git a/.changeset/cool-starfishes-press.md b/.changeset/cool-starfishes-press.md new file mode 100644 index 0000000000..6cba5c8204 --- /dev/null +++ b/.changeset/cool-starfishes-press.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cloudbuild': patch +--- + +Remove unnecessary dependency. diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 6fab2b8d35..da206280f4 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -41,7 +41,6 @@ "@material-ui/lab": "4.0.0-alpha.57", "luxon": "^2.0.2", "qs": "^6.9.4", - "react-lazylog": "^4.5.3", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" diff --git a/yarn.lock b/yarn.lock index a71567ffe8..015d5d6e79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8036,14 +8036,6 @@ dependencies: "@types/react" "*" -"@types/react-lazylog@^4.5.0": - version "4.5.1" - resolved "https://registry.npmjs.org/@types/react-lazylog/-/react-lazylog-4.5.1.tgz#babb5d814f7035b5434518769975e12f299356a8" - integrity sha512-g4yeosa1zYhu2BUJmuu2H2o0dsdRj0o8Omw3pBiVHdLHJaeYIyArvyMRR3bI/MxZxG4EaiRl8AOQ6zeM8P46jA== - dependencies: - "@types/react" "*" - immutable ">=3.8.2" - "@types/react-redux@^7.1.16": version "7.1.19" resolved "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.19.tgz#477bd0a9b01bae6d6bf809418cdfa7d3c16d4c62" @@ -17047,7 +17039,7 @@ immer@^9.0.1, immer@^9.0.6: resolved "https://registry.npmjs.org/immer/-/immer-9.0.7.tgz#b6156bd7db55db7abc73fd2fdadf4e579a701075" integrity sha512-KGllzpbamZDvOIxnmJ0jI840g7Oikx58lBPWV0hUh7dtAyZpFqqrBZdKka5GlTwMTZ1Tjc/bKKW4VSFAt6BqMA== -immutable@>=3.8.2, immutable@^3.8.2, immutable@^3.x.x: +immutable@^3.8.2, immutable@^3.x.x: version "3.8.2" resolved "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM= @@ -24561,7 +24553,7 @@ react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-i resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-lazylog@^4.5.2, react-lazylog@^4.5.3: +react-lazylog@^4.5.2: version "4.5.3" resolved "https://registry.npmjs.org/react-lazylog/-/react-lazylog-4.5.3.tgz#289e24995b5599e75943556ac63f5e2c04d0001e" integrity sha512-lyov32A/4BqihgXgtNXTHCajXSXkYHPlIEmV8RbYjHIMxCFSnmtdg4kDCI3vATz7dURtiFTvrw5yonHnrS+NNg== From e83950028621608417542389b63003f97424a551 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 21:29:55 +0100 Subject: [PATCH 097/116] changesets: added changeset for LogViewer Signed-off-by: Patrik Oldsberg --- .changeset/many-items-own.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/many-items-own.md diff --git a/.changeset/many-items-own.md b/.changeset/many-items-own.md new file mode 100644 index 0000000000..64c8abafe2 --- /dev/null +++ b/.changeset/many-items-own.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Introduce new `LogViewer` component that can be used to display logs. It supports copying, searching, filtering, and displaying text with ANSI color escape codes. From a6622d97046e171ec0eaa1803e22b1fdb1ed66b6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 21:47:01 +0100 Subject: [PATCH 098/116] core-components: document LogViewer + restructure Signed-off-by: Patrik Oldsberg --- .../components/LogViewer/LazyLogViewer.tsx | 32 ----- .../LogViewer/LogViewer.stories.tsx | 4 +- .../src/components/LogViewer/LogViewer.tsx | 132 +++++------------- .../components/LogViewer/RealLogViewer.tsx | 126 +++++++++++++++++ .../src/components/LogViewer/index.ts | 2 +- 5 files changed, 162 insertions(+), 134 deletions(-) delete mode 100644 packages/core-components/src/components/LogViewer/LazyLogViewer.tsx create mode 100644 packages/core-components/src/components/LogViewer/RealLogViewer.tsx diff --git a/packages/core-components/src/components/LogViewer/LazyLogViewer.tsx b/packages/core-components/src/components/LogViewer/LazyLogViewer.tsx deleted file mode 100644 index fb765bbbc4..0000000000 --- a/packages/core-components/src/components/LogViewer/LazyLogViewer.tsx +++ /dev/null @@ -1,32 +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, { lazy, Suspense } from 'react'; -import { useApp } from '@backstage/core-plugin-api'; -import { LogViewerProps } from './LogViewer'; - -const LogViewer = lazy(() => - import('./LogViewer').then(m => ({ default: m.LogViewer })), -); - -export function LazyLogViewer(props: LogViewerProps) { - const { Progress } = useApp().getComponents(); - return ( - }> - - - ); -} diff --git a/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx b/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx index b7a56e1a7b..255e1d03b8 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.stories.tsx @@ -14,12 +14,14 @@ * limitations under the License. */ -import React from 'react'; +import React, { ComponentType } from 'react'; +import { wrapInTestApp } from '@backstage/test-utils'; import { LogViewer } from './LogViewer'; export default { title: 'Data Display/LogViewer', component: LogViewer, + decorators: [(Story: ComponentType<{}>) => wrapInTestApp()], }; const exampleLog = `Starting up task with 3 steps diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index 36d25153cd..a85603b465 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -14,113 +14,45 @@ * limitations under the License. */ -import React, { useEffect, useMemo, useRef } from 'react'; -import IconButton from '@material-ui/core/IconButton'; -import CopyIcon from '@material-ui/icons/FileCopy'; -import AutoSizer from 'react-virtualized-auto-sizer'; -import { FixedSizeList } from 'react-window'; -import { AnsiProcessor } from './AnsiProcessor'; -import { HEADER_SIZE, useStyles } from './styles'; -import clsx from 'clsx'; -import { LogLine } from './LogLine'; -import { LogViewerControls } from './LogViewerControls'; -import { useLogViewerSearch } from './useLogViewerSearch'; -import { useLogViewerSelection } from './useLogViewerSelection'; +import React, { lazy, Suspense } from 'react'; +import { useApp } from '@backstage/core-plugin-api'; +const RealLogViewer = lazy(() => + import('./RealLogViewer').then(m => ({ default: m.RealLogViewer })), +); + +/** + * The properties for the LogViewer component. + */ export interface LogViewerProps { + /** + * The text of the logs to display. + * + * The LogViewer component is optimized for appending content at the end of the text. + */ text: string; + /** + * The className to apply to the root LogViewer element inside the auto sizer. + */ className?: string; } +/** + * A component that displays logs in a scrollable text area. + * + * The LogViewer has support for search and filtering, as well as displaying + * text content with ANSI color escape codes. + * + * Since the LogViewer uses windowing to avoid rendering all contents at once, the + * log is sized automatically to fill the available vertical space. This means + * it may often be needed to wrap the LogViewer in a container that provides it + * with a fixed amount of space. + */ export function LogViewer(props: LogViewerProps) { - const classes = useStyles(); - const listRef = useRef(null); - - // The processor keeps state that optimizes appending to the text - const processor = useMemo(() => new AnsiProcessor(), []); - const lines = processor.process(props.text); - - const search = useLogViewerSearch(lines); - const selection = useLogViewerSelection(lines); - - useEffect(() => { - if (search.resultLine !== undefined && listRef.current) { - listRef.current.scrollToItem(search.resultLine - 1, 'center'); - } - }, [search.resultLine]); - - const handleSelectLine = ( - line: number, - event: { shiftKey: boolean; preventDefault: () => void }, - ) => { - event.preventDefault(); - selection.setSelection(line, event.shiftKey); - }; - + const { Progress } = useApp().getComponents(); return ( - - {({ height, width }) => ( -
-
- -
- - {({ index, style, data }) => { - const line = data[index]; - const { lineNumber } = line; - return ( -
- {selection.shouldShowButton(lineNumber) && ( - selection.copySelection()} - > - - - )} - handleSelectLine(lineNumber, event)} - onKeyPress={event => handleSelectLine(lineNumber, event)} - > - {lineNumber} - - -
- ); - }} -
-
- )} -
+ }> + + ); } diff --git a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx new file mode 100644 index 0000000000..d41ce80672 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx @@ -0,0 +1,126 @@ +/* + * 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, { useEffect, useMemo, useRef } from 'react'; +import IconButton from '@material-ui/core/IconButton'; +import CopyIcon from '@material-ui/icons/FileCopy'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { FixedSizeList } from 'react-window'; +import { AnsiProcessor } from './AnsiProcessor'; +import { HEADER_SIZE, useStyles } from './styles'; +import clsx from 'clsx'; +import { LogLine } from './LogLine'; +import { LogViewerControls } from './LogViewerControls'; +import { useLogViewerSearch } from './useLogViewerSearch'; +import { useLogViewerSelection } from './useLogViewerSelection'; + +export interface RealLogViewerProps { + text: string; + className?: string; +} + +export function RealLogViewer(props: RealLogViewerProps) { + const classes = useStyles(); + const listRef = useRef(null); + + // The processor keeps state that optimizes appending to the text + const processor = useMemo(() => new AnsiProcessor(), []); + const lines = processor.process(props.text); + + const search = useLogViewerSearch(lines); + const selection = useLogViewerSelection(lines); + + useEffect(() => { + if (search.resultLine !== undefined && listRef.current) { + listRef.current.scrollToItem(search.resultLine - 1, 'center'); + } + }, [search.resultLine]); + + const handleSelectLine = ( + line: number, + event: { shiftKey: boolean; preventDefault: () => void }, + ) => { + event.preventDefault(); + selection.setSelection(line, event.shiftKey); + }; + + return ( + + {({ height, width }) => ( +
+
+ +
+ + {({ index, style, data }) => { + const line = data[index]; + const { lineNumber } = line; + return ( +
+ {selection.shouldShowButton(lineNumber) && ( + selection.copySelection()} + > + + + )} + handleSelectLine(lineNumber, event)} + onKeyPress={event => handleSelectLine(lineNumber, event)} + > + {lineNumber} + + +
+ ); + }} +
+
+ )} +
+ ); +} diff --git a/packages/core-components/src/components/LogViewer/index.ts b/packages/core-components/src/components/LogViewer/index.ts index aba2e8ea16..839f34f81e 100644 --- a/packages/core-components/src/components/LogViewer/index.ts +++ b/packages/core-components/src/components/LogViewer/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { LazyLogViewer as LogViewer } from './LazyLogViewer'; +export { LogViewer } from './LogViewer'; export type { LogViewerProps } from './LogViewer'; From beed531a9d5f5619730d02a852cb3eb73ce6085b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 23:14:50 +0100 Subject: [PATCH 099/116] core-components: tests for LogViewer search state + refactor Signed-off-by: Patrik Oldsberg --- .../LogViewer/LogViewerControls.tsx | 24 +- .../LogViewer/useLogViewerSearch.test.tsx | 221 ++++++++++++++++++ .../LogViewer/useLogViewerSearch.tsx | 29 ++- 3 files changed, 248 insertions(+), 26 deletions(-) create mode 100644 packages/core-components/src/components/LogViewer/useLogViewerSearch.test.tsx diff --git a/packages/core-components/src/components/LogViewer/LogViewerControls.tsx b/packages/core-components/src/components/LogViewer/LogViewerControls.tsx index 6d55e6f76f..4d3baae68d 100644 --- a/packages/core-components/src/components/LogViewer/LogViewerControls.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewerControls.tsx @@ -26,31 +26,15 @@ import { LogViewerSearch } from './useLogViewerSearch'; export interface LogViewerControlsProps extends LogViewerSearch {} export function LogViewerControls(props: LogViewerControlsProps) { - const { resultCount, setResultIndex, toggleShouldFilter } = props; + const { resultCount, resultIndexStep, toggleShouldFilter } = props; const resultIndex = props.resultIndex ?? 0; - const increment = () => { - if (resultCount !== undefined) { - const next = resultIndex + 1; - setResultIndex(next >= resultCount ? 0 : next); - } - }; - - const decrement = () => { - if (resultCount !== undefined) { - const next = resultIndex - 1; - setResultIndex(next < 0 ? resultCount - 1 : next); - } - }; - const handleKeyPress = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { if (event.metaKey || event.ctrlKey || event.altKey) { toggleShouldFilter(); - } else if (event.shiftKey) { - decrement(); } else { - increment(); + resultIndexStep(event.shiftKey); } } }; @@ -59,13 +43,13 @@ export function LogViewerControls(props: LogViewerControlsProps) { <> {resultCount !== undefined && ( <> - + resultIndexStep(true)}> {Math.min(resultIndex + 1, resultCount)}/{resultCount} - + resultIndexStep()}> diff --git a/packages/core-components/src/components/LogViewer/useLogViewerSearch.test.tsx b/packages/core-components/src/components/LogViewer/useLogViewerSearch.test.tsx new file mode 100644 index 0000000000..a64f22765b --- /dev/null +++ b/packages/core-components/src/components/LogViewer/useLogViewerSearch.test.tsx @@ -0,0 +1,221 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { applySearchFilter, useLogViewerSearch } from './useLogViewerSearch'; +import { AnsiLine } from './AnsiProcessor'; + +const lines = [ + new AnsiLine(1, [{ text: 'FooBar', modifiers: {} }]), + new AnsiLine(2, [{ text: 'Baz', modifiers: {} }]), + new AnsiLine(3, [{ text: 'FooBarFoo', modifiers: {} }]), + new AnsiLine(4, [{ text: 'Baz', modifiers: {} }]), + new AnsiLine(5, [{ text: 'BazFoo', modifiers: {} }]), + new AnsiLine(6, [{ text: 'FooFooFoo', modifiers: {} }]), + new AnsiLine(7, [{ text: '', modifiers: {} }]), + new AnsiLine(8, [{ text: 'Bar', modifiers: {} }]), +]; + +describe('applySearchFilter', () => { + it('should find search results', () => { + expect(applySearchFilter(lines, '')).toEqual({ + lines: lines, + results: undefined, + }); + expect(applySearchFilter(lines, 'foo')).toEqual({ + lines: [lines[0], lines[2], lines[4], lines[5]], + results: [ + { lineNumber: 1, lineIndex: 0 }, + { lineNumber: 3, lineIndex: 0 }, + { lineNumber: 3, lineIndex: 1 }, + { lineNumber: 5, lineIndex: 0 }, + { lineNumber: 6, lineIndex: 0 }, + { lineNumber: 6, lineIndex: 1 }, + { lineNumber: 6, lineIndex: 2 }, + ], + }); + expect(applySearchFilter(lines, 'bar')).toEqual({ + lines: [lines[0], lines[2], lines[7]], + results: [ + { lineNumber: 1, lineIndex: 0 }, + { lineNumber: 3, lineIndex: 0 }, + { lineNumber: 8, lineIndex: 0 }, + ], + }); + expect(applySearchFilter(lines, 'baz')).toEqual({ + lines: [lines[1], lines[3], lines[4]], + results: [ + { lineNumber: 2, lineIndex: 0 }, + { lineNumber: 4, lineIndex: 0 }, + { lineNumber: 5, lineIndex: 0 }, + ], + }); + }); +}); + +describe('useLogViewerSearch', () => { + it('should provide search state', () => { + const rendered = renderHook(() => useLogViewerSearch(lines)); + expect(rendered.result.current).toMatchObject({ + lines, + searchText: '', + shouldFilter: false, + resultCount: undefined, + resultIndex: 0, + resultLine: undefined, + resultLineIndex: undefined, + }); + + rendered.result.current.resultIndexStep(); + expect(rendered.result.current).toMatchObject({ + lines, + searchText: '', + shouldFilter: false, + resultCount: undefined, + resultIndex: 0, + resultLine: undefined, + resultLineIndex: undefined, + }); + + act(() => rendered.result.current.toggleShouldFilter()); + expect(rendered.result.current).toMatchObject({ + lines, + searchText: '', + shouldFilter: true, + resultCount: undefined, + resultIndex: 0, + resultLine: undefined, + resultLineIndex: undefined, + }); + + act(() => rendered.result.current.setSearchInput('BAR')); + expect(rendered.result.current).toMatchObject({ + lines: [lines[0], lines[2], lines[7]], + searchInput: 'BAR', + searchText: 'bar', + shouldFilter: true, + resultCount: 3, + resultIndex: 0, + resultLine: 1, + resultLineIndex: 0, + }); + + act(() => rendered.result.current.resultIndexStep()); + expect(rendered.result.current).toMatchObject({ + lines: [lines[0], lines[2], lines[7]], + resultIndex: 1, + resultLine: 3, + resultLineIndex: 0, + }); + + act(() => rendered.result.current.resultIndexStep()); + expect(rendered.result.current).toMatchObject({ + lines: [lines[0], lines[2], lines[7]], + resultIndex: 2, + resultLine: 8, + resultLineIndex: 0, + }); + + act(() => rendered.result.current.resultIndexStep()); + expect(rendered.result.current).toMatchObject({ + lines: [lines[0], lines[2], lines[7]], + resultIndex: 0, + resultLine: 1, + resultLineIndex: 0, + }); + + act(() => rendered.result.current.resultIndexStep(true)); + expect(rendered.result.current).toMatchObject({ + lines: [lines[0], lines[2], lines[7]], + resultIndex: 2, + resultLine: 8, + resultLineIndex: 0, + }); + + act(() => rendered.result.current.setSearchInput('FOO')); + expect(rendered.result.current).toMatchObject({ + lines: [lines[0], lines[2], lines[4], lines[5]], + searchInput: 'FOO', + searchText: 'foo', + shouldFilter: true, + resultCount: 7, + resultIndex: 2, + resultLine: 3, + resultLineIndex: 1, + }); + + act(() => rendered.result.current.toggleShouldFilter()); + expect(rendered.result.current).toMatchObject({ + lines, + shouldFilter: false, + resultCount: 7, + resultIndex: 2, + resultLine: 3, + resultLineIndex: 1, + }); + + act(() => rendered.result.current.resultIndexStep()); + expect(rendered.result.current).toMatchObject({ + lines, + searchInput: 'FOO', + searchText: 'foo', + shouldFilter: false, + resultCount: 7, + resultIndex: 3, + resultLine: 5, + resultLineIndex: 0, + }); + + act(() => rendered.result.current.resultIndexStep()); + expect(rendered.result.current).toMatchObject({ + resultIndex: 4, + resultLine: 6, + resultLineIndex: 0, + }); + + act(() => rendered.result.current.resultIndexStep()); + expect(rendered.result.current).toMatchObject({ + resultIndex: 5, + resultLine: 6, + resultLineIndex: 1, + }); + + act(() => rendered.result.current.resultIndexStep()); + expect(rendered.result.current).toMatchObject({ + resultIndex: 6, + resultLine: 6, + resultLineIndex: 2, + }); + + act(() => rendered.result.current.setSearchInput('BAR')); + expect(rendered.result.current).toMatchObject({ + searchText: 'bar', + resultCount: 3, + resultIndex: 6, + resultLine: 8, + resultLineIndex: 0, + }); + + act(() => rendered.result.current.resultIndexStep(true)); + expect(rendered.result.current).toMatchObject({ + searchText: 'bar', + resultCount: 3, + resultIndex: 1, + resultLine: 3, + resultLineIndex: 0, + }); + }); +}); diff --git a/packages/core-components/src/components/LogViewer/useLogViewerSearch.tsx b/packages/core-components/src/components/LogViewer/useLogViewerSearch.tsx index 4e93178c20..4462a3ff50 100644 --- a/packages/core-components/src/components/LogViewer/useLogViewerSearch.tsx +++ b/packages/core-components/src/components/LogViewer/useLogViewerSearch.tsx @@ -61,9 +61,9 @@ export interface LogViewerSearch { shouldFilter: boolean; toggleShouldFilter: () => void; - resultIndex: number | undefined; resultCount: number | undefined; - setResultIndex: (number: number) => void; + resultIndex: number | undefined; + resultIndexStep: (decrement?: boolean) => void; resultLine: number | undefined; resultLineIndex: number | undefined; @@ -73,7 +73,7 @@ export function useLogViewerSearch(lines: AnsiLine[]): LogViewerSearch { const [searchInput, setSearchInput] = useState(''); const searchText = searchInput.toLocaleLowerCase('en-US'); - const [resultIndex, setResultIndex] = useState(); + const [resultIndex, setResultIndex] = useState(0); const [shouldFilter, toggleShouldFilter] = useToggle(false); @@ -82,7 +82,24 @@ export function useLogViewerSearch(lines: AnsiLine[]): LogViewerSearch { [lines, searchText], ); - const searchResult = filter.results?.[resultIndex ?? 0]; + const searchResult = filter.results + ? filter.results[Math.min(resultIndex, filter.results.length - 1)] + : undefined; + const resultCount = filter.results?.length; + + const resultIndexStep = (decrement?: boolean) => { + if (decrement) { + if (resultCount !== undefined) { + const next = Math.min(resultIndex - 1, resultCount - 2); + setResultIndex(next < 0 ? resultCount - 1 : next); + } + } else { + if (resultCount !== undefined) { + const next = resultIndex + 1; + setResultIndex(next >= resultCount ? 0 : next); + } + } + }; return { lines: shouldFilter ? filter.lines : lines, @@ -91,9 +108,9 @@ export function useLogViewerSearch(lines: AnsiLine[]): LogViewerSearch { setSearchInput, shouldFilter, toggleShouldFilter, + resultCount, resultIndex, - resultCount: filter.results?.length, - setResultIndex, + resultIndexStep, resultLine: searchResult?.lineNumber, resultLineIndex: searchResult?.lineIndex, }; From 6078c7147a7911d6c5a49dd7fd030afb24ed807d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Dec 2021 23:44:37 +0100 Subject: [PATCH 100/116] core-components: tests + fix for LogViewer selection handling Signed-off-by: Patrik Oldsberg --- .../LogViewer/useLogViewerSelection.test.tsx | 123 ++++++++++++++++++ .../LogViewer/useLogViewerSelection.tsx | 2 +- 2 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx diff --git a/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx b/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx new file mode 100644 index 0000000000..7e00d50f33 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx @@ -0,0 +1,123 @@ +/* + * 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 from 'react'; +import { act, renderHook } from '@testing-library/react-hooks'; +import { TestApiProvider, MockErrorApi } from '@backstage/test-utils'; +import { errorApiRef } from '@backstage/core-plugin-api'; +import { AnsiLine } from './AnsiProcessor'; +import { useLogViewerSelection } from './useLogViewerSelection'; +// eslint-disable-next-line import/no-extraneous-dependencies +import copyToClipboard from 'copy-to-clipboard'; + +// Used by useCopyToClipboard +jest.mock('copy-to-clipboard', () => ({ + __esModule: true, + default: jest.fn(), +})); + +const lines = [ + new AnsiLine(1, [{ text: '1', modifiers: {} }]), + new AnsiLine(2, [{ text: '2', modifiers: {} }]), + new AnsiLine(3, [{ text: '3', modifiers: {} }]), + new AnsiLine(4, [{ text: '4', modifiers: {} }]), + new AnsiLine(5, [{ text: '5', modifiers: {} }]), +]; + +describe('useLogViewerSelection', () => { + it('should manage a selection', () => { + const rendered = renderHook(() => useLogViewerSelection(lines), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + expect(rendered.result.current.isSelected(1)).toBe(false); + expect(rendered.result.current.isSelected(2)).toBe(false); + expect(rendered.result.current.isSelected(3)).toBe(false); + + expect(rendered.result.current.shouldShowButton(1)).toBe(false); + expect(rendered.result.current.shouldShowButton(2)).toBe(false); + expect(rendered.result.current.shouldShowButton(3)).toBe(false); + + act(() => rendered.result.current.setSelection(2, false)); + + expect(rendered.result.current.isSelected(1)).toBe(false); + expect(rendered.result.current.isSelected(2)).toBe(true); + expect(rendered.result.current.isSelected(3)).toBe(false); + + expect(rendered.result.current.shouldShowButton(1)).toBe(false); + expect(rendered.result.current.shouldShowButton(2)).toBe(true); + expect(rendered.result.current.shouldShowButton(3)).toBe(false); + + act(() => rendered.result.current.setSelection(3, false)); + + expect(rendered.result.current.isSelected(1)).toBe(false); + expect(rendered.result.current.isSelected(2)).toBe(false); + expect(rendered.result.current.isSelected(3)).toBe(true); + expect(rendered.result.current.isSelected(4)).toBe(false); + + expect(rendered.result.current.shouldShowButton(1)).toBe(false); + expect(rendered.result.current.shouldShowButton(2)).toBe(false); + expect(rendered.result.current.shouldShowButton(3)).toBe(true); + expect(rendered.result.current.shouldShowButton(4)).toBe(false); + + act(() => rendered.result.current.setSelection(1, true)); + + expect(rendered.result.current.isSelected(1)).toBe(true); + expect(rendered.result.current.isSelected(2)).toBe(true); + expect(rendered.result.current.isSelected(3)).toBe(true); + expect(rendered.result.current.isSelected(4)).toBe(false); + + expect(rendered.result.current.shouldShowButton(1)).toBe(true); + expect(rendered.result.current.shouldShowButton(2)).toBe(false); + expect(rendered.result.current.shouldShowButton(3)).toBe(true); + expect(rendered.result.current.shouldShowButton(4)).toBe(false); + + act(() => rendered.result.current.setSelection(4, true)); + + expect(rendered.result.current.isSelected(1)).toBe(false); + expect(rendered.result.current.isSelected(2)).toBe(false); + expect(rendered.result.current.isSelected(3)).toBe(true); + expect(rendered.result.current.isSelected(4)).toBe(true); + expect(rendered.result.current.isSelected(5)).toBe(false); + + expect(rendered.result.current.shouldShowButton(1)).toBe(false); + expect(rendered.result.current.shouldShowButton(2)).toBe(false); + expect(rendered.result.current.shouldShowButton(3)).toBe(true); + expect(rendered.result.current.shouldShowButton(4)).toBe(true); + expect(rendered.result.current.shouldShowButton(5)).toBe(false); + + expect(copyToClipboard).not.toHaveBeenCalled(); + act(() => rendered.result.current.copySelection()); + expect(copyToClipboard).toHaveBeenLastCalledWith('3\n4'); + + act(() => rendered.result.current.setSelection(2, true)); + act(() => rendered.result.current.setSelection(4, true)); + + act(() => rendered.result.current.copySelection()); + expect(copyToClipboard).toHaveBeenCalledWith('2\n3\n4'); + + act(() => rendered.result.current.setSelection(2, false)); + act(() => rendered.result.current.setSelection(4, false)); + act(() => rendered.result.current.setSelection(4, false)); + act(() => rendered.result.current.setSelection(5, true)); + act(() => rendered.result.current.copySelection()); + expect(copyToClipboard).toHaveBeenCalledWith('5'); + }); +}); diff --git a/packages/core-components/src/components/LogViewer/useLogViewerSelection.tsx b/packages/core-components/src/components/LogViewer/useLogViewerSelection.tsx index cae56e5e43..3bed43d0c5 100644 --- a/packages/core-components/src/components/LogViewer/useLogViewerSelection.tsx +++ b/packages/core-components/src/components/LogViewer/useLogViewerSelection.tsx @@ -59,7 +59,7 @@ export function useLogViewerSelection(lines: AnsiLine[]) { copySelection() { if (sel) { const copyText = lines - .slice(sel.start - 1, sel.end) + .slice(Math.min(sel.start, sel.end) - 1, Math.max(sel.start, sel.end)) .map(l => l.text) .join('\n'); copyToClipboard(copyText); From da0fdf9b5eade742dc452acbf53ea87092bb14f9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Dec 2021 00:32:25 +0100 Subject: [PATCH 101/116] core-components: added test for LogViewer and fix copy Signed-off-by: Patrik Oldsberg --- .../LogViewer/RealLogViewer.test.tsx | 79 +++++++++++++++++++ .../components/LogViewer/RealLogViewer.tsx | 1 + .../LogViewer/useLogViewerSelection.tsx | 2 +- 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx diff --git a/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx b/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx new file mode 100644 index 0000000000..8d5efb9724 --- /dev/null +++ b/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx @@ -0,0 +1,79 @@ +/* + * 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, { ReactNode } from 'react'; +import UserEvent from '@testing-library/user-event'; +import { renderInTestApp } from '@backstage/test-utils'; +import { RealLogViewer } from './RealLogViewer'; +// eslint-disable-next-line import/no-extraneous-dependencies +import copyToClipboard from 'copy-to-clipboard'; + +// Used by useCopyToClipboard +jest.mock('copy-to-clipboard', () => ({ + __esModule: true, + default: jest.fn(), +})); + +// The inside needs mocking to render in jsdom +jest.mock('react-virtualized-auto-sizer', () => ({ + __esModule: true, + default: (props: { + children: (size: { width: number; height: number }) => ReactNode; + }) => <>{props.children({ width: 400, height: 200 })}, +})); + +const testText = `Some Log Line +Derp +Foo + +Foo Foo +Wat`; + +describe('RealLogViewer', () => { + it('should render text with search and filtering and copying', async () => { + const rendered = await renderInTestApp(); + expect(rendered.getByText('Derp')).toBeInTheDocument(); + expect(rendered.getByText('Foo Foo')).toBeInTheDocument(); + + UserEvent.tab(); + UserEvent.keyboard('Foo'); + + expect(rendered.getByText('1/3')).toBeInTheDocument(); + UserEvent.keyboard('{enter}'); + expect(rendered.getByText('2/3')).toBeInTheDocument(); + UserEvent.keyboard('{enter}'); + expect(rendered.getByText('3/3')).toBeInTheDocument(); + UserEvent.keyboard('{enter}'); + expect(rendered.getByText('1/3')).toBeInTheDocument(); + UserEvent.keyboard('{shift}{enter}{/shift}'); + expect(rendered.getByText('3/3')).toBeInTheDocument(); + + expect(rendered.queryByText('Some Log Line')).toBeInTheDocument(); + UserEvent.keyboard('{meta}{enter}{/meta}'); + expect(rendered.queryByText('Some Log Line')).not.toBeInTheDocument(); + UserEvent.keyboard('{meta}{enter}{/meta}'); + expect(rendered.queryByText('Some Log Line')).toBeInTheDocument(); + + // Tab down to line #2 and click + UserEvent.tab(); + UserEvent.tab(); + UserEvent.tab(); + UserEvent.click(document.activeElement!); + UserEvent.click(rendered.getByTestId('copy-button')); + + expect(copyToClipboard).toHaveBeenCalledWith('Derp'); + }); +}); diff --git a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx index d41ce80672..47bc05c85c 100644 --- a/packages/core-components/src/components/LogViewer/RealLogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/RealLogViewer.tsx @@ -88,6 +88,7 @@ export function RealLogViewer(props: RealLogViewerProps) { > {selection.shouldShowButton(lineNumber) && ( selection.copySelection()} diff --git a/packages/core-components/src/components/LogViewer/useLogViewerSelection.tsx b/packages/core-components/src/components/LogViewer/useLogViewerSelection.tsx index 3bed43d0c5..a41f5e7159 100644 --- a/packages/core-components/src/components/LogViewer/useLogViewerSelection.tsx +++ b/packages/core-components/src/components/LogViewer/useLogViewerSelection.tsx @@ -60,7 +60,7 @@ export function useLogViewerSelection(lines: AnsiLine[]) { if (sel) { const copyText = lines .slice(Math.min(sel.start, sel.end) - 1, Math.max(sel.start, sel.end)) - .map(l => l.text) + .map(l => l.chunks.map(c => c.text).join('')) .join('\n'); copyToClipboard(copyText); setSelection(undefined); From 1efed71f6018e2ba8f3e0418b0183be1f5a3308c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Dec 2021 00:38:45 +0100 Subject: [PATCH 102/116] core-components: make LogViewer styles overridable Signed-off-by: Patrik Oldsberg --- .../src/components/LogViewer/index.ts | 1 + .../src/components/LogViewer/styles.ts | 247 ++++++++++-------- .../src/overridableComponents.ts | 2 + 3 files changed, 144 insertions(+), 106 deletions(-) diff --git a/packages/core-components/src/components/LogViewer/index.ts b/packages/core-components/src/components/LogViewer/index.ts index 839f34f81e..f99695f163 100644 --- a/packages/core-components/src/components/LogViewer/index.ts +++ b/packages/core-components/src/components/LogViewer/index.ts @@ -16,3 +16,4 @@ export { LogViewer } from './LogViewer'; export type { LogViewerProps } from './LogViewer'; +export type { LogViewerClassKey } from './styles'; diff --git a/packages/core-components/src/components/LogViewer/styles.ts b/packages/core-components/src/components/LogViewer/styles.ts index 4f4312bdd9..edb2086675 100644 --- a/packages/core-components/src/components/LogViewer/styles.ts +++ b/packages/core-components/src/components/LogViewer/styles.ts @@ -19,114 +19,149 @@ import * as colors from '@material-ui/core/colors'; export const HEADER_SIZE = 40; -export const useStyles = makeStyles(theme => ({ - root: { - background: theme.palette.background.paper, - }, - header: { - height: HEADER_SIZE, - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-end', - }, - log: { - fontFamily: '"Monaco", monospace', - fontSize: theme.typography.pxToRem(12), - }, - line: { - position: 'relative', - whiteSpace: 'pre', +export type LogViewerClassKey = + | 'root' + | 'header' + | 'log' + | 'line' + | 'lineSelected' + | 'lineCopyButton' + | 'lineNumber' + | 'textHighlight' + | 'textSelectedHighlight' + | 'modifierBold' + | 'modifierItalic' + | 'modifierUnderline' + | 'modifierForegroundBlack' + | 'modifierForegroundRed' + | 'modifierForegroundGreen' + | 'modifierForegroundYellow' + | 'modifierForegroundBlue' + | 'modifierForegroundMagenta' + | 'modifierForegroundCyan' + | 'modifierForegroundWhite' + | 'modifierForegroundGrey' + | 'modifierBackgroundBlack' + | 'modifierBackgroundRed' + | 'modifierBackgroundGreen' + | 'modifierBackgroundYellow' + | 'modifierBackgroundBlue' + | 'modifierBackgroundMagenta' + | 'modifierBackgroundCyan' + | 'modifierBackgroundWhite' + | 'modifierBackgroundGrey'; - '&:hover': { - background: theme.palette.action.hover, +export const useStyles = makeStyles( + theme => ({ + root: { + background: theme.palette.background.paper, }, - }, - lineSelected: { - background: theme.palette.action.selected, + header: { + height: HEADER_SIZE, + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + }, + log: { + fontFamily: '"Monaco", monospace', + fontSize: theme.typography.pxToRem(12), + }, + line: { + position: 'relative', + whiteSpace: 'pre', - '&:hover': { + '&:hover': { + background: theme.palette.action.hover, + }, + }, + lineSelected: { background: theme.palette.action.selected, + + '&:hover': { + background: theme.palette.action.selected, + }, }, - }, - lineCopyButton: { - position: 'absolute', - paddingTop: 0, - paddingBottom: 0, - }, - lineNumber: { - display: 'inline-block', - textAlign: 'end', - width: 60, - marginRight: theme.spacing(1), - cursor: 'pointer', - }, - textHighlight: { - background: alpha(theme.palette.info.main, 0.15), - }, - textSelectedHighlight: { - background: alpha(theme.palette.info.main, 0.4), - }, - modifierBold: { - fontWeight: theme.typography.fontWeightBold, - }, - modifierItalic: { - fontStyle: 'italic', - }, - modifierUnderline: { - textDecoration: 'underline', - }, - modifierForegroundBlack: { - color: colors.common.black, - }, - modifierForegroundRed: { - color: colors.red[500], - }, - modifierForegroundGreen: { - color: colors.green[500], - }, - modifierForegroundYellow: { - color: colors.yellow[500], - }, - modifierForegroundBlue: { - color: colors.blue[500], - }, - modifierForegroundMagenta: { - color: colors.purple[500], - }, - modifierForegroundCyan: { - color: colors.cyan[500], - }, - modifierForegroundWhite: { - color: colors.common.white, - }, - modifierForegroundGrey: { - color: colors.grey[500], - }, - modifierBackgroundBlack: { - background: colors.common.black, - }, - modifierBackgroundRed: { - background: colors.red[500], - }, - modifierBackgroundGreen: { - background: colors.green[500], - }, - modifierBackgroundYellow: { - background: colors.yellow[500], - }, - modifierBackgroundBlue: { - background: colors.blue[500], - }, - modifierBackgroundMagenta: { - background: colors.purple[500], - }, - modifierBackgroundCyan: { - background: colors.cyan[500], - }, - modifierBackgroundWhite: { - background: colors.common.white, - }, - modifierBackgroundGrey: { - background: colors.grey[500], - }, -})); + lineCopyButton: { + position: 'absolute', + paddingTop: 0, + paddingBottom: 0, + }, + lineNumber: { + display: 'inline-block', + textAlign: 'end', + width: 60, + marginRight: theme.spacing(1), + cursor: 'pointer', + }, + textHighlight: { + background: alpha(theme.palette.info.main, 0.15), + }, + textSelectedHighlight: { + background: alpha(theme.palette.info.main, 0.4), + }, + modifierBold: { + fontWeight: theme.typography.fontWeightBold, + }, + modifierItalic: { + fontStyle: 'italic', + }, + modifierUnderline: { + textDecoration: 'underline', + }, + modifierForegroundBlack: { + color: colors.common.black, + }, + modifierForegroundRed: { + color: colors.red[500], + }, + modifierForegroundGreen: { + color: colors.green[500], + }, + modifierForegroundYellow: { + color: colors.yellow[500], + }, + modifierForegroundBlue: { + color: colors.blue[500], + }, + modifierForegroundMagenta: { + color: colors.purple[500], + }, + modifierForegroundCyan: { + color: colors.cyan[500], + }, + modifierForegroundWhite: { + color: colors.common.white, + }, + modifierForegroundGrey: { + color: colors.grey[500], + }, + modifierBackgroundBlack: { + background: colors.common.black, + }, + modifierBackgroundRed: { + background: colors.red[500], + }, + modifierBackgroundGreen: { + background: colors.green[500], + }, + modifierBackgroundYellow: { + background: colors.yellow[500], + }, + modifierBackgroundBlue: { + background: colors.blue[500], + }, + modifierBackgroundMagenta: { + background: colors.purple[500], + }, + modifierBackgroundCyan: { + background: colors.cyan[500], + }, + modifierBackgroundWhite: { + background: colors.common.white, + }, + modifierBackgroundGrey: { + background: colors.grey[500], + }, + }), + { name: 'BackstageLogViewer' }, +); diff --git a/packages/core-components/src/overridableComponents.ts b/packages/core-components/src/overridableComponents.ts index af35445e24..98b8f83abd 100644 --- a/packages/core-components/src/overridableComponents.ts +++ b/packages/core-components/src/overridableComponents.ts @@ -34,6 +34,7 @@ import { LifecycleClassKey, MarkdownContentClassKey, LoginRequestListItemClassKey, + LogViewerClassKey, OAuthRequestDialogClassKey, OverflowTooltipClassKey, GaugeClassKey, @@ -110,6 +111,7 @@ type BackstageComponentsNameToClassKey = { BackstageLifecycle: LifecycleClassKey; BackstageMarkdownContent: MarkdownContentClassKey; BackstageLoginRequestListItem: LoginRequestListItemClassKey; + BackstageLogViewer: LogViewerClassKey; OAuthRequestDialog: OAuthRequestDialogClassKey; BackstageOverflowTooltip: OverflowTooltipClassKey; BackstageGauge: GaugeClassKey; From 85d54b888cefbab8e7d025d6ce558e3cc9106f73 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Dec 2021 00:57:35 +0100 Subject: [PATCH 103/116] core-components: update API report for LogViewer + fixes Signed-off-by: Patrik Oldsberg --- packages/core-components/api-report.md | 42 +++++++++++++++++++ .../src/components/LogViewer/LogViewer.tsx | 4 ++ .../src/components/LogViewer/styles.ts | 1 + 3 files changed, 47 insertions(+) diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index cc7db32586..a8611ad299 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -605,6 +605,48 @@ export type LinkProps = LinkProps_2 & // @public (undocumented) export type LoginRequestListItemClassKey = 'root'; +// @public +export function LogViewer(props: LogViewerProps): JSX.Element; + +// @public +export type LogViewerClassKey = + | 'root' + | 'header' + | 'log' + | 'line' + | 'lineSelected' + | 'lineCopyButton' + | 'lineNumber' + | 'textHighlight' + | 'textSelectedHighlight' + | 'modifierBold' + | 'modifierItalic' + | 'modifierUnderline' + | 'modifierForegroundBlack' + | 'modifierForegroundRed' + | 'modifierForegroundGreen' + | 'modifierForegroundYellow' + | 'modifierForegroundBlue' + | 'modifierForegroundMagenta' + | 'modifierForegroundCyan' + | 'modifierForegroundWhite' + | 'modifierForegroundGrey' + | 'modifierBackgroundBlack' + | 'modifierBackgroundRed' + | 'modifierBackgroundGreen' + | 'modifierBackgroundYellow' + | 'modifierBackgroundBlue' + | 'modifierBackgroundMagenta' + | 'modifierBackgroundCyan' + | 'modifierBackgroundWhite' + | 'modifierBackgroundGrey'; + +// @public +export interface LogViewerProps { + className?: string; + text: string; +} + // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "MarkdownContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/packages/core-components/src/components/LogViewer/LogViewer.tsx b/packages/core-components/src/components/LogViewer/LogViewer.tsx index a85603b465..643d2ba9f8 100644 --- a/packages/core-components/src/components/LogViewer/LogViewer.tsx +++ b/packages/core-components/src/components/LogViewer/LogViewer.tsx @@ -23,6 +23,8 @@ const RealLogViewer = lazy(() => /** * The properties for the LogViewer component. + * + * @public */ export interface LogViewerProps { /** @@ -47,6 +49,8 @@ export interface LogViewerProps { * log is sized automatically to fill the available vertical space. This means * it may often be needed to wrap the LogViewer in a container that provides it * with a fixed amount of space. + * + * @public */ export function LogViewer(props: LogViewerProps) { const { Progress } = useApp().getComponents(); diff --git a/packages/core-components/src/components/LogViewer/styles.ts b/packages/core-components/src/components/LogViewer/styles.ts index edb2086675..2028809543 100644 --- a/packages/core-components/src/components/LogViewer/styles.ts +++ b/packages/core-components/src/components/LogViewer/styles.ts @@ -19,6 +19,7 @@ import * as colors from '@material-ui/core/colors'; export const HEADER_SIZE = 40; +/** @public Class keys for overriding LogViewer styles */ export type LogViewerClassKey = | 'root' | 'header' From fa0b8ab6a89db893183f4de1bf27c654db0ea850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 8 Dec 2021 13:47:04 +0100 Subject: [PATCH 104/116] patch level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/rare-toes-burn.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/rare-toes-burn.md b/.changeset/rare-toes-burn.md index 264576eb58..2865eed51f 100644 --- a/.changeset/rare-toes-burn.md +++ b/.changeset/rare-toes-burn.md @@ -1,5 +1,6 @@ --- -'@backstage/plugin-azure-devops-backend': minor +'@backstage/plugin-azure-devops-backend': patch +'@backstage/plugin-azure-devops-common': patch --- Added getting builds by definition name From ed3a7567a0e1638cc372f1e167fc84ae08ba41a6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 6 Dec 2021 00:59:31 +0100 Subject: [PATCH 105/116] core-components: remove dead code in LogViewer Signed-off-by: Patrik Oldsberg chore: updating new yarn.lock changes Signed-off-by: blam --- .../src/components/LogViewer/AnsiProcessor.ts | 5 -- yarn.lock | 49 +++++++++++++++++-- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/packages/core-components/src/components/LogViewer/AnsiProcessor.ts b/packages/core-components/src/components/LogViewer/AnsiProcessor.ts index 5c4bf58865..d0f835a70e 100644 --- a/packages/core-components/src/components/LogViewer/AnsiProcessor.ts +++ b/packages/core-components/src/components/LogViewer/AnsiProcessor.ts @@ -71,11 +71,6 @@ export interface ChunkModifiers { underline?: boolean; } -// export interface AnsiLine { -// lineNumber: number; -// chunks: AnsiChunk[]; -// } - export interface AnsiChunk { text: string; modifiers: ChunkModifiers; diff --git a/yarn.lock b/yarn.lock index 015d5d6e79..f1c17c0022 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6974,6 +6974,13 @@ dependencies: "@types/node" "*" +"@types/ansi-regex@^5.0.0": + version "5.0.0" + resolved "https://registry.npmjs.org/@types/ansi-regex/-/ansi-regex-5.0.0.tgz#569a5189a92cc46d63fb2ad91e6b130f33d999c1" + integrity sha512-SQafVL3pXFh/5qq/nN6p5858g//zSVzcb8JzCLtoVxm8YNPggMQfEIm7aaTNysxpw1S+lFTaW8kv+aR0/CEhCA== + dependencies: + ansi-regex "*" + "@types/archiver@^5.1.0": version "5.3.0" resolved "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.0.tgz#2b34ba56d4d7102d256b922c7e91e09eab79db6f" @@ -8088,6 +8095,20 @@ dependencies: "@types/react" "*" +"@types/react-virtualized-auto-sizer@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz#b3187dae1dfc4c15880c9cfc5b45f2719ea6ebd4" + integrity sha512-GH8sAnBEM5GV9LTeiz56r4ZhMOUSrP43tAQNSRVxNexDjcNKLCEtnxusAItg1owFUFE6k0NslV26gqVClVvong== + dependencies: + "@types/react" "*" + +"@types/react-window@^1.8.5": + version "1.8.5" + resolved "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1" + integrity sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw== + dependencies: + "@types/react" "*" + "@types/react@*", "@types/react@>=16.9.0", "@types/react@^16.13.1 || ^17.0.0": version "16.14.18" resolved "https://registry.npmjs.org/@types/react/-/react-16.14.18.tgz#b2bcea05ee244fde92d409f91bd888ca8e54b20f" @@ -9227,6 +9248,11 @@ ansi-html@0.0.7, ansi-html@^0.0.7: resolved "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= +ansi-regex@*, ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -9247,11 +9273,6 @@ ansi-regex@^5.0.0, ansi-regex@^5.0.1: resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== - ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -20516,6 +20537,11 @@ memjs@^1.3.0: resolved "https://registry.npmjs.org/memjs/-/memjs-1.3.0.tgz#b7959b4ff3770e4c785463fd147f1e4fafd47a24" integrity sha512-y/V9a0auepA9Lgyr4QieK6K2FczjHucEdTpSS+hHVNmVEkYxruXhkHu8n6DSRQ4HXHEE3cc6Sf9f88WCJXGXsQ== +"memoize-one@>=3.1.1 <6": + version "5.2.1" + resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + memoize-one@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" @@ -24799,6 +24825,11 @@ react-use@^17.2.4: ts-easing "^0.2.0" tslib "^2.1.0" +react-virtualized-auto-sizer@^1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz#66c5b1c9278064c5ef1699ed40a29c11518f97ca" + integrity sha512-7tQ0BmZqfVF6YYEWcIGuoR3OdYe8I/ZFbNclFlGOC3pMqunkYF/oL30NCjSGl9sMEb17AnzixDz98Kqc3N76HQ== + react-virtualized@^9.21.0: version "9.21.2" resolved "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.21.2.tgz#02e6df65c1e020c8dbf574ec4ce971652afca84e" @@ -24811,6 +24842,14 @@ react-virtualized@^9.21.0: prop-types "^15.6.0" react-lifecycles-compat "^3.0.4" +react-window@^1.8.6: + version "1.8.6" + resolved "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz#d011950ac643a994118632665aad0c6382e2a112" + integrity sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg== + dependencies: + "@babel/runtime" "^7.0.0" + memoize-one ">=3.1.1 <6" + react@^16.12.0, react@^16.13.1: version "16.13.1" resolved "https://registry.npmjs.org/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" From 6bccc7d794ebec402256de35021f95d7fea3b664 Mon Sep 17 00:00:00 2001 From: Minn Soe Date: Wed, 8 Dec 2021 13:21:53 +0000 Subject: [PATCH 106/116] changesets: add gitlab changes in catalog-backend Signed-off-by: Minn Soe --- .changeset/hip-bananas-laugh.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/hip-bananas-laugh.md diff --git a/.changeset/hip-bananas-laugh.md b/.changeset/hip-bananas-laugh.md new file mode 100644 index 0000000000..c3f09da880 --- /dev/null +++ b/.changeset/hip-bananas-laugh.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +The `pagedRequest` method in the GitLab ingestion client is now public for re-use and may be used to make other calls to the GitLab API. Developers can now pass in a type into the GitLab `paginated` and `pagedRequest` functions as generics instead of forcing `any` (defaults to `any` to maintain compatibility). The `GitLabClient` now provides a `isSelfManaged` convenience method. From bf736f80580f61f07e555c668790f03ab14aa3c8 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 8 Dec 2021 14:39:11 +0100 Subject: [PATCH 107/116] CI: Install build deps to workaround internet issues Signed-off-by: Johan Haals --- .github/workflows/chromatic-storybook-test.yml | 5 +++++ .github/workflows/ci.yml | 5 +++++ .github/workflows/e2e.yml | 6 ++++++ .github/workflows/master.yml | 5 +++++ .github/workflows/nightly.yml | 6 ++++++ .github/workflows/prettify.yml | 6 ++++++ .github/workflows/snyk-github-issue-sync.yml | 5 +++++ .github/workflows/techdocs-e2e.yml | 5 +++++ 8 files changed, 43 insertions(+) diff --git a/.github/workflows/chromatic-storybook-test.yml b/.github/workflows/chromatic-storybook-test.yml index 4c64db309d..1ff4e31553 100644 --- a/.github/workflows/chromatic-storybook-test.yml +++ b/.github/workflows/chromatic-storybook-test.yml @@ -39,6 +39,11 @@ jobs: key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- + # https://github.com/Automattic/node-canvas/issues/1945 + - name: Install dependencies to fix temporary issue in canvas build + run: | + sudo apt update + sudo apt install -y libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev librsvg2-dev - name: yarn install run: yarn install --frozen-lockfile # End of yarn setup diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7222edef08..d84f974ba8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,11 @@ jobs: INTEGRATION_TEST_AZURE_TOKEN: ${{ secrets.INTEGRATION_TEST_AZURE_TOKEN }} steps: + # https://github.com/Automattic/node-canvas/issues/1945 + - name: Install dependencies to fix temporary issue in canvas build + run: | + sudo apt update + sudo apt install -y libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev librsvg2-dev - uses: actions/checkout@v2 - name: fetch branch master run: git fetch origin master diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 287ef91f16..289841f170 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -35,6 +35,12 @@ jobs: name: Node ${{ matrix.node-version }} on ${{ matrix.os }} steps: + # https://github.com/Automattic/node-canvas/issues/1945 + - name: Install dependencies to fix temporary issue in canvas build + run: | + sudo apt update + sudo apt install -y libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev librsvg2-dev + - uses: actions/checkout@v2 # Beginning of yarn setup, keep in sync between all workflows, see ci.yml diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 8106dd1198..2fa67bf4ad 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -86,6 +86,11 @@ jobs: key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- + # https://github.com/Automattic/node-canvas/issues/1945 + - name: Install dependencies to fix temporary issue in canvas build + run: | + sudo apt update + sudo apt install -y libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev librsvg2-dev - name: yarn install run: yarn install --frozen-lockfile # End of yarn setup diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 846882d853..aa2f4b3376 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -45,6 +45,12 @@ jobs: key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- + # https://github.com/Automattic/node-canvas/issues/1945 + - name: Install dependencies to fix temporary issue in canvas build + run: | + sudo apt update + sudo apt install -y libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev librsvg2-dev + - name: yarn install run: yarn install --frozen-lockfile # End of yarn setup diff --git a/.github/workflows/prettify.yml b/.github/workflows/prettify.yml index 4a34ff0c88..60a9aa6d0e 100644 --- a/.github/workflows/prettify.yml +++ b/.github/workflows/prettify.yml @@ -39,6 +39,12 @@ jobs: key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- + # https://github.com/Automattic/node-canvas/issues/1945 + - name: Install dependencies to fix temporary issue in canvas build + run: | + sudo apt update + sudo apt install -y libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev librsvg2-dev + - name: yarn install run: yarn install --frozen-lockfile # End of yarn setup diff --git a/.github/workflows/snyk-github-issue-sync.yml b/.github/workflows/snyk-github-issue-sync.yml index 7b93374609..591ad566eb 100644 --- a/.github/workflows/snyk-github-issue-sync.yml +++ b/.github/workflows/snyk-github-issue-sync.yml @@ -41,6 +41,11 @@ jobs: key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- + # https://github.com/Automattic/node-canvas/issues/1945 + - name: Install dependencies to fix temporary issue in canvas build + run: | + sudo apt update + sudo apt install -y libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev librsvg2-dev - name: yarn install run: yarn install --frozen-lockfile # End of yarn setup diff --git a/.github/workflows/techdocs-e2e.yml b/.github/workflows/techdocs-e2e.yml index 191274ec84..b41f5be11a 100644 --- a/.github/workflows/techdocs-e2e.yml +++ b/.github/workflows/techdocs-e2e.yml @@ -21,6 +21,11 @@ jobs: NODE_OPTIONS: --max-old-space-size=4096 steps: + # https://github.com/Automattic/node-canvas/issues/1945 + - name: Install dependencies to fix temporary issue in canvas build + run: | + sudo apt update + sudo apt install -y libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev librsvg2-dev - uses: actions/checkout@v2 - uses: actions/setup-python@v2 From d2ad94df9734340051ef53a9baffda2f1b39b81a Mon Sep 17 00:00:00 2001 From: radoslaw-wielonski-nc <86358570+radoslaw-wielonski-nc@users.noreply.github.com> Date: Wed, 8 Dec 2021 15:07:24 +0100 Subject: [PATCH 108/116] Update packages/backend-common/src/service/types.ts Co-authored-by: Johan Haals Signed-off-by: Radoslaw Wielonski --- packages/backend-common/src/service/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend-common/src/service/types.ts b/packages/backend-common/src/service/types.ts index 07075b4d16..17ce24c58d 100644 --- a/packages/backend-common/src/service/types.ts +++ b/packages/backend-common/src/service/types.ts @@ -111,7 +111,6 @@ export type ServiceBuilder = { /** * Disable default error handler * - * If it's not called, default error handler is used */ disableDefaultErrorHandler(): ServiceBuilder; From 0105e3cd6447d6b181b0e1362c98efd49b4e0fce Mon Sep 17 00:00:00 2001 From: Radoslaw Wielonski Date: Wed, 8 Dec 2021 15:11:07 +0100 Subject: [PATCH 109/116] docs: update documentation of setErrorHandler method Signed-off-by: Radoslaw Wielonski --- packages/backend-common/src/service/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend-common/src/service/types.ts b/packages/backend-common/src/service/types.ts index 17ce24c58d..2f2a3e2f4c 100644 --- a/packages/backend-common/src/service/types.ts +++ b/packages/backend-common/src/service/types.ts @@ -101,8 +101,8 @@ export type ServiceBuilder = { /** * Sets an additional errorHandler to run before the defaultErrorHandler. * - * If we want to use only custom errorHandler without defaultErrorHandler we need to - * disable the defaultErrorHandler by invoking disableDefaultErrorHandler() + * For execution of only the custom error handler make sure to also invoke disableDefaultErrorHandler() + * otherwise the defaultErrorHandler is executed at the end of the error middleware chain. * * @param errorHandler - an error handler */ From aec2c96c88ea39e5a1194ea44729ef951b1be287 Mon Sep 17 00:00:00 2001 From: Radoslaw Wielonski Date: Wed, 8 Dec 2021 15:13:37 +0100 Subject: [PATCH 110/116] docs: update documentation of disableDefaultErrorHandler method Signed-off-by: Radoslaw Wielonski --- packages/backend-common/src/service/types.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/backend-common/src/service/types.ts b/packages/backend-common/src/service/types.ts index 2f2a3e2f4c..3e94196006 100644 --- a/packages/backend-common/src/service/types.ts +++ b/packages/backend-common/src/service/types.ts @@ -109,8 +109,7 @@ export type ServiceBuilder = { setErrorHandler(errorHandler: ErrorRequestHandler): ServiceBuilder; /** - * Disable default error handler - * + * Disables the default error handler */ disableDefaultErrorHandler(): ServiceBuilder; From 1e48c3de825e43b03b1924ff8d20279bd75e77b5 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 8 Dec 2021 15:30:32 +0100 Subject: [PATCH 111/116] chore: fixing the last of the mpl violations Signed-off-by: blam --- packages/app/package.json | 1 - .../app/src/components/catalog/EntityPage.tsx | 9 +- yarn.lock | 99 +------------------ 3 files changed, 6 insertions(+), 103 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 4d7e7d9a62..07e0b4a00d 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -50,7 +50,6 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@octokit/rest": "^18.5.3", - "@roadiehq/backstage-plugin-buildkite": "^1.0.8", "@roadiehq/backstage-plugin-github-insights": "^1.1.23", "@roadiehq/backstage-plugin-github-pull-requests": "^1.0.13", "@roadiehq/backstage-plugin-travis-ci": "^1.0.11", diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 3b9b381380..9810f49386 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -106,10 +106,7 @@ import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; import { EntityTodoContent } from '@backstage/plugin-todo'; import { Button, Grid } from '@material-ui/core'; import BadgeIcon from '@material-ui/icons/CallToAction'; -import { - EntityBuildkiteContent, - isBuildkiteAvailable, -} from '@roadiehq/backstage-plugin-buildkite'; + import { EntityGithubInsightsContent, EntityGithubInsightsLanguagesCard, @@ -168,10 +165,6 @@ export const cicdContent = ( - - - - diff --git a/yarn.lock b/yarn.lock index f1c17c0022..beb3700b4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4629,7 +4629,7 @@ react-beautiful-dnd "^13.0.0" react-double-scrollbar "0.0.15" -"@material-ui/core@^4.11.0", "@material-ui/core@^4.11.3", "@material-ui/core@^4.12.1", "@material-ui/core@^4.12.2": +"@material-ui/core@^4.11.0", "@material-ui/core@^4.11.3", "@material-ui/core@^4.12.2": version "4.12.3" resolved "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz#80d665caf0f1f034e52355c5450c0e38b099d3ca" integrity sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw== @@ -4734,13 +4734,6 @@ prop-types "^15.7.2" react-is "^16.8.0 || ^17.0.0" -"@mattiasbuelens/web-streams-polyfill@^0.2.0": - version "0.2.1" - resolved "https://registry.npmjs.org/@mattiasbuelens/web-streams-polyfill/-/web-streams-polyfill-0.2.1.tgz#d7c4aa94f98084ec0787be084d47167d62ea5f67" - integrity sha512-oKuFCQFa3W7Hj7zKn0+4ypI8JFm4ZKIoncwAC6wd5WwFW2sL7O1hpPoJdSWpynQ4DJ4lQ6MvFoVDmCLilonDFg== - dependencies: - "@types/whatwg-streams" "^0.0.7" - "@mdx-js/mdx@^1.6.22": version "1.6.22" resolved "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba" @@ -5445,29 +5438,6 @@ resolved "https://registry.npmjs.org/@rjsf/material-ui/-/material-ui-3.2.1.tgz#84fbf322485aee3a84101e189161f0687779ec8d" integrity sha512-8UiDeDbjCImFSfOegGu13otQ7OdP9FOYpcLjeouppnhs+MPeIEAtYS+jCcBKmi3reyTagC15/KVSRhde1wS1vg== -"@roadiehq/backstage-plugin-buildkite@^1.0.8": - version "1.0.8" - resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-buildkite/-/backstage-plugin-buildkite-1.0.8.tgz#c377ae194682426a957366e85263749ff75b8db1" - integrity sha512-v3OOQj5Ksvs/8SZcNBgq6bzmyJvMIzdGsQrwW8uaLgOX076T0nmK8c3Ojz3w1opymEl3s20sLxLUXjhd7V/nIA== - dependencies: - "@backstage/catalog-model" "^0.9.0" - "@backstage/core-app-api" "^0.1.3" - "@backstage/core-components" "^0.3.0" - "@backstage/core-plugin-api" "^0.1.3" - "@backstage/plugin-catalog-react" "^0.4.0" - "@backstage/theme" "^0.2.6" - "@material-ui/core" "^4.12.1" - "@material-ui/icons" "^4.11.2" - "@material-ui/lab" "4.0.0-alpha.57" - history "^5.0.0" - moment "^2.29.1" - react "^16.13.1" - react-dom "^16.13.1" - react-lazylog "^4.5.2" - react-router "6.0.0-beta.0" - react-router-dom "6.0.0-beta.0" - react-use "^17.2.4" - "@roadiehq/backstage-plugin-github-insights@^1.1.23": version "1.2.2" resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-insights/-/backstage-plugin-github-insights-1.2.2.tgz#09d958ed15adbb598afda34187f45deedec2264d" @@ -8500,11 +8470,6 @@ dependencies: "@types/node" "*" -"@types/whatwg-streams@^0.0.7": - version "0.0.7" - resolved "https://registry.npmjs.org/@types/whatwg-streams/-/whatwg-streams-0.0.7.tgz#28bfe73dc850562296367249c4b32a50db81e9d3" - integrity sha512-6sDiSEP6DWcY2ZolsJ2s39ZmsoGQ7KVwBDI3sESQsEm9P2dHTcqnDIHRZFRNtLCzWp7hCFGqYbw5GyfpQnJ01A== - "@types/ws@^6.0.1": version "6.0.4" resolved "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz#7797707c8acce8f76d8c34b370d4645b70421ff1" @@ -11468,7 +11433,7 @@ cloneable-readable@^1.0.0: process-nextick-args "^2.0.0" readable-stream "^2.3.5" -clsx@^1.0.1, clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0: +clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0: version "1.1.1" resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== @@ -13427,7 +13392,7 @@ dom-helpers@^3.4.0: dependencies: "@babel/runtime" "^7.1.2" -dom-helpers@^5.0.0, dom-helpers@^5.0.1: +dom-helpers@^5.0.1: version "5.1.4" resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz#4609680ab5c79a45f2531441f1949b79d6587f4b" integrity sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A== @@ -14989,11 +14954,6 @@ fecha@^4.2.0: resolved "https://registry.npmjs.org/fecha/-/fecha-4.2.0.tgz#3ffb6395453e3f3efff850404f0a59b6747f5f41" integrity sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg== -fetch-readablestream@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/fetch-readablestream/-/fetch-readablestream-0.2.0.tgz#eaa6d1a76b12de2d4731a343393c6ccdcfe2c795" - integrity sha512-qu4mXWf4wus4idBIN/kVH+XSer8IZ9CwHP+Pd7DL7TuKNC1hP7ykon4kkBjwJF3EMX2WsFp4hH7gU7CyL7ucXw== - figgy-pudding@^3.5.1: version "3.5.2" resolved "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" @@ -17060,7 +17020,7 @@ immer@^9.0.1, immer@^9.0.6: resolved "https://registry.npmjs.org/immer/-/immer-9.0.7.tgz#b6156bd7db55db7abc73fd2fdadf4e579a701075" integrity sha512-KGllzpbamZDvOIxnmJ0jI840g7Oikx58lBPWV0hUh7dtAyZpFqqrBZdKka5GlTwMTZ1Tjc/bKKW4VSFAt6BqMA== -immutable@^3.8.2, immutable@^3.x.x: +immutable@^3.x.x: version "3.8.2" resolved "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM= @@ -19991,7 +19951,7 @@ longest-streak@^3.0.0: resolved "https://registry.npmjs.org/longest-streak/-/longest-streak-3.0.0.tgz#f127e2bded83caa6a35ac5f7a2f2b2f94b36f3dc" integrity sha512-XhUjWR5CFaQ03JOP+iSDS9koy8T5jfoImCZ4XprElw3BXsSk4MpVYOLw/6LTDKZhO13PlAXnB5gS4MHQTpkSOw== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -21233,11 +21193,6 @@ mississippi@^3.0.0: stream-each "^1.1.0" through2 "^2.0.0" -mitt@^1.1.2: - version "1.2.0" - resolved "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz#cb24e6569c806e31bd4e3995787fe38a04fdf90d" - integrity sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw== - mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" @@ -24579,21 +24534,6 @@ react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-i resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-lazylog@^4.5.2: - version "4.5.3" - resolved "https://registry.npmjs.org/react-lazylog/-/react-lazylog-4.5.3.tgz#289e24995b5599e75943556ac63f5e2c04d0001e" - integrity sha512-lyov32A/4BqihgXgtNXTHCajXSXkYHPlIEmV8RbYjHIMxCFSnmtdg4kDCI3vATz7dURtiFTvrw5yonHnrS+NNg== - dependencies: - "@mattiasbuelens/web-streams-polyfill" "^0.2.0" - fetch-readablestream "^0.2.0" - immutable "^3.8.2" - mitt "^1.1.2" - prop-types "^15.6.1" - react-string-replace "^0.4.1" - react-virtualized "^9.21.0" - text-encoding-utf-8 "^1.0.1" - whatwg-fetch "^2.0.4" - react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" @@ -24725,13 +24665,6 @@ react-sparklines@^1.7.0: dependencies: prop-types "^15.5.10" -react-string-replace@^0.4.1: - version "0.4.4" - resolved "https://registry.npmjs.org/react-string-replace/-/react-string-replace-0.4.4.tgz#24006fbe0db573d5be583133df38b1a735cb4225" - integrity sha512-FAMkhxmDpCsGTwTZg7p/2v+/GTmxAp73so3fbSvlAcBBX36ujiGRNEaM/1u+jiYQrArhns+7eE92g2pi5E5FUA== - dependencies: - lodash "^4.17.4" - react-syntax-highlighter@^13.5.3: version "13.5.3" resolved "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-13.5.3.tgz#9712850f883a3e19eb858cf93fad7bb357eea9c6" @@ -24830,18 +24763,6 @@ react-virtualized-auto-sizer@^1.0.6: resolved "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz#66c5b1c9278064c5ef1699ed40a29c11518f97ca" integrity sha512-7tQ0BmZqfVF6YYEWcIGuoR3OdYe8I/ZFbNclFlGOC3pMqunkYF/oL30NCjSGl9sMEb17AnzixDz98Kqc3N76HQ== -react-virtualized@^9.21.0: - version "9.21.2" - resolved "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.21.2.tgz#02e6df65c1e020c8dbf574ec4ce971652afca84e" - integrity sha512-oX7I7KYiUM7lVXQzmhtF4Xg/4UA5duSA+/ZcAvdWlTLFCoFYq1SbauJT5gZK9cZS/wdYR6TPGpX/dqzvTqQeBA== - dependencies: - babel-runtime "^6.26.0" - clsx "^1.0.1" - dom-helpers "^5.0.0" - loose-envify "^1.3.0" - prop-types "^15.6.0" - react-lifecycles-compat "^3.0.4" - react-window@^1.8.6: version "1.8.6" resolved "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz#d011950ac643a994118632665aad0c6382e2a112" @@ -27833,11 +27754,6 @@ testcontainers@^7.23.0: ssh-remote-port-forward "^1.0.4" tar-fs "^2.1.1" -text-encoding-utf-8@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" - integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg== - text-extensions@^1.0.0: version "1.9.0" resolved "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26" @@ -29590,11 +29506,6 @@ whatwg-encoding@^1.0.5: dependencies: iconv-lite "0.4.24" -whatwg-fetch@^2.0.4: - version "2.0.4" - resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" - integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== - whatwg-fetch@^3.4.1: version "3.4.1" resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz#e5f871572d6879663fa5674c8f833f15a8425ab3" From 1357ac30f159c5ae6b98bc5635bb1fc3060c3abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 8 Dec 2021 15:23:28 +0100 Subject: [PATCH 112/116] Standardize on classnames instead of both that and clsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/odd-gifts-decide.md | 5 +++++ packages/core-components/package.json | 1 - .../src/components/LogViewer/LogLine.tsx | 4 ++-- .../src/components/LogViewer/RealLogViewer.tsx | 6 +++--- .../core-components/src/layout/Sidebar/Bar.tsx | 4 ++-- .../core-components/src/layout/Sidebar/Items.tsx | 15 +++++++++------ .../src/layout/Sidebar/SidebarSubmenu.tsx | 4 ++-- .../src/layout/Sidebar/SidebarSubmenuItem.tsx | 9 ++++++--- 8 files changed, 29 insertions(+), 19 deletions(-) create mode 100644 .changeset/odd-gifts-decide.md diff --git a/.changeset/odd-gifts-decide.md b/.changeset/odd-gifts-decide.md new file mode 100644 index 0000000000..317191f4c6 --- /dev/null +++ b/.changeset/odd-gifts-decide.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Standardize on `classnames` instead of both that and `clsx`. diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 0d17d30461..12f5474c44 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -41,7 +41,6 @@ "@types/react-text-truncate": "^0.14.0", "ansi-regex": "^5.0.1", "classnames": "^2.2.6", - "clsx": "^1.1.0", "d3-selection": "^3.0.0", "d3-shape": "^3.0.0", "d3-zoom": "^3.0.0", diff --git a/packages/core-components/src/components/LogViewer/LogLine.tsx b/packages/core-components/src/components/LogViewer/LogLine.tsx index c14df4a45a..ed3396360e 100644 --- a/packages/core-components/src/components/LogViewer/LogLine.tsx +++ b/packages/core-components/src/components/LogViewer/LogLine.tsx @@ -17,7 +17,7 @@ import React, { useMemo } from 'react'; import { AnsiChunk, AnsiLine, ChunkModifiers } from './AnsiProcessor'; import startCase from 'lodash/startCase'; -import clsx from 'clsx'; +import classnames from 'classnames'; import { useStyles } from './styles'; export function getModifierClasses( @@ -160,7 +160,7 @@ export function LogLine({ chunks.map(({ text, modifiers, highlight }, index) => ( (
@@ -82,7 +82,7 @@ export function RealLogViewer(props: RealLogViewerProps) { return (
diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx index 775c537695..bada68a028 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.tsx @@ -16,7 +16,7 @@ import { makeStyles } from '@material-ui/core/styles'; import useMediaQuery from '@material-ui/core/useMediaQuery'; -import clsx from 'clsx'; +import classnames from 'classnames'; import React, { useState, useContext, PropsWithChildren, useRef } from 'react'; import { sidebarConfig, SidebarContext } from './config'; import { BackstageTheme } from '@backstage/theme'; @@ -172,7 +172,7 @@ export function Sidebar(props: PropsWithChildren) { }} >
diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index 903053eda4..c8c6db13a2 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -23,7 +23,7 @@ import Typography from '@material-ui/core/Typography'; import { CreateCSSProperties } from '@material-ui/core/styles/withStyles'; import ArrowRightIcon from '@material-ui/icons/ArrowRight'; import SearchIcon from '@material-ui/icons/Search'; -import clsx from 'clsx'; +import classnames from 'classnames'; import React, { Children, forwardRef, @@ -253,12 +253,12 @@ const SidebarItemWithSubmenu = ({ >
); }); @@ -388,7 +391,7 @@ export const SidebarItem = forwardRef((props, ref) => { variant="dot" overlap="circular" invisible={!hasNotifications} - className={clsx({ [classes.closedItemIcon]: !isOpen })} + className={classnames({ [classes.closedItemIcon]: !isOpen })} > @@ -414,7 +417,7 @@ export const SidebarItem = forwardRef((props, ref) => { const childProps = { onClick, - className: clsx( + className: classnames( className, classes.root, isOpen ? classes.open : classes.closed, diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx index df671c7514..1e9afe86ef 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx @@ -15,7 +15,7 @@ */ import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; -import clsx from 'clsx'; +import classnames from 'classnames'; import React, { ReactNode, useContext } from 'react'; import { SidebarItemWithSubmenuContext, @@ -93,7 +93,7 @@ export const SidebarSubmenu = (props: SidebarSubmenuProps) => { const { isHoveredOn } = useContext(SidebarItemWithSubmenuContext); return (
diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx index 2bb8a13592..56192ced77 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenuItem.tsx @@ -24,7 +24,7 @@ import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import Link from '@material-ui/core/Link'; import { IconComponent } from '@backstage/core-plugin-api'; -import clsx from 'clsx'; +import classnames from 'classnames'; import { BackstageTheme } from '@backstage/theme'; import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; import ArrowDropUpIcon from '@material-ui/icons/ArrowDropUp'; @@ -138,7 +138,7 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => {