From 2510b6418592fd02899d2a3a180a22f3f1216e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Tunkl?= Date: Fri, 5 Aug 2022 15:20:07 +0200 Subject: [PATCH 01/41] Adds possibility to not skip default namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomáš Tunkl --- .../components/EntityRefLink/humanize.test.ts | 28 +++++++++++++++++++ .../src/components/EntityRefLink/humanize.ts | 8 ++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts index d41d3c6635..ac873b5140 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts @@ -34,6 +34,23 @@ describe('humanizeEntityRef', () => { expect(title).toEqual('component:software'); }); + it('formats entity in default namespace without skipping default namespace', () => { + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + }, + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + }; + const title = humanizeEntityRef(entity, {skipDefaultNamespace: false}); + expect(title).toEqual('component:default/software'); + }); + it('formats entity in other namespace', () => { const entity = { apiVersion: 'v1', @@ -103,4 +120,15 @@ describe('humanizeEntityRef', () => { }); expect(title).toEqual('test/software'); }); + + it('formats entity name in default namespace without skip of default namespace', () => { + const entityName = { + kind: 'Component', + namespace: 'default', + name: 'software', + }; + + const title = humanizeEntityRef(entityName, {skipDefaultNamespace: false}); + expect(title).toEqual('component:default/software'); + }); }); diff --git a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts index 2f72a7ee3b..bbf826ecb2 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts @@ -23,9 +23,13 @@ import { /** @public */ export function humanizeEntityRef( entityRef: Entity | CompoundEntityRef, - opts?: { defaultKind?: string }, + opts?: { + defaultKind?: string + skipDefaultNamespace?: boolean + }, ) { const defaultKind = opts?.defaultKind; + const skipDefaultNamespace = opts?.skipDefaultNamespace ?? true; let kind; let namespace; let name; @@ -40,7 +44,7 @@ export function humanizeEntityRef( name = entityRef.name; } - if (namespace === DEFAULT_NAMESPACE) { + if (skipDefaultNamespace === true && namespace === DEFAULT_NAMESPACE) { namespace = undefined; } From f6033d1121a053901f1df02c852db25171ffb43c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Tunkl?= Date: Tue, 23 Aug 2022 12:06:57 +0200 Subject: [PATCH 02/41] New changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomáš Tunkl --- .changeset/eight-shrimps-call.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/eight-shrimps-call.md diff --git a/.changeset/eight-shrimps-call.md b/.changeset/eight-shrimps-call.md new file mode 100644 index 0000000000..2e19681f7a --- /dev/null +++ b/.changeset/eight-shrimps-call.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +humanizeEntityRef function can now be forced to include default namespace From 727e66aeb60258c04399defdd56585b159f1e3aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Tunkl?= Date: Tue, 23 Aug 2022 12:50:27 +0200 Subject: [PATCH 03/41] Applied prettier on files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomáš Tunkl --- .../src/components/EntityRefLink/humanize.test.ts | 6 ++++-- .../catalog-react/src/components/EntityRefLink/humanize.ts | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts index ac873b5140..9f08290095 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts @@ -47,7 +47,7 @@ describe('humanizeEntityRef', () => { lifecycle: 'production', }, }; - const title = humanizeEntityRef(entity, {skipDefaultNamespace: false}); + const title = humanizeEntityRef(entity, { skipDefaultNamespace: false }); expect(title).toEqual('component:default/software'); }); @@ -128,7 +128,9 @@ describe('humanizeEntityRef', () => { name: 'software', }; - const title = humanizeEntityRef(entityName, {skipDefaultNamespace: false}); + const title = humanizeEntityRef(entityName, { + skipDefaultNamespace: false, + }); expect(title).toEqual('component:default/software'); }); }); diff --git a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts index bbf826ecb2..bd97ce2548 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts @@ -24,8 +24,8 @@ import { export function humanizeEntityRef( entityRef: Entity | CompoundEntityRef, opts?: { - defaultKind?: string - skipDefaultNamespace?: boolean + defaultKind?: string; + skipDefaultNamespace?: boolean; }, ) { const defaultKind = opts?.defaultKind; From 6cd4e06d91d9f74f0c49256b64f2e819d1b7efb5 Mon Sep 17 00:00:00 2001 From: Luka Siric Date: Tue, 6 Sep 2022 10:55:03 +0200 Subject: [PATCH 04/41] passing select attr to the getUsers Signed-off-by: Luka Siric --- .../catalog-backend-module-msgraph/src/microsoftGraph/read.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index ecf74b34d9..0542e4fd8e 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -89,6 +89,7 @@ export async function readMicrosoftGraphUsers( queryMode?: 'basic' | 'advanced'; userFilter?: string; userExpand?: string; + userSelect?: string[]; transformer?: UserTransformer; logger: Logger; }, @@ -105,6 +106,7 @@ export async function readMicrosoftGraphUsers( { filter: options.userFilter, expand: options.userExpand, + select: options.userSelect || [], }, options.queryMode, )) { From b38f4cc987cbdad737b38e6a38b53a320329dc58 Mon Sep 17 00:00:00 2001 From: Luka Siric Date: Tue, 6 Sep 2022 11:04:56 +0200 Subject: [PATCH 05/41] updated readme Signed-off-by: Luka Siric --- plugins/catalog-backend-module-msgraph/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/catalog-backend-module-msgraph/README.md b/plugins/catalog-backend-module-msgraph/README.md index fe2e568b5a..9bf979f6a6 100644 --- a/plugins/catalog-backend-module-msgraph/README.md +++ b/plugins/catalog-backend-module-msgraph/README.md @@ -54,6 +54,8 @@ catalog: # and for the syntax https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter # This and userGroupMemberFilter are mutually exclusive, only one can be specified filter: accountEnabled eq true and userType eq 'member' + # See https://docs.microsoft.com/en-us/graph/api/resources/schemaextension?view=graph-rest-1.0 + select: ['id', 'displayName', 'description'] # Optional configuration block userGroupMember: # Optional filter for users, use group membership to get users. From d80aab31aed670120da2d7199639e857b1bceae5 Mon Sep 17 00:00:00 2001 From: Luka Siric Date: Tue, 6 Sep 2022 11:06:09 +0200 Subject: [PATCH 06/41] added changeset Signed-off-by: Luka Siric --- .changeset/cuddly-clocks-dance.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cuddly-clocks-dance.md diff --git a/.changeset/cuddly-clocks-dance.md b/.changeset/cuddly-clocks-dance.md new file mode 100644 index 0000000000..1a9c05bf20 --- /dev/null +++ b/.changeset/cuddly-clocks-dance.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': minor +--- + +Added $select attribute to user query From ced29ddfeea99060825bd40af144dddc08f29f8e Mon Sep 17 00:00:00 2001 From: Luka Siric Date: Tue, 6 Sep 2022 13:48:39 +0200 Subject: [PATCH 07/41] passing userSelect to the readMicrosoftGraphOrg Signed-off-by: Luka Siric --- .../src/microsoftGraph/read.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index 0542e4fd8e..8e1d9a3776 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -106,7 +106,7 @@ export async function readMicrosoftGraphUsers( { filter: options.userFilter, expand: options.userExpand, - select: options.userSelect || [], + select: options.userSelect, }, options.queryMode, )) { @@ -147,6 +147,7 @@ export async function readMicrosoftGraphUsersInGroups( options: { queryMode?: 'basic' | 'advanced'; userExpand?: string; + userSelect?: string[]; userGroupMemberSearch?: string; userGroupMemberFilter?: string; groupExpand?: string; @@ -536,6 +537,7 @@ export async function readMicrosoftGraphOrg( options: { userExpand?: string; userFilter?: string; + userSelect?: string[]; userGroupMemberSearch?: string; userGroupMemberFilter?: string; groupExpand?: string; @@ -567,6 +569,7 @@ export async function readMicrosoftGraphOrg( const { users: usersWithFilter } = await readMicrosoftGraphUsers(client, { queryMode: options.queryMode, userFilter: options.userFilter, + userSelect: options.userSelect, userExpand: options.userExpand, transformer: options.userTransformer, logger: options.logger, From e821a3db3d132696a8ea27f9bd6eb3bb33f4db6c Mon Sep 17 00:00:00 2001 From: Luka Siric Date: Tue, 6 Sep 2022 13:49:02 +0200 Subject: [PATCH 08/41] added unit test for userSelect option Signed-off-by: Luka Siric --- .../src/microsoftGraph/read.test.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts index 3f43807809..7fe98cf1cf 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts @@ -1002,6 +1002,12 @@ describe('read microsoft graph', () => { }; } + async function* getExampleUsersEmail() { + yield { + mail: 'user.name@example.com', + }; + } + async function getExampleUserProfile(userId: string) { return { id: userId, @@ -1109,6 +1115,37 @@ describe('read microsoft graph', () => { ); }); + it('should read users with userSelect', async () => { + client.getOrganization.mockResolvedValue({ + id: 'tenantid', + displayName: 'Organization Name', + }); + + client.getUsers.mockImplementation(getExampleUsersEmail); + client.getUserPhotoWithSizeLimit.mockResolvedValue( + 'data:image/jpeg;base64,...', + ); + + client.getGroups.mockImplementation(getExampleGroups); + client.getGroupMembers.mockImplementation(getExampleGroupMembers); + client.getGroupPhotoWithSizeLimit.mockResolvedValue( + 'data:image/jpeg;base64,...', + ); + + await readMicrosoftGraphOrg(client, 'tenantid', { + logger: getVoidLogger(), + userSelect: ['mail'], + }); + + expect(client.getUsers).toHaveBeenCalledTimes(1); + expect(client.getUsers).toHaveBeenCalledWith( + { + select: ['mail'], + }, + undefined, + ); + }); + it('should read users using userExpand and userGroupMemberFilter', async () => { client.getOrganization.mockResolvedValue({ id: 'tenantid', From 86b22c7d5dac2126fc18a1878b6edeb4642d89a0 Mon Sep 17 00:00:00 2001 From: Luka Siric Date: Tue, 6 Sep 2022 13:50:17 +0200 Subject: [PATCH 09/41] removed unused mock getGroupPhotoWithSizeLimit Signed-off-by: Luka Siric --- .../src/microsoftGraph/read.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts index 7fe98cf1cf..a537526a9e 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts @@ -1128,9 +1128,6 @@ describe('read microsoft graph', () => { client.getGroups.mockImplementation(getExampleGroups); client.getGroupMembers.mockImplementation(getExampleGroupMembers); - client.getGroupPhotoWithSizeLimit.mockResolvedValue( - 'data:image/jpeg;base64,...', - ); await readMicrosoftGraphOrg(client, 'tenantid', { logger: getVoidLogger(), From 8948d577426a72bdca102f7e0ebfcd27d822f4ae Mon Sep 17 00:00:00 2001 From: Luka Siric Date: Tue, 6 Sep 2022 14:19:19 +0200 Subject: [PATCH 10/41] generated new api report for catalog-backend-module-msgraph Signed-off-by: Luka Siric --- plugins/catalog-backend-module-msgraph/api-report.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index 8f773e1b90..2a244c60d8 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -232,6 +232,7 @@ export function readMicrosoftGraphOrg( options: { userExpand?: string; userFilter?: string; + userSelect?: string[]; userGroupMemberSearch?: string; userGroupMemberFilter?: string; groupExpand?: string; From 852ebeeb24fcc62aeed8b5e6bd67f371347becf3 Mon Sep 17 00:00:00 2001 From: Luka Siric Date: Tue, 6 Sep 2022 14:33:31 +0200 Subject: [PATCH 11/41] added userSelect attribute to config Signed-off-by: Luka Siric --- plugins/catalog-backend-module-msgraph/config.d.ts | 6 ++++++ .../src/microsoftGraph/config.ts | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/plugins/catalog-backend-module-msgraph/config.d.ts b/plugins/catalog-backend-module-msgraph/config.d.ts index 8748df5974..df41b581f9 100644 --- a/plugins/catalog-backend-module-msgraph/config.d.ts +++ b/plugins/catalog-backend-module-msgraph/config.d.ts @@ -74,6 +74,12 @@ export interface Config { * E.g. "securityEnabled eq false and mailEnabled eq true" */ groupFilter?: string; + /** + * The fields to be fetched on query. + * + * E.g. ["id", "displayName", "description"] + */ + userSelect?: string[]; /** * The search criteria to apply to extract users by groups memberships. * diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts index f7675acf42..2429b67c89 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts @@ -62,6 +62,12 @@ export type MicrosoftGraphProviderConfig = { * E.g. "accountEnabled eq true and userType eq 'member'" */ userFilter?: string; + /** + * The fields to be fetched on query. + * + * E.g. ["id", "displayName", "description"] + */ + userSelect?: string[]; /** * The "expand" argument to apply to users. * @@ -144,6 +150,7 @@ export function readMicrosoftGraphConfig( const userExpand = providerConfig.getOptionalString('userExpand'); const userFilter = providerConfig.getOptionalString('userFilter'); + const userSelect = providerConfig.getOptionalStringArray('userSelect'); const userGroupMemberFilter = providerConfig.getOptionalString( 'userGroupMemberFilter', ); @@ -196,6 +203,7 @@ export function readMicrosoftGraphConfig( clientSecret, userExpand, userFilter, + userSelect, userGroupMemberFilter, userGroupMemberSearch, groupExpand, From b6af632b83bed51b6d60bd79fe64e9d4c9103557 Mon Sep 17 00:00:00 2001 From: Luka Siric Date: Tue, 6 Sep 2022 15:30:48 +0200 Subject: [PATCH 12/41] generated new api report for catalog-backend-module-msgraph Signed-off-by: Luka Siric --- plugins/catalog-backend-module-msgraph/api-report.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index 2a244c60d8..8b77ddf7c3 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -193,6 +193,7 @@ export type MicrosoftGraphProviderConfig = { clientId?: string; clientSecret?: string; userFilter?: string; + userSelect?: string[]; userExpand?: string; userGroupMemberFilter?: string; userGroupMemberSearch?: string; From df1e0d44bfa565ba54e9c01739d06931cdb0fd7a Mon Sep 17 00:00:00 2001 From: Luka Siric Date: Thu, 8 Sep 2022 09:18:26 +0200 Subject: [PATCH 13/41] Update .changeset/cuddly-clocks-dance.md changed plugin-catalog-backend-module-msgraph change to "patch" Co-authored-by: Patrik Oldsberg Signed-off-by: Luka Siric --- .changeset/cuddly-clocks-dance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/cuddly-clocks-dance.md b/.changeset/cuddly-clocks-dance.md index 1a9c05bf20..9d33535a62 100644 --- a/.changeset/cuddly-clocks-dance.md +++ b/.changeset/cuddly-clocks-dance.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-catalog-backend-module-msgraph': minor +'@backstage/plugin-catalog-backend-module-msgraph': patch --- Added $select attribute to user query From ec12bbe64b5060a87f521bf2f81baf5174ed4aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Tunkl?= Date: Thu, 8 Sep 2022 13:04:03 +0200 Subject: [PATCH 14/41] Implemented new tests for humanize update. Fix of logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomáš Tunkl --- .../components/EntityRefLink/humanize.test.ts | 80 ++++++++++++++++++- .../src/components/EntityRefLink/humanize.ts | 18 ++++- 2 files changed, 92 insertions(+), 6 deletions(-) diff --git a/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts index 9f08290095..80ce2100f6 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts @@ -47,7 +47,7 @@ describe('humanizeEntityRef', () => { lifecycle: 'production', }, }; - const title = humanizeEntityRef(entity, { skipDefaultNamespace: false }); + const title = humanizeEntityRef(entity, { defaultNamespace: false }); expect(title).toEqual('component:default/software'); }); @@ -69,6 +69,24 @@ describe('humanizeEntityRef', () => { expect(title).toEqual('component:test/software'); }); + it('formats entity in other namespace and hides this namespace', () => { + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + namespace: 'test', + }, + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + }; + const title = humanizeEntityRef(entity, { defaultNamespace: 'test' }); + expect(title).toEqual('component:software'); + }); + it('formats entity and hides default kind', () => { const entity = { apiVersion: 'v1', @@ -87,6 +105,27 @@ describe('humanizeEntityRef', () => { expect(title).toEqual('test/software'); }); + it('formats entity and hides default kind and hiding namespace', () => { + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + namespace: 'test', + }, + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + }; + const title = humanizeEntityRef(entity, { + defaultKind: 'Component', + defaultNamespace: 'test', + }); + expect(title).toEqual('software'); + }); + it('formats entity name in default namespace', () => { const entityName = { kind: 'Component', @@ -97,6 +136,16 @@ describe('humanizeEntityRef', () => { expect(title).toEqual('component:software'); }); + it('formats entity name in default namespace and does not skip default namespace', () => { + const entityName = { + kind: 'Component', + namespace: 'default', + name: 'software', + }; + const title = humanizeEntityRef(entityName, { defaultNamespace: false }); + expect(title).toEqual('component:default/software'); + }); + it('formats entity name in other namespace', () => { const entityName = { kind: 'Component', @@ -108,6 +157,19 @@ describe('humanizeEntityRef', () => { expect(title).toEqual('component:test/software'); }); + it('formats entity name in other namespace with skipping this namespace', () => { + const entityName = { + kind: 'Component', + namespace: 'test', + name: 'software', + }; + + const title = humanizeEntityRef(entityName, { + defaultNamespace: 'test', + }); + expect(title).toEqual('component:software'); + }); + it('renders link for entity name and hides default kind', () => { const entityName = { kind: 'Component', @@ -121,6 +183,20 @@ describe('humanizeEntityRef', () => { expect(title).toEqual('test/software'); }); + it('renders link for entity name and hides default kind with skipping namespace', () => { + const entityName = { + kind: 'Component', + namespace: 'test', + name: 'software', + }; + + const title = humanizeEntityRef(entityName, { + defaultKind: 'component', + defaultNamespace: 'test', + }); + expect(title).toEqual('software'); + }); + it('formats entity name in default namespace without skip of default namespace', () => { const entityName = { kind: 'Component', @@ -129,7 +205,7 @@ describe('humanizeEntityRef', () => { }; const title = humanizeEntityRef(entityName, { - skipDefaultNamespace: false, + defaultNamespace: false, }); expect(title).toEqual('component:default/software'); }); diff --git a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts index bd97ce2548..536b9a3a6e 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts @@ -20,16 +20,19 @@ import { DEFAULT_NAMESPACE, } from '@backstage/catalog-model'; -/** @public */ +/** + * @property defaultNamespace - if set to false then namespace is never ommited, + * if set to string which matches namespace of entity then omited + * + * @public */ export function humanizeEntityRef( entityRef: Entity | CompoundEntityRef, opts?: { defaultKind?: string; - skipDefaultNamespace?: boolean; + defaultNamespace?: string | boolean; }, ) { const defaultKind = opts?.defaultKind; - const skipDefaultNamespace = opts?.skipDefaultNamespace ?? true; let kind; let namespace; let name; @@ -44,7 +47,14 @@ export function humanizeEntityRef( name = entityRef.name; } - if (skipDefaultNamespace === true && namespace === DEFAULT_NAMESPACE) { + if (namespace === undefined || namespace === '') { + namespace = DEFAULT_NAMESPACE; + } + if (opts?.defaultNamespace !== undefined) { + if (opts?.defaultNamespace === namespace) { + namespace = undefined; + } + } else if (namespace === DEFAULT_NAMESPACE) { namespace = undefined; } From 5986fa4f2d77abccaed491b65cbcab16a581f6fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Tunkl?= Date: Thu, 8 Sep 2022 14:11:37 +0200 Subject: [PATCH 15/41] Fix of typo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomáš Tunkl --- .../catalog-react/src/components/EntityRefLink/humanize.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts index 536b9a3a6e..4a53ace1e3 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts @@ -21,8 +21,8 @@ import { } from '@backstage/catalog-model'; /** - * @property defaultNamespace - if set to false then namespace is never ommited, - * if set to string which matches namespace of entity then omited + * @property defaultNamespace - if set to false then namespace is never omitted, + * if set to string which matches namespace of entity then omitted * * @public */ export function humanizeEntityRef( From 6522e459aae698f2e1da1c04ac3cc25d387e5ca7 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Thu, 8 Sep 2022 14:45:09 -0400 Subject: [PATCH 16/41] feat(EntityTagPicker): support displaying and ordering by counts Signed-off-by: Phil Kuang --- .changeset/lucky-ads-worry.md | 5 +++ plugins/scaffolder/api-report.md | 4 ++ .../EntityTagsPicker/EntityTagsPicker.tsx | 40 +++++++++++++------ 3 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 .changeset/lucky-ads-worry.md diff --git a/.changeset/lucky-ads-worry.md b/.changeset/lucky-ads-worry.md new file mode 100644 index 0000000000..a7f749da17 --- /dev/null +++ b/.changeset/lucky-ads-worry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Support displaying and ordering by counts in `EntityTagPicker` field. Add the `showCounts` option to enable this. Also support configuring `helperText`. diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index d611309df3..9541098934 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -76,8 +76,12 @@ export const EntityTagsPickerFieldExtension: FieldExtensionComponent< // @public export interface EntityTagsPickerUiOptions { + // (undocumented) + helperText?: string; // (undocumented) kinds?: string[]; + // (undocumented) + showCounts?: boolean; } // @public diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx b/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx index 5cf98cb8aa..c0e3aab436 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx @@ -16,8 +16,8 @@ import React, { useState } from 'react'; import useAsync from 'react-use/lib/useAsync'; import useEffectOnce from 'react-use/lib/useEffectOnce'; -import { GetEntitiesRequest } from '@backstage/catalog-client'; -import { Entity, makeValidator } from '@backstage/catalog-model'; +import { GetEntityFacetsRequest } from '@backstage/catalog-client'; +import { makeValidator } from '@backstage/catalog-model'; import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { FormControl, TextField } from '@material-ui/core'; @@ -32,6 +32,8 @@ import { FieldExtensionComponentProps } from '../../../extensions'; */ export interface EntityTagsPickerUiOptions { kinds?: string[]; + showCounts?: boolean; + helperText?: string; } /** @@ -45,26 +47,34 @@ export const EntityTagsPicker = ( ) => { const { formData, onChange, uiSchema } = props; const catalogApi = useApi(catalogApiRef); + const [tagOptions, setTagOptions] = useState([]); const [inputValue, setInputValue] = useState(''); const [inputError, setInputError] = useState(false); const tagValidator = makeValidator().isValidTag; const kinds = uiSchema['ui:options']?.kinds; + const showCounts = uiSchema['ui:options']?.showCounts; + const helperText = uiSchema['ui:options']?.helperText; const { loading, value: existingTags } = useAsync(async () => { - const tagsRequest: GetEntitiesRequest = { fields: ['metadata.tags'] }; + const facet = 'metadata.tags'; + const tagsRequest: GetEntityFacetsRequest = { facets: [facet] }; if (kinds) { tagsRequest.filter = { kind: kinds }; } - const entities = await catalogApi.getEntities(tagsRequest); + const { facets } = await catalogApi.getEntityFacets(tagsRequest); - return [ - ...new Set( - entities.items - .flatMap((e: Entity) => e.metadata?.tags) - .filter(Boolean) as string[], + const tagFacets = Object.fromEntries( + facets[facet].map(({ value, count }) => [value, count]), + ); + + setTagOptions( + Object.keys(tagFacets).sort((a, b) => + showCounts ? tagFacets[b] - tagFacets[a] : a.localeCompare(b), ), - ].sort(); + ); + + return tagFacets; }); const setTags = (_: React.ChangeEvent<{}>, values: string[] | null) => { @@ -102,15 +112,21 @@ export const EntityTagsPicker = ( value={formData || []} inputValue={inputValue} loading={loading} - options={existingTags || []} + options={tagOptions} ChipProps={{ size: 'small' }} + renderOption={option => + showCounts ? `${option} (${existingTags?.[option]})` : option + } renderInput={params => ( setInputValue(e.target.value)} error={inputError} - helperText="Add any relevant tags, hit 'Enter' to add new tags. Valid format: [a-z0-9+#] separated by [-], at most 63 characters" + helperText={ + helperText ?? + "Add any relevant tags, hit 'Enter' to add new tags. Valid format: [a-z0-9+#] separated by [-], at most 63 characters" + } /> )} /> From 978d691ed2b5adfafdf9e3dad659f4f4c2d07349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Tunkl?= Date: Fri, 9 Sep 2022 09:53:09 +0200 Subject: [PATCH 17/41] Update of api-report.md after change of humanize public api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomáš Tunkl --- plugins/catalog-react/api-report.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 5232c49150..a5567aa9c8 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -436,11 +436,14 @@ export function getEntitySourceLocation( scmIntegrationsApi: ScmIntegrationRegistry, ): EntitySourceLocation | undefined; -// @public (undocumented) +// Warning: (tsdoc-undefined-tag) The TSDoc tag "@property" is not defined in this configuration +// +// @public export function humanizeEntityRef( entityRef: Entity | CompoundEntityRef, opts?: { defaultKind?: string; + defaultNamespace?: string | boolean; }, ): string; From 007204a8befe871f5de7ce62bd5ffcce820bd68f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Tunkl?= Date: Fri, 9 Sep 2022 11:03:30 +0200 Subject: [PATCH 18/41] Update of api-report.md and humanize documentation. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomáš Tunkl --- plugins/catalog-react/api-report.md | 4 +--- .../catalog-react/src/components/EntityRefLink/humanize.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index a5567aa9c8..b2b098ec68 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -436,9 +436,7 @@ export function getEntitySourceLocation( scmIntegrationsApi: ScmIntegrationRegistry, ): EntitySourceLocation | undefined; -// Warning: (tsdoc-undefined-tag) The TSDoc tag "@property" is not defined in this configuration -// -// @public +// @public (undocumented) export function humanizeEntityRef( entityRef: Entity | CompoundEntityRef, opts?: { diff --git a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts index 4a53ace1e3..358b6a5851 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts @@ -21,7 +21,7 @@ import { } from '@backstage/catalog-model'; /** - * @property defaultNamespace - if set to false then namespace is never omitted, + * @param defaultNamespace - if set to false then namespace is never omitted, * if set to string which matches namespace of entity then omitted * * @public */ From bf329f41026f1fa53d87cf5dba8874d4305e80b3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Sep 2022 14:45:55 +0000 Subject: [PATCH 19/41] fix(deps): update dependency @swc/core to v1.2.249 Signed-off-by: Renovate Bot --- storybook/yarn.lock | 110 ++++++++++++++++++++++---------------------- yarn.lock | 110 ++++++++++++++++++++++---------------------- 2 files changed, 110 insertions(+), 110 deletions(-) diff --git a/storybook/yarn.lock b/storybook/yarn.lock index 3592f58d4b..0bcfa0c48a 100644 --- a/storybook/yarn.lock +++ b/storybook/yarn.lock @@ -2866,126 +2866,126 @@ __metadata: languageName: node linkType: hard -"@swc/core-android-arm-eabi@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-android-arm-eabi@npm:1.2.247" +"@swc/core-android-arm-eabi@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-android-arm-eabi@npm:1.2.249" dependencies: "@swc/wasm": 1.2.122 conditions: os=android & cpu=arm languageName: node linkType: hard -"@swc/core-android-arm64@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-android-arm64@npm:1.2.247" +"@swc/core-android-arm64@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-android-arm64@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-darwin-arm64@npm:1.2.247" +"@swc/core-darwin-arm64@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-darwin-arm64@npm:1.2.249" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-darwin-x64@npm:1.2.247" +"@swc/core-darwin-x64@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-darwin-x64@npm:1.2.249" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-freebsd-x64@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-freebsd-x64@npm:1.2.247" +"@swc/core-freebsd-x64@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-freebsd-x64@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.2.247" +"@swc/core-linux-arm-gnueabihf@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-arm64-gnu@npm:1.2.247" +"@swc/core-linux-arm64-gnu@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-arm64-gnu@npm:1.2.249" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-arm64-musl@npm:1.2.247" +"@swc/core-linux-arm64-musl@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-arm64-musl@npm:1.2.249" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-x64-gnu@npm:1.2.247" +"@swc/core-linux-x64-gnu@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-x64-gnu@npm:1.2.249" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-x64-musl@npm:1.2.247" +"@swc/core-linux-x64-musl@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-x64-musl@npm:1.2.249" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-win32-arm64-msvc@npm:1.2.247" +"@swc/core-win32-arm64-msvc@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-win32-arm64-msvc@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-win32-ia32-msvc@npm:1.2.247" +"@swc/core-win32-ia32-msvc@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-win32-ia32-msvc@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-win32-x64-msvc@npm:1.2.247" +"@swc/core-win32-x64-msvc@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-win32-x64-msvc@npm:1.2.249" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@swc/core@npm:^1.2.239": - version: 1.2.247 - resolution: "@swc/core@npm:1.2.247" + version: 1.2.249 + resolution: "@swc/core@npm:1.2.249" dependencies: - "@swc/core-android-arm-eabi": 1.2.247 - "@swc/core-android-arm64": 1.2.247 - "@swc/core-darwin-arm64": 1.2.247 - "@swc/core-darwin-x64": 1.2.247 - "@swc/core-freebsd-x64": 1.2.247 - "@swc/core-linux-arm-gnueabihf": 1.2.247 - "@swc/core-linux-arm64-gnu": 1.2.247 - "@swc/core-linux-arm64-musl": 1.2.247 - "@swc/core-linux-x64-gnu": 1.2.247 - "@swc/core-linux-x64-musl": 1.2.247 - "@swc/core-win32-arm64-msvc": 1.2.247 - "@swc/core-win32-ia32-msvc": 1.2.247 - "@swc/core-win32-x64-msvc": 1.2.247 + "@swc/core-android-arm-eabi": 1.2.249 + "@swc/core-android-arm64": 1.2.249 + "@swc/core-darwin-arm64": 1.2.249 + "@swc/core-darwin-x64": 1.2.249 + "@swc/core-freebsd-x64": 1.2.249 + "@swc/core-linux-arm-gnueabihf": 1.2.249 + "@swc/core-linux-arm64-gnu": 1.2.249 + "@swc/core-linux-arm64-musl": 1.2.249 + "@swc/core-linux-x64-gnu": 1.2.249 + "@swc/core-linux-x64-musl": 1.2.249 + "@swc/core-win32-arm64-msvc": 1.2.249 + "@swc/core-win32-ia32-msvc": 1.2.249 + "@swc/core-win32-x64-msvc": 1.2.249 dependenciesMeta: "@swc/core-android-arm-eabi": optional: true @@ -3015,7 +3015,7 @@ __metadata: optional: true bin: swcx: run_swcx.js - checksum: 8ad850c5637405473cb7680865c0b1bdd9e955c0f32e86e6564d43c6a3fb0941b69d7ca152b661694e614530c69fe9824f32d10b606564187730d1d146f62805 + checksum: c47f17fefccd94fb7be3787e832f3e3fd62c07e22f8bf566435bd1de032efd9fdefb4a64e0eefd684a9a24902509a235afeacb5ed93e03512fa09064bf1e1268 languageName: node linkType: hard diff --git a/yarn.lock b/yarn.lock index f4c0b81d8c..350e067b4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13048,126 +13048,126 @@ __metadata: languageName: node linkType: hard -"@swc/core-android-arm-eabi@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-android-arm-eabi@npm:1.2.247" +"@swc/core-android-arm-eabi@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-android-arm-eabi@npm:1.2.249" dependencies: "@swc/wasm": 1.2.122 conditions: os=android & cpu=arm languageName: node linkType: hard -"@swc/core-android-arm64@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-android-arm64@npm:1.2.247" +"@swc/core-android-arm64@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-android-arm64@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-darwin-arm64@npm:1.2.247" +"@swc/core-darwin-arm64@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-darwin-arm64@npm:1.2.249" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-darwin-x64@npm:1.2.247" +"@swc/core-darwin-x64@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-darwin-x64@npm:1.2.249" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-freebsd-x64@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-freebsd-x64@npm:1.2.247" +"@swc/core-freebsd-x64@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-freebsd-x64@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.2.247" +"@swc/core-linux-arm-gnueabihf@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-arm64-gnu@npm:1.2.247" +"@swc/core-linux-arm64-gnu@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-arm64-gnu@npm:1.2.249" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-arm64-musl@npm:1.2.247" +"@swc/core-linux-arm64-musl@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-arm64-musl@npm:1.2.249" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-x64-gnu@npm:1.2.247" +"@swc/core-linux-x64-gnu@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-x64-gnu@npm:1.2.249" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-linux-x64-musl@npm:1.2.247" +"@swc/core-linux-x64-musl@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-linux-x64-musl@npm:1.2.249" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-win32-arm64-msvc@npm:1.2.247" +"@swc/core-win32-arm64-msvc@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-win32-arm64-msvc@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-win32-ia32-msvc@npm:1.2.247" +"@swc/core-win32-ia32-msvc@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-win32-ia32-msvc@npm:1.2.249" dependencies: "@swc/wasm": 1.2.130 conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.2.247": - version: 1.2.247 - resolution: "@swc/core-win32-x64-msvc@npm:1.2.247" +"@swc/core-win32-x64-msvc@npm:1.2.249": + version: 1.2.249 + resolution: "@swc/core-win32-x64-msvc@npm:1.2.249" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@swc/core@npm:^1.2.239": - version: 1.2.247 - resolution: "@swc/core@npm:1.2.247" + version: 1.2.249 + resolution: "@swc/core@npm:1.2.249" dependencies: - "@swc/core-android-arm-eabi": 1.2.247 - "@swc/core-android-arm64": 1.2.247 - "@swc/core-darwin-arm64": 1.2.247 - "@swc/core-darwin-x64": 1.2.247 - "@swc/core-freebsd-x64": 1.2.247 - "@swc/core-linux-arm-gnueabihf": 1.2.247 - "@swc/core-linux-arm64-gnu": 1.2.247 - "@swc/core-linux-arm64-musl": 1.2.247 - "@swc/core-linux-x64-gnu": 1.2.247 - "@swc/core-linux-x64-musl": 1.2.247 - "@swc/core-win32-arm64-msvc": 1.2.247 - "@swc/core-win32-ia32-msvc": 1.2.247 - "@swc/core-win32-x64-msvc": 1.2.247 + "@swc/core-android-arm-eabi": 1.2.249 + "@swc/core-android-arm64": 1.2.249 + "@swc/core-darwin-arm64": 1.2.249 + "@swc/core-darwin-x64": 1.2.249 + "@swc/core-freebsd-x64": 1.2.249 + "@swc/core-linux-arm-gnueabihf": 1.2.249 + "@swc/core-linux-arm64-gnu": 1.2.249 + "@swc/core-linux-arm64-musl": 1.2.249 + "@swc/core-linux-x64-gnu": 1.2.249 + "@swc/core-linux-x64-musl": 1.2.249 + "@swc/core-win32-arm64-msvc": 1.2.249 + "@swc/core-win32-ia32-msvc": 1.2.249 + "@swc/core-win32-x64-msvc": 1.2.249 dependenciesMeta: "@swc/core-android-arm-eabi": optional: true @@ -13197,7 +13197,7 @@ __metadata: optional: true bin: swcx: run_swcx.js - checksum: 8ad850c5637405473cb7680865c0b1bdd9e955c0f32e86e6564d43c6a3fb0941b69d7ca152b661694e614530c69fe9824f32d10b606564187730d1d146f62805 + checksum: c47f17fefccd94fb7be3787e832f3e3fd62c07e22f8bf566435bd1de032efd9fdefb4a64e0eefd684a9a24902509a235afeacb5ed93e03512fa09064bf1e1268 languageName: node linkType: hard From 229a1fa92716a384f67850cfe887bbfa91668f01 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 8 Sep 2022 15:46:16 +0200 Subject: [PATCH 20/41] backend-{plugin,app}-api: introduce service scopes and update registry to support 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 Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- .../src/wiring/ServiceRegistry.test.ts | 155 +++++++-------- .../src/wiring/ServiceRegistry.ts | 185 ++++++++++++------ packages/backend-app-api/src/wiring/types.ts | 3 +- .../src/services/system/types.ts | 135 ++++++++----- 4 files changed, 284 insertions(+), 194 deletions(-) diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts index a00f6c0b4b..ab709dcb77 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts @@ -21,28 +21,28 @@ import { } from '@backstage/backend-plugin-api'; import { ServiceRegistry } from './ServiceRegistry'; -const ref1 = createServiceRef<{ x: number; pluginId: string }>({ +const ref1 = createServiceRef<{ x: number }>({ id: '1', }); const sf1 = createServiceFactory({ service: ref1, deps: {}, factory: async () => { - return async pluginId => { - return { x: 1, pluginId }; + return async () => { + return { x: 1 }; }; }, }); -const ref2 = createServiceRef<{ x: number; pluginId: string }>({ +const ref2 = createServiceRef<{ x: number }>({ id: '2', }); const sf2 = createServiceFactory({ service: ref2, deps: {}, factory: async () => { - return async pluginId => { - return { x: 2, pluginId }; + return async () => { + return { x: 2 }; }; }, }); @@ -50,126 +50,115 @@ const sf2b = createServiceFactory({ service: ref2, deps: {}, factory: async () => { - return async pluginId => { - return { x: 22, pluginId }; + return async () => { + return { x: 22 }; }; }, }); -const refDefault1 = createServiceRef<{ x: number; pluginId: string }>({ +const refDefault1 = createServiceRef<{ x: number }>({ id: '1', defaultFactory: async service => createServiceFactory({ service, deps: {}, - factory: async () => async pluginId => ({ x: 10, pluginId }), + factory: async () => async () => ({ x: 10 }), }), }); -const refDefault2a = createServiceRef<{ x: number; pluginId: string }>({ +const refDefault2a = createServiceRef<{ x: number }>({ id: '2a', defaultFactory: async service => createServiceFactory({ service, deps: {}, - factory: async () => async pluginId => ({ x: 20, pluginId }), + factory: async () => async () => ({ x: 20 }), }), }); -const refDefault2b = createServiceRef<{ x: number; pluginId: string }>({ +const refDefault2b = createServiceRef<{ x: number }>({ id: '2b', defaultFactory: async service => createServiceFactory({ service, deps: {}, - factory: async () => async pluginId => ({ x: 220, pluginId }), + factory: async () => async () => ({ x: 220 }), }), }); describe('ServiceRegistry', () => { it('should return undefined if there is no factory defined', async () => { const registry = new ServiceRegistry([]); - expect(registry.get(ref1)).toBe(undefined); + expect(registry.get(ref1, 'catalog')).toBe(undefined); }); - it('should return a factory for a registered ref', async () => { + it('should return an implementation for a registered ref', async () => { const registry = new ServiceRegistry([sf1]); - const factory = registry.get(ref1)!; - expect(factory).toEqual(expect.any(Function)); - await expect(factory('catalog')).resolves.toEqual({ - x: 1, - pluginId: 'catalog', - }); - await expect(factory('scaffolder')).resolves.toEqual({ - x: 1, - pluginId: 'scaffolder', - }); - expect(await factory('catalog')).toBe(await factory('catalog')); + await expect(registry.get(ref1, 'catalog')).resolves.toEqual({ x: 1 }); + await expect(registry.get(ref1, 'scaffolder')).resolves.toEqual({ x: 1 }); + expect(await registry.get(ref1, 'catalog')).toBe( + await registry.get(ref1, 'catalog'), + ); + expect(await registry.get(ref1, 'scaffolder')).toBe( + await registry.get(ref1, 'scaffolder'), + ); + expect(await registry.get(ref1, 'catalog')).not.toBe( + await registry.get(ref1, 'scaffolder'), + ); }); it('should handle multiple factories with different serviceRefs', async () => { const registry = new ServiceRegistry([sf1, sf2]); - const factory1 = registry.get(ref1)!; - const factory2 = registry.get(ref2)!; - expect(factory1).toEqual(expect.any(Function)); - expect(factory2).toEqual(expect.any(Function)); - await expect(factory1('catalog')).resolves.toEqual({ + + await expect(registry.get(ref1, 'catalog')).resolves.toEqual({ x: 1, - pluginId: 'catalog', }); - await expect(factory2('catalog')).resolves.toEqual({ + await expect(registry.get(ref2, 'catalog')).resolves.toEqual({ x: 2, - pluginId: 'catalog', }); - expect(await factory1('catalog')).not.toBe(await factory2('catalog')); + expect(await registry.get(ref1, 'catalog')).not.toBe( + await registry.get(ref2, 'catalog'), + ); }); it('should use the last factory for each ref', async () => { const registry = new ServiceRegistry([sf2, sf2b]); - const factory2 = registry.get(ref2)!; - await expect(factory2('catalog')).resolves.toEqual({ + await expect(registry.get(ref2, 'catalog')).resolves.toEqual({ x: 22, - pluginId: 'catalog', }); }); - it('should return the defaultFactory from the ref if not provided to the registry', async () => { + it('should use the defaultFactory from the ref if not provided to the registry', async () => { const registry = new ServiceRegistry([]); - const factory = registry.get(refDefault1)!; - expect(factory).toEqual(expect.any(Function)); - await expect(factory('catalog')).resolves.toEqual({ + await expect(registry.get(refDefault1, 'catalog')).resolves.toEqual({ x: 10, - pluginId: 'catalog', }); }); - it('should not return the defaultFactory from the ref if provided to the registry', async () => { + it('should not use the defaultFactory from the ref if provided to the registry', async () => { const registry = new ServiceRegistry([sf1]); - const factory = registry.get(refDefault1)!; - expect(factory).toEqual(expect.any(Function)); - await expect(factory('catalog')).resolves.toEqual({ + await expect(registry.get(refDefault1, 'catalog')).resolves.toEqual({ x: 1, - pluginId: 'catalog', }); }); it('should handle duplicate defaultFactories by duplicating the implementations', async () => { const registry = new ServiceRegistry([]); - const factoryA = registry.get(refDefault2a)!; - const factoryB = registry.get(refDefault2b)!; - expect(factoryA).toEqual(expect.any(Function)); - expect(factoryB).toEqual(expect.any(Function)); - await expect(factoryA('catalog')).resolves.toEqual({ + await expect(registry.get(refDefault2a, 'catalog')).resolves.toEqual({ x: 20, - pluginId: 'catalog', }); - await expect(factoryB('catalog')).resolves.toEqual({ + await expect(registry.get(refDefault2b, 'catalog')).resolves.toEqual({ x: 220, - pluginId: 'catalog', }); - expect(await factoryA('catalog')).toBe(await factoryA('catalog')); - expect(await factoryB('catalog')).toBe(await factoryB('catalog')); - expect(await factoryA('catalog')).not.toBe(await factoryB('catalog')); + expect(await registry.get(refDefault2a, 'catalog')).toBe( + await registry.get(refDefault2a, 'catalog'), + ); + expect(await registry.get(refDefault2b, 'catalog')).toBe( + await registry.get(refDefault2b, 'catalog'), + ); + expect(await registry.get(refDefault2a, 'catalog')).not.toBe( + await registry.get(refDefault2b, 'catalog'), + ); }); it('should only call each default factory loader once', async () => { @@ -186,17 +175,16 @@ describe('ServiceRegistry', () => { }); const registry = new ServiceRegistry([]); - const factory = registry.get(ref)!; await Promise.all([ - expect(factory('catalog')).resolves.toBeUndefined(), - expect(factory('catalog')).resolves.toBeUndefined(), + expect(registry.get(ref, 'catalog')).resolves.toBeUndefined(), + expect(registry.get(ref, 'catalog')).resolves.toBeUndefined(), ]); expect(factoryLoader).toHaveBeenCalledTimes(1); }); it('should not call factory functions more than once', async () => { - const innerFactory = jest.fn(async (pluginId: string) => { - return { x: 1, pluginId }; + const innerFactory = jest.fn(async () => { + return { x: 1 }; }); const factory = jest.fn(async () => innerFactory); const myFactory = createServiceFactory({ @@ -208,17 +196,15 @@ describe('ServiceRegistry', () => { const registry = new ServiceRegistry([myFactory]); await Promise.all([ - registry.get(ref1)!('catalog')!, - registry.get(ref1)!('catalog')!, - registry.get(ref1)!('catalog')!, - registry.get(ref1)!('scaffolder')!, - registry.get(ref1)!('scaffolder')!, + registry.get(ref1, 'catalog')!, + registry.get(ref1, 'catalog')!, + registry.get(ref1, 'catalog')!, + registry.get(ref1, 'scaffolder')!, + registry.get(ref1, 'scaffolder')!, ]); expect(factory).toHaveBeenCalledTimes(1); expect(innerFactory).toHaveBeenCalledTimes(2); - expect(innerFactory).toHaveBeenCalledWith('catalog'); - expect(innerFactory).toHaveBeenCalledWith('scaffolder'); }); it('should throw if dependencies are not available', async () => { @@ -231,9 +217,8 @@ describe('ServiceRegistry', () => { }); const registry = new ServiceRegistry([myFactory]); - const factory = registry.get(ref1)!; - await expect(factory('catalog')).rejects.toThrow( + await expect(registry.get(ref1, 'catalog')).rejects.toThrow( "Failed to instantiate service '1' for 'catalog' because the following dependent services are missing: '2'", ); }); @@ -247,8 +232,8 @@ describe('ServiceRegistry', () => { const factoryA = createServiceFactory({ service: refA, deps: { b: refB }, - async factory({ b }) { - return async pluginId => b(pluginId); + async factory() { + return async ({ b }) => b; }, }); @@ -261,9 +246,8 @@ describe('ServiceRegistry', () => { }); const registry = new ServiceRegistry([factoryA, factoryB]); - const factory = registry.get(refA)!; - await expect(factory('catalog')).rejects.toThrow( + await expect(registry.get(refA, 'catalog')).rejects.toThrow( "Failed to instantiate service 'a' for 'catalog' because the factory function threw an error, Error: Failed to instantiate service 'b' for 'catalog' because the following dependent services are missing: 'c', 'd'", ); }); @@ -278,9 +262,8 @@ describe('ServiceRegistry', () => { }); const registry = new ServiceRegistry([myFactory]); - const factory = registry.get(ref1)!; - await expect(factory('catalog')).rejects.toThrow( + await expect(registry.get(ref1, 'catalog')).rejects.toThrow( "Failed to instantiate service '1' because the top-level factory function threw an error, Error: top-level error", ); }); @@ -290,17 +273,16 @@ describe('ServiceRegistry', () => { service: ref1, deps: {}, async factory() { - return pluginId => { - throw new Error(`error in plugin ${pluginId}`); + return () => { + throw new Error(`error in plugin`); }; }, }); const registry = new ServiceRegistry([myFactory]); - const factory = registry.get(ref1)!; - await expect(factory('catalog')).rejects.toThrow( - "Failed to instantiate service '1' for 'catalog' because the factory function threw an error, Error: error in plugin catalog", + await expect(registry.get(ref1, 'catalog')).rejects.toThrow( + "Failed to instantiate service '1' for 'catalog' because the factory function threw an error, Error: error in plugin", ); }); @@ -313,9 +295,8 @@ describe('ServiceRegistry', () => { }); const registry = new ServiceRegistry([]); - const factory = registry.get(ref)!; - await expect(factory('catalog')).rejects.toThrow( + await expect(registry.get(ref, 'catalog')).rejects.toThrow( "Failed to instantiate service '1' because the default factory loader threw an error, Error: default factory error", ); }); diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts index e0509aecde..007ca00c88 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts @@ -16,8 +16,8 @@ import { ServiceFactory, - FactoryFunc, ServiceRef, + pluginMetadataServiceRef, } from '@backstage/backend-plugin-api'; import { stringifyError } from '@backstage/errors'; @@ -37,7 +37,9 @@ export class ServiceRegistry { readonly #implementations: Map< ServiceFactory, { - factoryFunc: Promise>; + factoryFunc: Promise< + (deps: { [name in string]: unknown }) => Promise + >; byPlugin: Map>; } >; @@ -58,67 +60,123 @@ export class ServiceRegistry { this.#implementations = new Map(); } - get(ref: ServiceRef): FactoryFunc | undefined { - let factory = this.#providedFactories.get(ref.id); - const { __defaultFactory: defaultFactory } = ref as InternalServiceRef; - if (!factory && !defaultFactory) { + #resolveFactory( + ref: ServiceRef, + pluginId: string, + ): Promise | undefined { + // Special case handling of the plugin metadata service, generating a custom factory for it each time + if (ref.id === pluginMetadataServiceRef.id) { + return Promise.resolve({ + scope: 'plugin', + service: pluginMetadataServiceRef, + deps: {}, + factory: async () => async () => ({ + getId() { + return pluginId; + }, + }), + }); + } + + let resolvedFactory: Promise | ServiceFactory | undefined = + this.#providedFactories.get(ref.id); + const { __defaultFactory: defaultFactory } = + ref as InternalServiceRef; + if (!resolvedFactory && !defaultFactory) { return undefined; } - return async (pluginId: string): Promise => { - if (!factory) { - let loadedFactory = this.#loadedDefaultFactories.get(defaultFactory!); - if (!loadedFactory) { - loadedFactory = Promise.resolve() - .then(() => defaultFactory!(ref)) - .then(f => - typeof f === 'function' ? f() : f, - ) as Promise; - this.#loadedDefaultFactories.set(defaultFactory!, loadedFactory); - } - // NOTE: This await is safe as long as #providedFactories is not mutated. - factory = await loadedFactory.catch(error => { - throw new Error( - `Failed to instantiate service '${ - ref.id - }' because the default factory loader threw an error, ${stringifyError( - error, - )}`, + if (!resolvedFactory) { + let loadedFactory = this.#loadedDefaultFactories.get(defaultFactory!); + if (!loadedFactory) { + loadedFactory = Promise.resolve() + .then(() => defaultFactory!(ref)) + .then(f => + typeof f === 'function' ? f() : f, + ) as Promise; + this.#loadedDefaultFactories.set(defaultFactory!, loadedFactory); + } + resolvedFactory = loadedFactory.catch(error => { + throw new Error( + `Failed to instantiate service '${ + ref.id + }' because the default factory loader threw an error, ${stringifyError( + error, + )}`, + ); + }); + } + + return Promise.resolve(resolvedFactory); + } + + #separateMapForTheRootService = new Map>(); + + #checkForMissingDeps(factory: ServiceFactory, pluginId: string) { + const missingDeps = Object.values(factory.deps).filter(ref => { + if (ref.id === pluginMetadataServiceRef.id) { + return false; + } + if (this.#providedFactories.get(ref.id)) { + return false; + } + + return !(ref as InternalServiceRef).__defaultFactory; + }); + + if (missingDeps.length) { + const missing = missingDeps.map(r => `'${r.id}'`).join(', '); + throw new Error( + `Failed to instantiate service '${factory.service.id}' for '${pluginId}' because the following dependent services are missing: ${missing}`, + ); + } + } + + get(ref: ServiceRef, pluginId: string): Promise | undefined { + return this.#resolveFactory(ref, pluginId)?.then(factory => { + if (factory.scope === 'root') { + let existing = this.#separateMapForTheRootService.get(factory); + if (!existing) { + this.#checkForMissingDeps(factory, pluginId); + const rootDeps = new Array>(); + + for (const [name, serviceRef] of Object.entries(factory.deps)) { + if (serviceRef.scope !== 'root') { + throw new Error( + `Failed to instantiate 'root' scoped service '${ref.id}' because it depends on '${serviceRef.scope}' scoped service '${serviceRef.id}'.`, + ); + } + const target = this.get(serviceRef, pluginId)!; + rootDeps.push(target.then(impl => [name, impl])); + } + + existing = Promise.all(rootDeps).then(entries => + factory.factory(Object.fromEntries(entries)), ); - }); + this.#separateMapForTheRootService.set(factory, existing); + } + return existing as Promise; } let implementation = this.#implementations.get(factory); if (!implementation) { - const missingRefs = new Array>(); - const factoryDeps: { [name in string]: FactoryFunc } = {}; + this.#checkForMissingDeps(factory, pluginId); + const rootDeps = new Array>(); for (const [name, serviceRef] of Object.entries(factory.deps)) { - const target = this.get(serviceRef); - if (!target) { - missingRefs.push(serviceRef); - } else { - factoryDeps[name] = target; + if (serviceRef.scope === 'root') { + const target = this.get(serviceRef, pluginId)!; + rootDeps.push(target.then(impl => [name, impl])); } } - if (missingRefs.length) { - const missing = missingRefs.map(r => `'${r.id}'`).join(', '); - throw new Error( - `Failed to instantiate service '${ref.id}' for '${pluginId}' because the following dependent services are missing: ${missing}`, - ); - } - implementation = { - factoryFunc: Promise.resolve() - .then(() => factory!.factory(factoryDeps)) + factoryFunc: Promise.all(rootDeps) + .then(entries => factory.factory(Object.fromEntries(entries))) .catch(error => { + const cause = stringifyError(error); throw new Error( - `Failed to instantiate service '${ - ref.id - }' because the top-level factory function threw an error, ${stringifyError( - error, - )}`, + `Failed to instantiate service '${ref.id}' because the top-level factory function threw an error, ${cause}`, ); }), byPlugin: new Map(), @@ -129,24 +187,29 @@ export class ServiceRegistry { let result = implementation.byPlugin.get(pluginId) as Promise; if (!result) { - result = implementation.factoryFunc.then(func => - Promise.resolve() - .then(() => func(pluginId)) - .catch(error => { - throw new Error( - `Failed to instantiate service '${ - ref.id - }' for '${pluginId}' because the factory function threw an error, ${stringifyError( - error, - )}`, - ); - }), - ); + const allDeps = new Array>(); + for (const [name, serviceRef] of Object.entries(factory.deps)) { + const target = this.get(serviceRef, pluginId)!; + allDeps.push(target.then(impl => [name, impl])); + } + + result = implementation.factoryFunc + .then(func => + Promise.all(allDeps).then(entries => + func(Object.fromEntries(entries)), + ), + ) + .catch(error => { + const cause = stringifyError(error); + throw new Error( + `Failed to instantiate service '${ref.id}' for '${pluginId}' because the factory function threw an error, ${cause}`, + ); + }); implementation.byPlugin.set(pluginId, result); } return result; - }; + }); } } diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts index febc6830c7..40ce6aa1ae 100644 --- a/packages/backend-app-api/src/wiring/types.ts +++ b/packages/backend-app-api/src/wiring/types.ts @@ -18,7 +18,6 @@ import { ServiceFactory, BackendFeature, ExtensionPoint, - FactoryFunc, ServiceRef, } from '@backstage/backend-plugin-api'; import { BackstageBackend } from './BackstageBackend'; @@ -47,7 +46,7 @@ export interface CreateSpecializedBackendOptions { } export type ServiceHolder = { - get(api: ServiceRef): FactoryFunc | undefined; + get(api: ServiceRef, pluginId: string): Promise | undefined; }; /** diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index a19c17bb9c..4a9f91541a 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -19,63 +19,84 @@ * * @public */ -export type ServiceRef = { +export type ServiceRef< + TService, + TScope extends 'root' | 'plugin' = 'root' | 'plugin', +> = { id: string; + /** + * This determines the scope at which this service is available. + * + * Root scoped services are available to all other services but + * may only depend on other root scoped services. + * + * Plugin scoped services are only available to other plugin scoped + * services but may depend on all other services. + */ + scope: TScope; + /** * Utility for getting the type of the service, using `typeof serviceRef.T`. * Attempting to actually read this value will result in an exception. */ - T: T; + T: TService; toString(): string; $$ref: 'service'; }; -/** - * @internal - */ -export type InternalServiceRef = ServiceRef & { - /** - * The default factory that will be used to create service - * instances if no other factory is provided. - */ - __defaultFactory?: ( - service: ServiceRef, - ) => Promise | (() => ServiceFactory)>; -}; - /** @public */ export type TypesToServiceRef = { [key in keyof T]: ServiceRef }; /** @public */ -export type DepsToDepFactories = { - [key in keyof T]: (pluginId: string) => Promise; -}; - -/** @public */ -export type FactoryFunc = (pluginId: string) => Promise; - -/** @public */ -export type ServiceFactory = { - service: ServiceRef; - deps: { [key in string]: ServiceRef }; - factory(deps: { [key in string]: unknown }): Promise>; -}; +export type ServiceFactory = + | { + // This scope prop is needed in addition to the service ref, as TypeScript + // can't properly discriminate the two factory types otherwise. + scope: 'root'; + service: ServiceRef; + deps: { [key in string]: ServiceRef }; + factory(deps: { [key in string]: unknown }): Promise; + } + | { + scope: 'plugin'; + service: ServiceRef; + deps: { [key in string]: ServiceRef }; + factory(deps: { [key in string]: unknown }): Promise< + (deps: { [key in string]: unknown }) => Promise + >; + }; /** * @public */ export function createServiceRef(options: { id: string; + scope?: 'plugin'; defaultFactory?: ( service: ServiceRef, ) => Promise | (() => ServiceFactory)>; -}): ServiceRef { - const { id, defaultFactory } = options; +}): ServiceRef; +export function createServiceRef(options: { + id: string; + scope: 'root'; + defaultFactory?: ( + service: ServiceRef, + ) => Promise | (() => ServiceFactory)>; +}): ServiceRef; +export function createServiceRef(options: { + id: string; + scope?: 'plugin' | 'root'; + defaultFactory?: ( + service: ServiceRef, + ) => Promise | (() => ServiceFactory)>; +}): ServiceRef { + const { id, scope = 'plugin', defaultFactory } = options; return { id, + scope, get T(): T { throw new Error(`tried to read ServiceRef.T of ${this}`); }, @@ -84,32 +105,58 @@ export function createServiceRef(options: { }, $$ref: 'service', // TODO: declare __defaultFactory: defaultFactory, - } as InternalServiceRef; + } as ServiceRef & { + __defaultFactory?: ( + service: ServiceRef, + ) => Promise | (() => ServiceFactory)>; + }; } +type OnlyRootScopeDependencies< + TDeps extends { [key in string]: ServiceRef }, +> = Pick< + TDeps, + { + [name in keyof TDeps]: TDeps[name]['scope'] extends 'root' ? name : never; + }[keyof TDeps] +>; + +type DependencyRefsToInstances< + T extends { [key in string]: ServiceRef }, +> = { + [key in keyof T]: T[key] extends ServiceRef ? TImpl : never; +}; + /** * @public */ export function createServiceFactory< TService, + TScope extends 'root' | 'plugin', TImpl extends TService, - TDeps extends { [name in string]: unknown }, + TDeps extends { [name in string]: ServiceRef }, TOpts extends { [name in string]: unknown } | undefined = undefined, ->(factory: { - service: ServiceRef; - deps: TypesToServiceRef; +>(config: { + service: ServiceRef; + deps: TDeps; factory( - deps: DepsToDepFactories, + deps: DependencyRefsToInstances>, options: TOpts, - ): Promise>; + ): TScope extends 'root' + ? Promise + : Promise<(deps: DependencyRefsToInstances) => Promise>; }): undefined extends TOpts ? (options?: TOpts) => ServiceFactory : (options: TOpts) => ServiceFactory { - return (options?: TOpts) => ({ - service: factory.service, - deps: factory.deps, - factory(deps: DepsToDepFactories) { - return factory.factory(deps, options!); - }, - }); + return (options?: TOpts) => + ({ + scope: config.service.scope, + service: config.service, + deps: config.deps, + factory( + deps: DependencyRefsToInstances>, + ) { + return config.factory(deps, options!); + }, + } as ServiceFactory); } From 4b324c83f8bbc62f191139735cf9e4d43a100e54 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 8 Sep 2022 16:09:42 +0200 Subject: [PATCH 21/41] backend-app-api: 100% coverage of ServiceRegistry 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 Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- .../src/wiring/ServiceRegistry.test.ts | 72 ++++++++++++++++--- 1 file changed, 64 insertions(+), 8 deletions(-) diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts index ab709dcb77..211f9ac31e 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts @@ -18,6 +18,7 @@ import { createServiceRef, createServiceFactory, ServiceRef, + pluginMetadataServiceRef, } from '@backstage/backend-plugin-api'; import { ServiceRegistry } from './ServiceRegistry'; @@ -35,26 +36,23 @@ const sf1 = createServiceFactory({ }); const ref2 = createServiceRef<{ x: number }>({ + scope: 'root', id: '2', }); const sf2 = createServiceFactory({ service: ref2, deps: {}, factory: async () => { - return async () => { - return { x: 2 }; - }; + return { x: 2 }; }, }); const sf2b = createServiceFactory({ service: ref2, deps: {}, factory: async () => { - return async () => { - return { x: 22 }; - }; + return { x: 22 }; }, -}); +})(); const refDefault1 = createServiceRef<{ x: number }>({ id: '1', @@ -63,7 +61,7 @@ const refDefault1 = createServiceRef<{ x: number }>({ service, deps: {}, factory: async () => async () => ({ x: 10 }), - }), + })(), }); const refDefault2a = createServiceRef<{ x: number }>({ @@ -121,6 +119,64 @@ describe('ServiceRegistry', () => { ); }); + it('should not be possible for root scoped services to depend on plugin scoped services', async () => { + const factory = createServiceFactory({ + service: ref2, + deps: { pluginDep: ref1 }, + factory: async () => { + return { x: 2 }; + }, + }); + const registry = new ServiceRegistry([factory, sf1]); + await expect(registry.get(ref2, 'catalog')).rejects.toThrow( + "Failed to instantiate 'root' scoped service '2' because it depends on 'plugin' scoped service '1'.", + ); + }); + + it('should be possible for plugin scoped services to depend on root scoped services', async () => { + const factory = createServiceFactory({ + service: ref1, + deps: { rootDep: ref2 }, + factory: async ({ rootDep }) => { + return async () => ({ x: rootDep.x }); + }, + }); + const registry = new ServiceRegistry([factory, sf2]); + await expect(registry.get(ref1, 'catalog')).resolves.toEqual({ + x: 2, + }); + }); + + it('should be possible for root scoped services to depend on root scoped services', async () => { + const ref = createServiceRef<{ x: number }>({ id: 'x', scope: 'root' }); + const factory = createServiceFactory({ + service: ref, + deps: { rootDep: ref2 }, + factory: async ({ rootDep }) => { + return { x: rootDep.x }; + }, + }); + const registry = new ServiceRegistry([factory, sf2]); + await expect(registry.get(ref, 'catalog')).resolves.toEqual({ + x: 2, + }); + }); + + it('should return the pluginId from the pluginMetadata service', async () => { + const ref = createServiceRef<{ pluginId: string }>({ id: 'x' }); + const factory = createServiceFactory({ + service: ref, + deps: { meta: pluginMetadataServiceRef }, + factory: async ({}) => { + return async ({ meta }) => ({ pluginId: meta.getId() }); + }, + }); + const registry = new ServiceRegistry([factory]); + await expect(registry.get(ref, 'catalog')).resolves.toEqual({ + pluginId: 'catalog', + }); + }); + it('should use the last factory for each ref', async () => { const registry = new ServiceRegistry([sf2, sf2b]); await expect(registry.get(ref2, 'catalog')).resolves.toEqual({ From dc74ebd10d57f8f290db50c172aad3435e46e6bf Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 9 Sep 2022 15:29:44 +0200 Subject: [PATCH 22/41] backend-plugin-api: refactor createServiceFactory helper types 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 Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- .../src/services/system/types.ts | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index 4a9f91541a..579d7abaab 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -112,19 +112,13 @@ export function createServiceRef(options: { }; } -type OnlyRootScopeDependencies< - TDeps extends { [key in string]: ServiceRef }, -> = Pick< - TDeps, - { - [name in keyof TDeps]: TDeps[name]['scope'] extends 'root' ? name : never; - }[keyof TDeps] ->; - -type DependencyRefsToInstances< +type ServiceRefsToInstances< T extends { [key in string]: ServiceRef }, + TScope extends 'root' | 'plugin' = 'root' | 'plugin', > = { - [key in keyof T]: T[key] extends ServiceRef ? TImpl : never; + [key in keyof T]: T[key] extends ServiceRef + ? TImpl + : never; }; /** @@ -140,11 +134,11 @@ export function createServiceFactory< service: ServiceRef; deps: TDeps; factory( - deps: DependencyRefsToInstances>, + deps: ServiceRefsToInstances, options: TOpts, ): TScope extends 'root' ? Promise - : Promise<(deps: DependencyRefsToInstances) => Promise>; + : Promise<(deps: ServiceRefsToInstances) => Promise>; }): undefined extends TOpts ? (options?: TOpts) => ServiceFactory : (options: TOpts) => ServiceFactory { @@ -153,9 +147,7 @@ export function createServiceFactory< scope: config.service.scope, service: config.service, deps: config.deps, - factory( - deps: DependencyRefsToInstances>, - ) { + factory(deps: ServiceRefsToInstances) { return config.factory(deps, options!); }, } as ServiceFactory); From 2bf8855c1dc37ceb21dda11c1590795a96e8fa1e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 9 Sep 2022 15:30:08 +0200 Subject: [PATCH 23/41] core-app-api: switch to non-broken way of defining service factories 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: Johan Haals Signed-off-by: Patrik Oldsberg --- .../src/wiring/ServiceRegistry.test.ts | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts index 211f9ac31e..897ed4e3cd 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts @@ -28,7 +28,7 @@ const ref1 = createServiceRef<{ x: number }>({ const sf1 = createServiceFactory({ service: ref1, deps: {}, - factory: async () => { + async factory() { return async () => { return { x: 1 }; }; @@ -42,14 +42,14 @@ const ref2 = createServiceRef<{ x: number }>({ const sf2 = createServiceFactory({ service: ref2, deps: {}, - factory: async () => { + async factory() { return { x: 2 }; }, }); const sf2b = createServiceFactory({ service: ref2, deps: {}, - factory: async () => { + async factory() { return { x: 22 }; }, })(); @@ -60,7 +60,9 @@ const refDefault1 = createServiceRef<{ x: number }>({ createServiceFactory({ service, deps: {}, - factory: async () => async () => ({ x: 10 }), + async factory() { + return async () => ({ x: 10 }); + }, })(), }); @@ -70,7 +72,9 @@ const refDefault2a = createServiceRef<{ x: number }>({ createServiceFactory({ service, deps: {}, - factory: async () => async () => ({ x: 20 }), + async factory() { + return async () => ({ x: 20 }); + }, }), }); @@ -80,7 +84,9 @@ const refDefault2b = createServiceRef<{ x: number }>({ createServiceFactory({ service, deps: {}, - factory: async () => async () => ({ x: 220 }), + async factory() { + return async () => ({ x: 220 }); + }, }), }); @@ -123,7 +129,7 @@ describe('ServiceRegistry', () => { const factory = createServiceFactory({ service: ref2, deps: { pluginDep: ref1 }, - factory: async () => { + async factory() { return { x: 2 }; }, }); @@ -137,7 +143,7 @@ describe('ServiceRegistry', () => { const factory = createServiceFactory({ service: ref1, deps: { rootDep: ref2 }, - factory: async ({ rootDep }) => { + async factory({ rootDep }) { return async () => ({ x: rootDep.x }); }, }); @@ -152,7 +158,7 @@ describe('ServiceRegistry', () => { const factory = createServiceFactory({ service: ref, deps: { rootDep: ref2 }, - factory: async ({ rootDep }) => { + async factory({ rootDep }) { return { x: rootDep.x }; }, }); @@ -167,7 +173,7 @@ describe('ServiceRegistry', () => { const factory = createServiceFactory({ service: ref, deps: { meta: pluginMetadataServiceRef }, - factory: async ({}) => { + async factory() { return async ({ meta }) => ({ pluginId: meta.getId() }); }, }); @@ -222,7 +228,9 @@ describe('ServiceRegistry', () => { createServiceFactory({ service, deps: {}, - factory: async () => async () => {}, + async factory() { + return async () => {}; + }, }), ); const ref = createServiceRef({ From f55c11f29d55cf2639b3d75976167f1506274cfa Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 9 Sep 2022 15:34:29 +0200 Subject: [PATCH 24/41] core-plugin-api: narrow scope type of service refs passed to default factory loader Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- .../src/services/system/types.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index 579d7abaab..e52ef5cc5a 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -76,23 +76,27 @@ export function createServiceRef(options: { id: string; scope?: 'plugin'; defaultFactory?: ( - service: ServiceRef, + service: ServiceRef, ) => Promise | (() => ServiceFactory)>; }): ServiceRef; export function createServiceRef(options: { id: string; scope: 'root'; defaultFactory?: ( - service: ServiceRef, + service: ServiceRef, ) => Promise | (() => ServiceFactory)>; }): ServiceRef; export function createServiceRef(options: { id: string; scope?: 'plugin' | 'root'; - defaultFactory?: ( - service: ServiceRef, - ) => Promise | (() => ServiceFactory)>; -}): ServiceRef { + defaultFactory?: + | (( + service: ServiceRef, + ) => Promise | (() => ServiceFactory)>) + | (( + service: ServiceRef, + ) => Promise | (() => ServiceFactory)>); +}): ServiceRef { const { id, scope = 'plugin', defaultFactory } = options; return { id, From 06c744d142d7d9e8112dfe897ae0c3028560437a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 9 Sep 2022 15:35:21 +0200 Subject: [PATCH 25/41] backend-plugin-api: add initial PluginMetadataService definition 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 Co-authored-by: Johan Haals Signed-off-by: Patrik Oldsberg --- .../src/services/definitions/index.ts | 1 + .../definitions/pluginMetadataServiceRef.ts | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 packages/backend-plugin-api/src/services/definitions/pluginMetadataServiceRef.ts diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index 797cfb5a76..fb204df495 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -26,3 +26,4 @@ export { discoveryServiceRef } from './discoveryServiceRef'; export { tokenManagerServiceRef } from './tokenManagerServiceRef'; export { permissionsServiceRef } from './permissionsServiceRef'; export { schedulerServiceRef } from './schedulerServiceRef'; +export { pluginMetadataServiceRef } from './pluginMetadataServiceRef'; diff --git a/packages/backend-plugin-api/src/services/definitions/pluginMetadataServiceRef.ts b/packages/backend-plugin-api/src/services/definitions/pluginMetadataServiceRef.ts new file mode 100644 index 0000000000..3af6e54900 --- /dev/null +++ b/packages/backend-plugin-api/src/services/definitions/pluginMetadataServiceRef.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createServiceRef } from '../system/types'; + +/** + * @public + */ +export interface PluginMetadata { + getId(): string; +} + +/** + * @public + */ +export const pluginMetadataServiceRef = createServiceRef({ + id: 'core.plugin-metadata', +}); From fb93ecad9cf98f0410fb7341eff7d427347b1ab2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 9 Sep 2022 15:52:03 +0200 Subject: [PATCH 26/41] backend-plugin-api: remove services with scope mismatch from deps type Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- packages/backend-plugin-api/src/services/system/types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index e52ef5cc5a..3edee2ec0f 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -120,9 +120,9 @@ type ServiceRefsToInstances< T extends { [key in string]: ServiceRef }, TScope extends 'root' | 'plugin' = 'root' | 'plugin', > = { - [key in keyof T]: T[key] extends ServiceRef - ? TImpl - : never; + [name in { + [key in keyof T]: T[key] extends ServiceRef ? key : never; + }[keyof T]]: T[name] extends ServiceRef ? TImpl : never; }; /** From 2a29d24519cd5ae64e6787eb55beaa4ff7665ca5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 9 Sep 2022 16:10:59 +0200 Subject: [PATCH 27/41] backend-app-api,backend-plugin-api: refactor existing service factories and fix type issues Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- .../services/implementations/cacheService.ts | 11 +++-- .../services/implementations/configService.ts | 11 ++--- .../implementations/databaseService.ts | 11 +++-- .../implementations/discoveryService.ts | 5 +- .../implementations/httpRouterService.ts | 11 +++-- .../services/implementations/loggerService.ts | 33 ++++--------- .../implementations/permissionsService.ts | 22 ++++----- .../implementations/rootLoggerService.ts | 48 +++++++++++++++++++ .../implementations/schedulerService.ts | 11 +++-- .../implementations/tokenManagerService.ts | 29 ++--------- .../implementations/urlReaderService.ts | 11 ++--- .../src/wiring/BackendInitializer.ts | 7 +-- .../services/definitions/configServiceRef.ts | 3 +- .../src/services/definitions/index.ts | 1 + .../definitions/rootLoggerServiceRef.ts | 26 ++++++++++ .../src/services/system/index.ts | 8 +--- .../src/services/system/types.ts | 1 + plugins/catalog-node/src/catalogService.ts | 10 ++-- 18 files changed, 145 insertions(+), 114 deletions(-) create mode 100644 packages/backend-app-api/src/services/implementations/rootLoggerService.ts create mode 100644 packages/backend-plugin-api/src/services/definitions/rootLoggerServiceRef.ts diff --git a/packages/backend-app-api/src/services/implementations/cacheService.ts b/packages/backend-app-api/src/services/implementations/cacheService.ts index c5e58b4a41..7e15f031c6 100644 --- a/packages/backend-app-api/src/services/implementations/cacheService.ts +++ b/packages/backend-app-api/src/services/implementations/cacheService.ts @@ -18,6 +18,7 @@ import { CacheManager } from '@backstage/backend-common'; import { configServiceRef, createServiceFactory, + pluginMetadataServiceRef, cacheServiceRef, } from '@backstage/backend-plugin-api'; @@ -25,13 +26,13 @@ import { export const cacheFactory = createServiceFactory({ service: cacheServiceRef, deps: { - configFactory: configServiceRef, + config: configServiceRef, + plugin: pluginMetadataServiceRef, }, - factory: async ({ configFactory }) => { - const config = await configFactory('root'); + async factory({ config }) { const cacheManager = CacheManager.fromConfig(config); - return async (pluginId: string) => { - return cacheManager.forPlugin(pluginId); + return async ({ plugin }) => { + return cacheManager.forPlugin(plugin.getId()); }; }, }); diff --git a/packages/backend-app-api/src/services/implementations/configService.ts b/packages/backend-app-api/src/services/implementations/configService.ts index c4aa641f72..91327289df 100644 --- a/packages/backend-app-api/src/services/implementations/configService.ts +++ b/packages/backend-app-api/src/services/implementations/configService.ts @@ -19,23 +19,20 @@ import { configServiceRef, createServiceFactory, loggerToWinstonLogger, - loggerServiceRef, + rootLoggerServiceRef, } from '@backstage/backend-plugin-api'; /** @public */ export const configFactory = createServiceFactory({ service: configServiceRef, deps: { - loggerFactory: loggerServiceRef, + logger: rootLoggerServiceRef, }, - factory: async ({ loggerFactory }) => { - const logger = await loggerFactory('root'); + async factory({ logger }) { const config = await loadBackendConfig({ argv: process.argv, logger: loggerToWinstonLogger(logger), }); - return async () => { - return config; - }; + return config; }, }); diff --git a/packages/backend-app-api/src/services/implementations/databaseService.ts b/packages/backend-app-api/src/services/implementations/databaseService.ts index b2bc19de84..f6401528e6 100644 --- a/packages/backend-app-api/src/services/implementations/databaseService.ts +++ b/packages/backend-app-api/src/services/implementations/databaseService.ts @@ -19,19 +19,20 @@ import { configServiceRef, createServiceFactory, databaseServiceRef, + pluginMetadataServiceRef, } from '@backstage/backend-plugin-api'; /** @public */ export const databaseFactory = createServiceFactory({ service: databaseServiceRef, deps: { - configFactory: configServiceRef, + config: configServiceRef, + plugin: pluginMetadataServiceRef, }, - factory: async ({ configFactory }) => { - const config = await configFactory('root'); + async factory({ config }) { const databaseManager = DatabaseManager.fromConfig(config); - return async (pluginId: string) => { - return databaseManager.forPlugin(pluginId); + return async ({ plugin }) => { + return databaseManager.forPlugin(plugin.getId()); }; }, }); diff --git a/packages/backend-app-api/src/services/implementations/discoveryService.ts b/packages/backend-app-api/src/services/implementations/discoveryService.ts index 3f1a584c61..7f35bf2447 100644 --- a/packages/backend-app-api/src/services/implementations/discoveryService.ts +++ b/packages/backend-app-api/src/services/implementations/discoveryService.ts @@ -25,10 +25,9 @@ import { export const discoveryFactory = createServiceFactory({ service: discoveryServiceRef, deps: { - configFactory: configServiceRef, + config: configServiceRef, }, - factory: async ({ configFactory }) => { - const config = await configFactory('root'); + async factory({ config }) { const discovery = SingleHostDiscovery.fromConfig(config); return async () => { return discovery; diff --git a/packages/backend-app-api/src/services/implementations/httpRouterService.ts b/packages/backend-app-api/src/services/implementations/httpRouterService.ts index 6460a77eaf..be4af664c4 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouterService.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouterService.ts @@ -18,6 +18,7 @@ import { createServiceFactory, httpRouterServiceRef, configServiceRef, + pluginMetadataServiceRef, } from '@backstage/backend-plugin-api'; import Router from 'express-promise-router'; import { Handler } from 'express'; @@ -27,18 +28,20 @@ import { createServiceBuilder } from '@backstage/backend-common'; export const httpRouterFactory = createServiceFactory({ service: httpRouterServiceRef, deps: { - configFactory: configServiceRef, + config: configServiceRef, + plugin: pluginMetadataServiceRef, }, - factory: async ({ configFactory }) => { + async factory({ config }) { const rootRouter = Router(); const service = createServiceBuilder(module) - .loadConfig(await configFactory('root')) + .loadConfig(config) .addRouter('', rootRouter); await service.start(); - return async (pluginId?: string) => { + return async ({ plugin }) => { + const pluginId = plugin.getId(); const path = pluginId ? `/api/${pluginId}` : ''; return { use(handler: Handler) { diff --git a/packages/backend-app-api/src/services/implementations/loggerService.ts b/packages/backend-app-api/src/services/implementations/loggerService.ts index e90b591302..ff72020140 100644 --- a/packages/backend-app-api/src/services/implementations/loggerService.ts +++ b/packages/backend-app-api/src/services/implementations/loggerService.ts @@ -14,38 +14,23 @@ * limitations under the License. */ -import { createRootLogger } from '@backstage/backend-common'; import { createServiceFactory, - Logger, loggerServiceRef, + pluginMetadataServiceRef, + rootLoggerServiceRef, } from '@backstage/backend-plugin-api'; -import { Logger as WinstonLogger } from 'winston'; - -class BackstageLogger implements Logger { - static fromWinston(logger: WinstonLogger): BackstageLogger { - return new BackstageLogger(logger); - } - - private constructor(private readonly winston: WinstonLogger) {} - - info(message: string, ...meta: any[]): void { - this.winston.info(message, ...meta); - } - - child(fields: { [name: string]: string }): Logger { - return new BackstageLogger(this.winston.child(fields)); - } -} /** @public */ export const loggerFactory = createServiceFactory({ service: loggerServiceRef, - deps: {}, - factory: async () => { - const root = BackstageLogger.fromWinston(createRootLogger()); - return async (pluginId: string) => { - return root.child({ pluginId }); + deps: { + rootLogger: rootLoggerServiceRef, + plugin: pluginMetadataServiceRef, + }, + async factory({ rootLogger }) { + return async ({ plugin }) => { + return rootLogger.child({ pluginId: plugin.getId() }); }; }, }); diff --git a/packages/backend-app-api/src/services/implementations/permissionsService.ts b/packages/backend-app-api/src/services/implementations/permissionsService.ts index 26fe012a20..32e8a1a9f9 100644 --- a/packages/backend-app-api/src/services/implementations/permissionsService.ts +++ b/packages/backend-app-api/src/services/implementations/permissionsService.ts @@ -27,20 +27,16 @@ import { ServerPermissionClient } from '@backstage/plugin-permission-node'; export const permissionsFactory = createServiceFactory({ service: permissionsServiceRef, deps: { - configFactory: configServiceRef, - discoveryFactory: discoveryServiceRef, - tokenManagerFactory: tokenManagerServiceRef, + config: configServiceRef, + discovery: discoveryServiceRef, + tokenManager: tokenManagerServiceRef, }, - factory: async ({ configFactory, discoveryFactory, tokenManagerFactory }) => { - const config = await configFactory('root'); - const discovery = await discoveryFactory('root'); - const tokenManager = await tokenManagerFactory('root'); - const permissions = ServerPermissionClient.fromConfig(config, { - discovery, - tokenManager, - }); - return async (_pluginId: string) => { - return permissions; + async factory({ config }) { + return async ({ discovery, tokenManager }) => { + return ServerPermissionClient.fromConfig(config, { + discovery, + tokenManager, + }); }; }, }); diff --git a/packages/backend-app-api/src/services/implementations/rootLoggerService.ts b/packages/backend-app-api/src/services/implementations/rootLoggerService.ts new file mode 100644 index 0000000000..d7da11723e --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/rootLoggerService.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createRootLogger } from '@backstage/backend-common'; +import { + createServiceFactory, + Logger, + rootLoggerServiceRef, +} from '@backstage/backend-plugin-api'; +import { Logger as WinstonLogger } from 'winston'; + +class BackstageLogger implements Logger { + static fromWinston(logger: WinstonLogger): BackstageLogger { + return new BackstageLogger(logger); + } + + private constructor(private readonly winston: WinstonLogger) {} + + info(message: string, ...meta: any[]): void { + this.winston.info(message, ...meta); + } + + child(fields: { [name: string]: string }): Logger { + return new BackstageLogger(this.winston.child(fields)); + } +} + +/** @public */ +export const loggerFactory = createServiceFactory({ + service: rootLoggerServiceRef, + deps: {}, + async factory() { + return BackstageLogger.fromWinston(createRootLogger()); + }, +}); diff --git a/packages/backend-app-api/src/services/implementations/schedulerService.ts b/packages/backend-app-api/src/services/implementations/schedulerService.ts index 39dbf26ba9..40676344ec 100644 --- a/packages/backend-app-api/src/services/implementations/schedulerService.ts +++ b/packages/backend-app-api/src/services/implementations/schedulerService.ts @@ -17,6 +17,7 @@ import { configServiceRef, createServiceFactory, + pluginMetadataServiceRef, schedulerServiceRef, } from '@backstage/backend-plugin-api'; import { TaskScheduler } from '@backstage/backend-tasks'; @@ -25,13 +26,13 @@ import { TaskScheduler } from '@backstage/backend-tasks'; export const schedulerFactory = createServiceFactory({ service: schedulerServiceRef, deps: { - configFactory: configServiceRef, + config: configServiceRef, + plugin: pluginMetadataServiceRef, }, - factory: async ({ configFactory }) => { - const config = await configFactory('root'); + async factory({ config }) { const taskScheduler = TaskScheduler.fromConfig(config); - return async (pluginId: string) => { - return taskScheduler.forPlugin(pluginId); + return async ({ plugin }) => { + return taskScheduler.forPlugin(plugin.getId()); }; }, }); diff --git a/packages/backend-app-api/src/services/implementations/tokenManagerService.ts b/packages/backend-app-api/src/services/implementations/tokenManagerService.ts index 7767c17944..92f42c10db 100644 --- a/packages/backend-app-api/src/services/implementations/tokenManagerService.ts +++ b/packages/backend-app-api/src/services/implementations/tokenManagerService.ts @@ -27,32 +27,11 @@ import { ServerTokenManager } from '@backstage/backend-common'; export const tokenManagerFactory = createServiceFactory({ service: tokenManagerServiceRef, deps: { - configFactory: configServiceRef, - loggerFactory: loggerServiceRef, + config: configServiceRef, + logger: loggerServiceRef, }, - factory: async ({ configFactory, loggerFactory }) => { - const logger = await loggerFactory('root'); - const config = await configFactory('root'); - return async (_pluginId: string) => { - // doesn't the logger want to be inferred from the plugin tho here? - // maybe ... also why do we recreate it every time otherwise - // we should memoize on a per plugin right? so I think it's should be fine to re-use the plugin one - // we shouldn't recreate on a per plugin basis. - // hm - on the other hand, is this really ever called more than once? - // not this function right. should only be called when the plugin requests this serviceRef - // yeah so no need to worry about memo probably - // but we still want to scope the logger to the ServrTokenmanagfer>? - // mm sure maybe - // maybe in this case it doesn't provide so much value b - // oh hang on - isn't it up to THE MANAGER to make a child internally if it wants to do that - // so that it becomes a property intrinsic to that class, no matter how it's constructed - // or is that too much responsibility for it - making the constructor complex so to speak, making it harder to tweak that behavior - // this is not ultra efficient :) - - // I think the naming here is wrong to be gonest - // this isn't like the cache manager or the database manager - // the manager name is confusuion i think - // aye perhaps + async factory() { + return async ({ config, logger }) => { return ServerTokenManager.fromConfig(config, { logger: loggerToWinstonLogger(logger), }); diff --git a/packages/backend-app-api/src/services/implementations/urlReaderService.ts b/packages/backend-app-api/src/services/implementations/urlReaderService.ts index df353a52d2..d7d7502a08 100644 --- a/packages/backend-app-api/src/services/implementations/urlReaderService.ts +++ b/packages/backend-app-api/src/services/implementations/urlReaderService.ts @@ -27,15 +27,14 @@ import { export const urlReaderFactory = createServiceFactory({ service: urlReaderServiceRef, deps: { - configFactory: configServiceRef, - loggerFactory: loggerServiceRef, + config: configServiceRef, + logger: loggerServiceRef, }, - factory: async ({ configFactory, loggerFactory }) => { - return async (pluginId: string) => { - const logger = await loggerFactory(pluginId); + async factory() { + return async ({ config, logger }) => { return UrlReaders.default({ + config, logger: loggerToWinstonLogger(logger), - config: await configFactory(pluginId), }); }; }, diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index afb5250504..c86cd8382d 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -50,11 +50,12 @@ export class BackendInitializer { if (extensionPoint) { result.set(name, extensionPoint); } else { - const factory = await this.#serviceHolder.get( + const impl = await this.#serviceHolder.get( ref as ServiceRef, + pluginId, ); - if (factory) { - result.set(name, await factory(pluginId)); + if (impl) { + result.set(name, impl); } else { missingRefs.add(ref); } diff --git a/packages/backend-plugin-api/src/services/definitions/configServiceRef.ts b/packages/backend-plugin-api/src/services/definitions/configServiceRef.ts index ba5dcacdc8..f17c5f57bc 100644 --- a/packages/backend-plugin-api/src/services/definitions/configServiceRef.ts +++ b/packages/backend-plugin-api/src/services/definitions/configServiceRef.ts @@ -21,5 +21,6 @@ import { createServiceRef } from '../system/types'; * @public */ export const configServiceRef = createServiceRef({ - id: 'core.config', + id: 'core.root.config', + scope: 'root', }); diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index fb204df495..44ccb62978 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -26,4 +26,5 @@ export { discoveryServiceRef } from './discoveryServiceRef'; export { tokenManagerServiceRef } from './tokenManagerServiceRef'; export { permissionsServiceRef } from './permissionsServiceRef'; export { schedulerServiceRef } from './schedulerServiceRef'; +export { rootLoggerServiceRef } from './rootLoggerServiceRef'; export { pluginMetadataServiceRef } from './pluginMetadataServiceRef'; diff --git a/packages/backend-plugin-api/src/services/definitions/rootLoggerServiceRef.ts b/packages/backend-plugin-api/src/services/definitions/rootLoggerServiceRef.ts new file mode 100644 index 0000000000..62e22c53d9 --- /dev/null +++ b/packages/backend-plugin-api/src/services/definitions/rootLoggerServiceRef.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createServiceRef } from '../system/types'; +import { Logger } from './loggerServiceRef'; + +/** + * @public + */ +export const rootLoggerServiceRef = createServiceRef({ + id: 'core.root.logger', + scope: 'root', +}); diff --git a/packages/backend-plugin-api/src/services/system/index.ts b/packages/backend-plugin-api/src/services/system/index.ts index 817b0a590f..8c666af42e 100644 --- a/packages/backend-plugin-api/src/services/system/index.ts +++ b/packages/backend-plugin-api/src/services/system/index.ts @@ -14,11 +14,5 @@ * limitations under the License. */ -export type { - ServiceRef, - TypesToServiceRef, - DepsToDepFactories, - FactoryFunc, - ServiceFactory, -} from './types'; +export type { ServiceRef, TypesToServiceRef, ServiceFactory } from './types'; export { createServiceRef, createServiceFactory } from './types'; diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index 3edee2ec0f..a7bce3d220 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -116,6 +116,7 @@ export function createServiceRef(options: { }; } +/** @ignore */ type ServiceRefsToInstances< T extends { [key in string]: ServiceRef }, TScope extends 'root' | 'plugin' = 'root' | 'plugin', diff --git a/plugins/catalog-node/src/catalogService.ts b/plugins/catalog-node/src/catalogService.ts index ddfff75462..c6a8fb44f5 100644 --- a/plugins/catalog-node/src/catalogService.ts +++ b/plugins/catalog-node/src/catalogService.ts @@ -31,13 +31,11 @@ export const catalogServiceRef = createServiceRef({ createServiceFactory({ service, deps: { - discoveryFactory: discoveryServiceRef, + discoveryApi: discoveryServiceRef, }, - factory: async ({ discoveryFactory }) => { - const discoveryApi = await discoveryFactory('root'); - const catalogClient = new CatalogClient({ discoveryApi }); - return async _pluginId => { - return catalogClient; + async factory() { + return async ({ discoveryApi }) => { + return new CatalogClient({ discoveryApi }); }; }, }), From 06ad1b16de01f67d529e202e0aed997fd901b964 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 9 Sep 2022 16:15:09 +0200 Subject: [PATCH 28/41] update API reports + fixes Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- packages/backend-plugin-api/api-report.md | 108 ++++++++++++------ .../src/services/definitions/index.ts | 1 + .../src/services/system/types.ts | 5 +- plugins/catalog-node/api-report.md | 2 +- 4 files changed, 77 insertions(+), 39 deletions(-) diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 30e58237af..3344576db8 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -66,10 +66,10 @@ export interface BackendRegistrationPoints { } // @public (undocumented) -export const cacheServiceRef: ServiceRef; +export const cacheServiceRef: ServiceRef; // @public (undocumented) -export const configServiceRef: ServiceRef; +export const configServiceRef: ServiceRef; // @public (undocumented) export function createBackendModule< @@ -105,22 +105,25 @@ export function createExtensionPoint(options: { // @public (undocumented) export function createServiceFactory< TService, + TScope extends 'root' | 'plugin', TImpl extends TService, TDeps extends { - [name in string]: unknown; + [name in string]: ServiceRef; }, TOpts extends | { [name in string]: unknown; } | undefined = undefined, ->(factory: { - service: ServiceRef; - deps: TypesToServiceRef; +>(config: { + service: ServiceRef; + deps: TDeps; factory( - deps: DepsToDepFactories, + deps: ServiceRefsToInstances, options: TOpts, - ): Promise>; + ): TScope extends 'root' + ? Promise + : Promise<(deps: ServiceRefsToInstances) => Promise>; }): undefined extends TOpts ? (options?: TOpts) => ServiceFactory : (options: TOpts) => ServiceFactory; @@ -128,21 +131,26 @@ export function createServiceFactory< // @public (undocumented) export function createServiceRef(options: { id: string; + scope?: 'plugin'; defaultFactory?: ( - service: ServiceRef, + service: ServiceRef, ) => Promise | (() => ServiceFactory)>; -}): ServiceRef; +}): ServiceRef; // @public (undocumented) -export const databaseServiceRef: ServiceRef; +export function createServiceRef(options: { + id: string; + scope: 'root'; + defaultFactory?: ( + service: ServiceRef, + ) => Promise | (() => ServiceFactory)>; +}): ServiceRef; // @public (undocumented) -export type DepsToDepFactories = { - [key in keyof T]: (pluginId: string) => Promise; -}; +export const databaseServiceRef: ServiceRef; // @public (undocumented) -export const discoveryServiceRef: ServiceRef; +export const discoveryServiceRef: ServiceRef; // @public export type ExtensionPoint = { @@ -152,9 +160,6 @@ export type ExtensionPoint = { $$ref: 'extension-point'; }; -// @public (undocumented) -export type FactoryFunc = (pluginId: string) => Promise; - // @public (undocumented) export interface HttpRouterService { // (undocumented) @@ -162,7 +167,7 @@ export interface HttpRouterService { } // @public (undocumented) -export const httpRouterServiceRef: ServiceRef; +export const httpRouterServiceRef: ServiceRef; // @public (undocumented) export interface Logger { @@ -173,7 +178,7 @@ export interface Logger { } // @public (undocumented) -export const loggerServiceRef: ServiceRef; +export const loggerServiceRef: ServiceRef; // @public (undocumented) export function loggerToWinstonLogger( @@ -183,33 +188,66 @@ export function loggerToWinstonLogger( // @public (undocumented) export const permissionsServiceRef: ServiceRef< - PermissionAuthorizer | PermissionEvaluator + PermissionAuthorizer | PermissionEvaluator, + 'plugin' >; // @public (undocumented) -export const schedulerServiceRef: ServiceRef; +export interface PluginMetadata { + // (undocumented) + getId(): string; +} // @public (undocumented) -export type ServiceFactory = { - service: ServiceRef; - deps: { - [key in string]: ServiceRef; - }; - factory(deps: { - [key in string]: unknown; - }): Promise>; -}; +export const pluginMetadataServiceRef: ServiceRef; + +// @public (undocumented) +export const rootLoggerServiceRef: ServiceRef; + +// @public (undocumented) +export const schedulerServiceRef: ServiceRef; + +// @public (undocumented) +export type ServiceFactory = + | { + scope: 'root'; + service: ServiceRef; + deps: { + [key in string]: ServiceRef; + }; + factory(deps: { + [key in string]: unknown; + }): Promise; + } + | { + scope: 'plugin'; + service: ServiceRef; + deps: { + [key in string]: ServiceRef; + }; + factory(deps: { + [key in string]: unknown; + }): Promise< + (deps: { + [key in string]: unknown; + }) => Promise + >; + }; // @public -export type ServiceRef = { +export type ServiceRef< + TService, + TScope extends 'root' | 'plugin' = 'root' | 'plugin', +> = { id: string; - T: T; + scope: TScope; + T: TService; toString(): string; $$ref: 'service'; }; // @public (undocumented) -export const tokenManagerServiceRef: ServiceRef; +export const tokenManagerServiceRef: ServiceRef; // @public (undocumented) export type TypesToServiceRef = { @@ -217,5 +255,5 @@ export type TypesToServiceRef = { }; // @public (undocumented) -export const urlReaderServiceRef: ServiceRef; +export const urlReaderServiceRef: ServiceRef; ``` diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index 44ccb62978..e5f032ef60 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -28,3 +28,4 @@ export { permissionsServiceRef } from './permissionsServiceRef'; export { schedulerServiceRef } from './schedulerServiceRef'; export { rootLoggerServiceRef } from './rootLoggerServiceRef'; export { pluginMetadataServiceRef } from './pluginMetadataServiceRef'; +export type { PluginMetadata } from './pluginMetadataServiceRef'; diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index a7bce3d220..d20fd6fd8f 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -69,9 +69,7 @@ export type ServiceFactory = >; }; -/** - * @public - */ +/** @public */ export function createServiceRef(options: { id: string; scope?: 'plugin'; @@ -79,6 +77,7 @@ export function createServiceRef(options: { service: ServiceRef, ) => Promise | (() => ServiceFactory)>; }): ServiceRef; +/** @public */ export function createServiceRef(options: { id: string; scope: 'root'; diff --git a/plugins/catalog-node/api-report.md b/plugins/catalog-node/api-report.md index 3873e3376d..525516be54 100644 --- a/plugins/catalog-node/api-report.md +++ b/plugins/catalog-node/api-report.md @@ -109,7 +109,7 @@ export type CatalogProcessorResult = | CatalogProcessorRefreshKeysResult; // @alpha -export const catalogServiceRef: ServiceRef; +export const catalogServiceRef: ServiceRef; // @public export type DeferredEntity = { From 409ed984e8e506abc9327cb8381c2a12aa59b5ce Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 9 Sep 2022 18:26:34 +0200 Subject: [PATCH 29/41] changesets: added changesets for scoped services Signed-off-by: Patrik Oldsberg --- .changeset/flat-humans-dance.md | 5 +++++ .changeset/quick-items-invite.md | 5 +++++ .changeset/slow-phones-count.md | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 .changeset/flat-humans-dance.md create mode 100644 .changeset/quick-items-invite.md create mode 100644 .changeset/slow-phones-count.md diff --git a/.changeset/flat-humans-dance.md b/.changeset/flat-humans-dance.md new file mode 100644 index 0000000000..ea7571f5bc --- /dev/null +++ b/.changeset/flat-humans-dance.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Service are now scoped to either `'plugin'` or `'root'` scope. Service factories have been updated to provide dependency instances directly rather than factory functions. diff --git a/.changeset/quick-items-invite.md b/.changeset/quick-items-invite.md new file mode 100644 index 0000000000..33f90cb662 --- /dev/null +++ b/.changeset/quick-items-invite.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-node': patch +--- + +Updated usage of experimental backend service APIs. diff --git a/.changeset/slow-phones-count.md b/.changeset/slow-phones-count.md new file mode 100644 index 0000000000..ce2f9c8432 --- /dev/null +++ b/.changeset/slow-phones-count.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Updated service implementations and backend wiring to support scoped service. From ab014e17d99bf888b4bf571316b8de7e098cb2f3 Mon Sep 17 00:00:00 2001 From: John Kilmister Date: Fri, 9 Sep 2022 20:20:22 +0100 Subject: [PATCH 30/41] Added new docs for plugin feature flags Signed-off-by: John Kilmister --- docs/plugins/feature-flags.md | 64 +++++++++++++++++++++++++++++++++++ mkdocs.yml | 2 +- 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 docs/plugins/feature-flags.md diff --git a/docs/plugins/feature-flags.md b/docs/plugins/feature-flags.md new file mode 100644 index 0000000000..466fdace21 --- /dev/null +++ b/docs/plugins/feature-flags.md @@ -0,0 +1,64 @@ +--- +id: feature-flags +title: Feature Flags +description: Details the process of defining setting and reading a plugin feature flag. +--- + +Backstage offers the ability to define feature flags inside a plugin. This allows you to restrict parts of your plugin to those individual users who have toggled the feature flag to on. + +This page describes the process of defining setting and reading a plugin feature flag. If you are looking for using Feature flags with software templates that can be found under [Writing Templates](https://backstage.io/docs/features/software-templates/writing-templates#remove-sections-or-fields-based-on-feature-flags). + +## Defining a Feature Flag + +Before using a feature flag we must first define it. This is done when we create the plugin by passing the name of the feature flag into the `featureFlags` array. + +```ts +/* src/plugin.ts */ +import { createPlugin, createRouteRef } from '@backstage/core-plugin-api'; +import ExampleComponent from './components/ExampleComponent'; + +export const examplePlugin = createPlugin({ + id: 'example', + routes: { + root: rootRouteRef, + }, + featureFlags: [{ name: 'show-example-feature' }], +}); +``` + +## Enabling Feature Flags + +Feature flags are defaulted to off and can be updated by individual users in the backstage interface. + +These are set by navigating to the page under `Settings` > `Feature Flags`. + +The users selection is saved in the users browsers local storage. Once toggled it may be required for a user to refresh the page to see any new changes. + +## FeatureFlagged Component + +The easiest way to control content based on the state of a feature flag is to use the [FeatureFlagged](https://backstage.io/docs/reference/core-app-api.featureflagged) component. + +```ts +import { FeatureFlagged } from '@backstage/core-app-api' + +... + + + + + + + + +``` + +## Evaluating Feature Flag State + +It is also possible to test the feature flag state using the [FeatureFlags Api](https://backstage.io/docs/reference/core-plugin-api.featureflagsapi). + +```ts +import { useApi, featureFlagsApiRef } from '@backstage/core-plugin-api'; + +const featureFlagsApi = useApi(featureFlagsApiRef); +const isOn = featureFlagsApi.isActive('show-example-feature'); +``` diff --git a/mkdocs.yml b/mkdocs.yml index 7368602298..0d386e63fa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -127,10 +127,10 @@ nav: - Create a Backstage Plugin: 'plugins/create-a-plugin.md' - Plugin Development: 'plugins/plugin-development.md' - Structure of a plugin: 'plugins/structure-of-a-plugin.md' - - Plugin Development: 'plugins/plugin-development.md' - Integrate into the Software Catalog: 'plugins/integrating-plugin-into-software-catalog.md' - Composability System: 'plugins/composability.md' - Plugin Analytics: 'plugins/analytics.md' + - Feature Flags: 'plugins/feature-flags.md' - Backends and APIs: - Proxying: 'plugins/proxying.md' - Backend plugin: 'plugins/backend-plugin.md' From 90aefde6d3e51c4c3b8ef7c234f77fdb913a8447 Mon Sep 17 00:00:00 2001 From: John Kilmister Date: Sat, 10 Sep 2022 12:02:48 +0100 Subject: [PATCH 31/41] Updated case on feature flag text Signed-off-by: John Kilmister --- docs/plugins/feature-flags.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/feature-flags.md b/docs/plugins/feature-flags.md index 466fdace21..a26eb49a9b 100644 --- a/docs/plugins/feature-flags.md +++ b/docs/plugins/feature-flags.md @@ -6,7 +6,7 @@ description: Details the process of defining setting and reading a plugin featur Backstage offers the ability to define feature flags inside a plugin. This allows you to restrict parts of your plugin to those individual users who have toggled the feature flag to on. -This page describes the process of defining setting and reading a plugin feature flag. If you are looking for using Feature flags with software templates that can be found under [Writing Templates](https://backstage.io/docs/features/software-templates/writing-templates#remove-sections-or-fields-based-on-feature-flags). +This page describes the process of defining setting and reading a plugin feature flag. If you are looking for using feature flags with software templates that can be found under [Writing Templates](https://backstage.io/docs/features/software-templates/writing-templates#remove-sections-or-fields-based-on-feature-flags). ## Defining a Feature Flag From 1d2e28c26f826ec0eb0c6c1c0288db0cc738ee6e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 11 Sep 2022 14:25:00 +0200 Subject: [PATCH 32/41] core-app-api: clean up log output in tests Signed-off-by: Patrik Oldsberg --- .../core-app-api/src/routing/RoutingProvider.compat.test.tsx | 3 +-- .../core-app-api/src/routing/RoutingProvider.stable.test.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core-app-api/src/routing/RoutingProvider.compat.test.tsx b/packages/core-app-api/src/routing/RoutingProvider.compat.test.tsx index 1ff80fcacc..ac54780f7c 100644 --- a/packages/core-app-api/src/routing/RoutingProvider.compat.test.tsx +++ b/packages/core-app-api/src/routing/RoutingProvider.compat.test.tsx @@ -251,7 +251,6 @@ describe.each(['beta', 'stable'])('react-router %s', rrVersion => { await new Promise(r => setTimeout(r, 500)); - rendered.debug(); await expect( rendered.findByText('Path at inside: /foo/bar'), ).resolves.toBeInTheDocument(); @@ -344,7 +343,6 @@ describe.each(['beta', 'stable'])('react-router %s', rrVersion => { await expect( rendered.findByText('Path at inside: /foo/blob/baz'), ).resolves.toBeInTheDocument(); - rendered.debug(); }); it('should throw errors for routing to other routeRefs with unsupported parameters', () => { @@ -352,6 +350,7 @@ describe.each(['beta', 'stable'])('react-router %s', rrVersion => { const root = ( + } /> }> { await expect( rendered.findByText('Path at inside: /foo/blob/baz'), ).resolves.toBeInTheDocument(); - rendered.debug(); }); it('should throw errors for routing to other routeRefs with unsupported parameters', () => { const root = ( + } /> }> Date: Sun, 11 Sep 2022 14:32:30 +0200 Subject: [PATCH 33/41] core-plugin-api: clean up log output in tests Signed-off-by: Patrik Oldsberg --- .../src/extensions/extensions.test.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/core-plugin-api/src/extensions/extensions.test.tsx b/packages/core-plugin-api/src/extensions/extensions.test.tsx index dcc92d9d47..0601c01a17 100644 --- a/packages/core-plugin-api/src/extensions/extensions.test.tsx +++ b/packages/core-plugin-api/src/extensions/extensions.test.tsx @@ -61,11 +61,19 @@ describe('extensions', () => { const Component = () =>
; const routeRef = createRouteRef({ id: 'foo' }); - const extension1 = createComponentExtension({ - component: { - sync: Component, - }, + let extension1: ReturnType; + const { warn } = withLogCollector(['warn'], () => { + extension1 = createComponentExtension({ + component: { + sync: Component, + }, + }); }); + expect(warn).toEqual([ + expect.stringMatching( + /^Declaring extensions without name is DEPRECATED. /, + ), + ]); const extension2 = createRoutableExtension({ name: 'Extension2', @@ -73,7 +81,7 @@ describe('extensions', () => { mountPoint: routeRef, }); - const ExtensionComponent1 = plugin.provide(extension1); + const ExtensionComponent1 = plugin.provide(extension1!); const ExtensionComponent2 = plugin.provide(extension2); const element1 = ; From 5ecca7e44b9f5b67dfcd38b0b390c105f24c3e3d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 11 Sep 2022 15:24:37 +0200 Subject: [PATCH 34/41] config-loader: remove logging from remote config watch Signed-off-by: Patrik Oldsberg --- .changeset/empty-colts-whisper.md | 5 +++++ packages/config-loader/src/loader.ts | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 .changeset/empty-colts-whisper.md diff --git a/.changeset/empty-colts-whisper.md b/.changeset/empty-colts-whisper.md new file mode 100644 index 0000000000..1eabe8c58c --- /dev/null +++ b/.changeset/empty-colts-whisper.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +No longer log when reloading remote config. diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts index 9537d2d945..f5ce0eb80c 100644 --- a/packages/config-loader/src/loader.ts +++ b/packages/config-loader/src/loader.ts @@ -287,13 +287,10 @@ export async function loadConfig( let handle: NodeJS.Timeout | undefined; try { handle = setInterval(async () => { - console.info(`Checking for config update`); const newRemoteConfigs = await loadRemoteConfigFiles(); if (await hasConfigChanged(remoteConfigs, newRemoteConfigs)) { remoteConfigs = newRemoteConfigs; - console.info(`Remote config change, reloading config ...`); watchProp.onChange([...remoteConfigs, ...fileConfigs, ...envConfigs]); - console.info(`Remote config reloaded`); } }, remoteProp.reloadIntervalSeconds * 1000); } catch (error) { @@ -303,7 +300,6 @@ export async function loadConfig( if (watchProp.stopSignal) { watchProp.stopSignal.then(() => { if (handle !== undefined) { - console.info(`Stopping remote config watch`); clearInterval(handle); handle = undefined; } From 50d6c7e07e23a8b24be93587a122e69a91aaeaed Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 11 Sep 2022 15:51:31 +0200 Subject: [PATCH 35/41] create-app: clean up log output in tests Signed-off-by: Patrik Oldsberg --- packages/create-app/src/lib/tasks.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index 6a312436bf..d2efe81b90 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -19,6 +19,7 @@ import mockFs from 'mock-fs'; import child_process from 'child_process'; import path from 'path'; import { + Task, buildAppTask, checkAppExistsTask, checkPathExistsTask, @@ -27,6 +28,13 @@ import { templatingTask, } from './tasks'; +jest.spyOn(Task, 'log').mockReturnValue(undefined); +jest.spyOn(Task, 'error').mockReturnValue(undefined); +jest.spyOn(Task, 'section').mockReturnValue(undefined); +jest + .spyOn(Task, 'forItem') + .mockImplementation((_a, _b, taskFunc) => taskFunc()); + jest.mock('child_process'); // By mocking this the filesystem mocks won't mess with reading all of the package.jsons From 3742ccbfc319b4ff5fb2d7ef232c722d650afb91 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 11 Sep 2022 16:52:07 +0200 Subject: [PATCH 36/41] core-components: avoid logging in GaugeCard test Signed-off-by: Patrik Oldsberg --- .../src/components/ProgressBars/GaugeCard.test.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/core-components/src/components/ProgressBars/GaugeCard.test.tsx b/packages/core-components/src/components/ProgressBars/GaugeCard.test.tsx index ea29a288e6..e4d84ca059 100644 --- a/packages/core-components/src/components/ProgressBars/GaugeCard.test.tsx +++ b/packages/core-components/src/components/ProgressBars/GaugeCard.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, withLogCollector } from '@backstage/test-utils'; import { GaugeCard } from './GaugeCard'; @@ -40,7 +40,12 @@ describe('', () => { it('handles invalid numbers', async () => { const badProps = { title: 'Tingle upgrade', progress: 'hejjo' } as any; - const { getByText } = await renderInTestApp(); - expect(getByText(/N\/A.*/)).toBeInTheDocument(); + const { error } = await withLogCollector(async () => { + const { getByText } = await renderInTestApp(); + expect(getByText(/N\/A.*/)).toBeInTheDocument(); + }); + expect(error).toEqual([ + expect.stringMatching(/^Warning: `NaN` is an invalid value/), + ]); }); }); From a1a3a5f16a4ce1822e370673fda2f9e3a7d0a5da Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 11 Sep 2022 16:52:18 +0200 Subject: [PATCH 37/41] core-components: avoid logging in HeaderTabs test Signed-off-by: Patrik Oldsberg --- .../core-components/src/layout/HeaderTabs/HeaderTabs.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.test.tsx b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.test.tsx index 2501785701..f9348ffa9a 100644 --- a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.test.tsx +++ b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.test.tsx @@ -58,6 +58,7 @@ describe('', () => { const TextualBadge = React.forwardRef((props, ref) => ( From 4710e6eeb640edd8f61142a2710bb3020be87255 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 11 Sep 2022 16:54:25 +0200 Subject: [PATCH 38/41] core-components: avoid logging in HomepageTimer test Signed-off-by: Patrik Oldsberg --- .../HomepageTimer/HomepageTimer.test.tsx | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/core-components/src/layout/HomepageTimer/HomepageTimer.test.tsx b/packages/core-components/src/layout/HomepageTimer/HomepageTimer.test.tsx index 3d9dfe70a1..9a7a8ac47d 100644 --- a/packages/core-components/src/layout/HomepageTimer/HomepageTimer.test.tsx +++ b/packages/core-components/src/layout/HomepageTimer/HomepageTimer.test.tsx @@ -14,7 +14,11 @@ * limitations under the License. */ -import { renderWithEffects, TestApiProvider } from '@backstage/test-utils'; +import { + renderWithEffects, + TestApiProvider, + withLogCollector, +} from '@backstage/test-utils'; import { HomepageTimer } from './HomepageTimer'; import React from 'react'; import { lightTheme } from '@backstage/theme'; @@ -35,13 +39,18 @@ it('changes default timezone to GMT', async () => { context: 'test', }); - const rendered = await renderWithEffects( - - - - - , - ); + const { warn } = await withLogCollector(async () => { + const rendered = await renderWithEffects( + + + + + , + ); - expect(rendered.getByText('GMT')).toBeInTheDocument(); + expect(rendered.getByText('GMT')).toBeInTheDocument(); + }); + expect(warn).toEqual([ + 'The timezone America/New_Pork is invalid. Defaulting to GMT', + ]); }); From 87de8ed3f7e6784381f2eab535c3d8399e99c472 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 11 Sep 2022 16:57:35 +0200 Subject: [PATCH 39/41] core-components: fix usage of invalid elements in DependencyGrape tests Signed-off-by: Patrik Oldsberg --- .../components/DependencyGraph/Edge.test.tsx | 27 ++++++++++++++----- .../components/DependencyGraph/Node.test.tsx | 23 +++++++++++----- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/packages/core-components/src/components/DependencyGraph/Edge.test.tsx b/packages/core-components/src/components/DependencyGraph/Edge.test.tsx index 82f0435ebd..db6fbe7552 100644 --- a/packages/core-components/src/components/DependencyGraph/Edge.test.tsx +++ b/packages/core-components/src/components/DependencyGraph/Edge.test.tsx @@ -38,7 +38,7 @@ const id = { const setEdge = jest.fn(); const renderElement = jest.fn((props: RenderLabelProps) => ( - {props.edge.label} +
{props.edge.label}
)); const minProps = { @@ -53,8 +53,7 @@ const edgeWithLabel = { ...edge, label }; describe('', () => { beforeEach(() => { - // jsdom does not support SVG elements so we have to fall back to HTMLUnknownElement - Object.defineProperty(window.HTMLUnknownElement.prototype, 'getBBox', { + Object.defineProperty(window.SVGElement.prototype, 'getBBox', { value: () => ({ width: 100, height: 100 }), configurable: true, }); @@ -63,26 +62,40 @@ describe('', () => { afterEach(jest.clearAllMocks); it('does not render the supplied label element if label is missing', () => { - const { container } = render(); + const { container } = render( + + + , + ); expect(container.getElementsByTagName('g')).toHaveLength(0); }); it('renders the supplied label element if label is present', () => { - const { getByText } = render(); + const { getByText } = render( + + + , + ); expect(getByText(label)).toBeInTheDocument(); }); it('passes down edge properties to the render method if label is present', () => { const edgeWithRandomProp = { ...edge, label, randomProp: true }; render( - , + + + , ); expect(renderElement).toHaveBeenCalledWith({ edge: edgeWithRandomProp }); }); it('calls setEdge with edge ID and actual label size after rendering', () => { - const { getByText } = render(); + const { getByText } = render( + + + , + ); expect(getByText(label)).toBeInTheDocument(); // Updates the edge in the graph diff --git a/packages/core-components/src/components/DependencyGraph/Node.test.tsx b/packages/core-components/src/components/DependencyGraph/Node.test.tsx index aaba09c004..ea69220336 100644 --- a/packages/core-components/src/components/DependencyGraph/Node.test.tsx +++ b/packages/core-components/src/components/DependencyGraph/Node.test.tsx @@ -23,7 +23,7 @@ import { RenderNodeProps } from './types'; const node = { id: 'abc', x: 0, y: 0, width: 0, height: 0 }; const setNode = jest.fn(() => new dagre.graphlib.Graph()); const renderElement = jest.fn((props: RenderNodeProps) => ( - {props.node.id} +
{props.node.id}
)); const minProps = { @@ -34,8 +34,7 @@ const minProps = { describe('', () => { beforeEach(() => { - // jsdom does not support SVG elements so we have to fall back to HTMLUnknownElement - Object.defineProperty(window.HTMLUnknownElement.prototype, 'getBBox', { + Object.defineProperty(window.SVGElement.prototype, 'getBBox', { value: () => ({ width: 100, height: 100 }), configurable: true, }); @@ -44,19 +43,31 @@ describe('', () => { afterEach(jest.clearAllMocks); it('renders the supplied element', () => { - const { getByText } = render(); + const { getByText } = render( + + + , + ); expect(getByText(minProps.node.id)).toBeInTheDocument(); }); it('passes down node properties to the render method', () => { const nodeWithRandomProp = { ...node, randomProp: true }; - render(); + render( + + + , + ); expect(renderElement).toHaveBeenCalledWith({ node: nodeWithRandomProp }); }); it('calls setNode with node ID and actual size after rendering', () => { - const { getByText } = render(); + const { getByText } = render( + + + , + ); expect(getByText(minProps.node.id)).toBeInTheDocument(); // Updates the node in the graph From d9ab14d04cfa73e14882d50fe95c95e92e31c4d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Tunkl?= Date: Mon, 12 Sep 2022 14:05:34 +0200 Subject: [PATCH 40/41] Update of humanize method parameters type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomáš Tunkl --- plugins/catalog-react/api-report.md | 2 +- plugins/catalog-react/src/components/EntityRefLink/humanize.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index b2b098ec68..f6ead9a31a 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -441,7 +441,7 @@ export function humanizeEntityRef( entityRef: Entity | CompoundEntityRef, opts?: { defaultKind?: string; - defaultNamespace?: string | boolean; + defaultNamespace?: string | false; }, ): string; diff --git a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts index 358b6a5851..bfc8e2af84 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts @@ -29,7 +29,7 @@ export function humanizeEntityRef( entityRef: Entity | CompoundEntityRef, opts?: { defaultKind?: string; - defaultNamespace?: string | boolean; + defaultNamespace?: string | false; }, ) { const defaultKind = opts?.defaultKind; From 07fa501761e432effe6148b43cf49ed91f6a23e5 Mon Sep 17 00:00:00 2001 From: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Date: Mon, 12 Sep 2022 15:12:50 -0500 Subject: [PATCH 41/41] Minor updates to the Pemissions docs Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> --- docs/permissions/custom-rules.md | 4 +++- docs/permissions/getting-started.md | 5 +---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/permissions/custom-rules.md b/docs/permissions/custom-rules.md index 0ff0bc230c..1408d732d3 100644 --- a/docs/permissions/custom-rules.md +++ b/docs/permissions/custom-rules.md @@ -49,6 +49,8 @@ The api for providing custom rules may differ between plugins, but there should // packages/backend/src/plugins/catalog.ts import { isInSystemRule } from './permission'; +// The CatalogBuilder with the addPermissionRules function is in the alpha path +import { CatalogBuilder } from '@backstage/plugin-catalog-backend/alpha'; ... @@ -56,7 +58,7 @@ export default async function createPlugin( env: PluginEnvironment, ): Promise { const builder = await CatalogBuilder.create(env); - builder.addPermissionRules(isInSystem); + builder.addPermissionRules(isInSystemRule); ... return router; } diff --git a/docs/permissions/getting-started.md b/docs/permissions/getting-started.md index 6daf448cdd..6e6bcd2d03 100644 --- a/docs/permissions/getting-started.md +++ b/docs/permissions/getting-started.md @@ -77,10 +77,7 @@ export default async function createPlugin( logger: env.logger, discovery: env.discovery, policy: new TestPermissionPolicy(), - identity: IdentityClient.create({ - discovery: env.discovery, - issuer: await env.discovery.getExternalBaseUrl('auth'), - }), + identity: env.identity, }); } ```