From d1191ee2d76a95a25bebe3860ca564032a6b3378 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 1 Aug 2024 14:52:04 +0200 Subject: [PATCH 01/18] feat: added a new PageBlueprint Signed-off-by: blam Signed-off-by: blam --- .../src/extensions/PageBlueprint.test.tsx | 154 ++++++++++++++++++ .../src/extensions/PageBlueprint.tsx | 71 ++++++++ 2 files changed, 225 insertions(+) create mode 100644 packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx create mode 100644 packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx diff --git a/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx new file mode 100644 index 0000000000..abe2d96a85 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx @@ -0,0 +1,154 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { createRouteRef } from '../routing'; +import { PageBlueprint } from './PageBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { + coreExtensionData, + createExtensionBlueprint, + createExtensionInput, +} from '../wiring'; +import { waitFor } from '@testing-library/react'; + +describe('PageBlueprint', () => { + const mockRouteRef = createRouteRef(); + + it('should return an extension when calling make with sensible defaults', () => { + const myPage = PageBlueprint.make({ + name: 'test-page', + params: { + loader: () => Promise.resolve(
Test
), + defaultPath: '/test', + routeRef: mockRouteRef, + }, + }); + + expect(myPage).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/routes", + "input": "routes", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "page", + "name": "test-page", + "namespace": undefined, + "output": [ + [Function], + [Function], + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "core.routing.ref", + "optional": [Function], + "toString": [Function], + }, + ], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should output a routeRef with the input routeRef', async () => { + const myPage = PageBlueprint.make({ + name: 'test-page', + params: { + loader: () => Promise.resolve(
Test
), + defaultPath: '/test', + routeRef: mockRouteRef, + }, + }); + + const tester = createExtensionTester(myPage); + + // TODO(blam): test for the routePath output doesn't work. + // expect(tester.data(coreExtensionData.routePath)).toBe('/test'); + + expect(tester.data(coreExtensionData.routeRef)).toBe(mockRouteRef); + + const { getByTestId } = tester.render(); + + await waitFor(() => expect(getByTestId('test')).toBeInTheDocument()); + }); + + it('should allow defining additional inputs to the extension', async () => { + const myPage = PageBlueprint.make({ + name: 'test-page', + params: { + loader: async ({ inputs }) => { + return ( +
+ {/* todo(blam): need to fix the typescript here, as inputs is not the right type */} + {inputs.cards.map(c => c.get(coreExtensionData.reactElement))} +
+ ); + }, + defaultPath: '/test', + routeRef: mockRouteRef, + }, + inputs: { + cards: createExtensionInput([coreExtensionData.reactElement], { + optional: false, + singleton: false, + }), + }, + }); + + const CardBlueprint = createExtensionBlueprint({ + kind: 'card', + attachTo: { id: 'page:test-page', input: 'cards' }, + output: [coreExtensionData.reactElement], + factory() { + return [ + coreExtensionData.reactElement( +
I'm a lovely card
, + ), + ]; + }, + }); + + const tester = createExtensionTester(myPage).add( + CardBlueprint.make({ name: 'card', params: {} }), + ); + + const { getByTestId, getByText } = tester.render(); + + await waitFor(() => expect(getByTestId('card')).toBeInTheDocument()); + await waitFor(() => + expect(getByText("I'm a lovely card")).toBeInTheDocument(), + ); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx new file mode 100644 index 0000000000..7ae26cf387 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx @@ -0,0 +1,71 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { lazy } from 'react'; +import { RouteRef } from '../routing'; +import { coreExtensionData, createExtensionBlueprint } from '../wiring'; +import { ExtensionBoundary } from '../components'; + +export const PageBlueprint = createExtensionBlueprint({ + kind: 'page', + attachTo: { id: 'app/routes', input: 'routes' }, + output: [ + coreExtensionData.routePath, + coreExtensionData.reactElement, + coreExtensionData.routeRef.optional(), + ], + config: { + schema: { + path: z => z.string().optional(), + }, + }, + factory( + { + defaultPath, + loader, + routeRef, + }: { + defaultPath?: string; + loader: (opts: { + config: typeof config; + inputs: typeof inputs; + }) => Promise; + routeRef?: RouteRef; + }, + { config, inputs, node }, + ) { + const ExtensionComponent = lazy(() => + loader({ config, inputs }).then(element => ({ default: () => element })), + ); + + // TODO(blam): this is a little awkward for optional returns. + // I wonder if we should be using generators or yield instead + // for a better API here. + const outputs = [ + coreExtensionData.routePath(config.path ?? defaultPath!), + coreExtensionData.reactElement( + + + , + ), + ]; + + if (routeRef) { + return [...outputs, coreExtensionData.routeRef(routeRef)]; + } + + return outputs; + }, +}); From 8897c29ecc13ec2aa70dec12baeb6b944f33aad8 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 1 Aug 2024 15:46:39 +0200 Subject: [PATCH 02/18] chore: migrate ThemeCreator Signed-off-by: blam --- .../src/extensions/ThemeBlueprint.test.ts | 64 +++++++++++++++++++ .../src/extensions/ThemeBlueprint.ts | 32 ++++++++++ 2 files changed, 96 insertions(+) create mode 100644 packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts create mode 100644 packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts diff --git a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts new file mode 100644 index 0000000000..d3d05a5e68 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright 2024 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 { AppTheme } from '@backstage/core-plugin-api'; +import { ThemeBlueprint } from './ThemeBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; + +describe('ThemeBlueprint', () => { + const theme = { + id: 'light', + colors: { primary: 'blue' }, + variant: 'dark', + title: 'lols', + Provider: (_: { children: React.ReactNode }) => null, + } as AppTheme; + + it('should create an extension with sensible defaults', () => { + expect( + // todo(blam): we can't inject theme.id as the name here like the old extension creator. + // Wonder if theres a better solution. + ThemeBlueprint.make({ name: 'blob', params: { theme } }), + ).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app", + "input": "themes", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "theme", + "name": "blob", + "namespace": "app", + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should return the theme as an themeDataRef', async () => { + const extension = ThemeBlueprint.make({ name: 'blob', params: { theme } }); + + expect( + createExtensionTester(extension).data(ThemeBlueprint.dataRefs.theme), + ).toEqual(theme); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts new file mode 100644 index 0000000000..4ddb5a298a --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2024 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 { AppTheme } from '@backstage/core-plugin-api'; +import { createExtensionBlueprint } from '../wiring'; +import { createThemeExtension } from './createThemeExtension'; + +export const ThemeBlueprint = createExtensionBlueprint({ + kind: 'theme', + namespace: 'app', + attachTo: { id: 'app', input: 'themes' }, + output: [createThemeExtension.themeDataRef], + dataRefs: { + theme: createThemeExtension.themeDataRef, + }, + factory: ({ theme }: { theme: AppTheme }) => [ + createThemeExtension.themeDataRef(theme), + ], +}); From da75ca4e4702b057b8c3bd23bd6e6e514b8a8e77 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 1 Aug 2024 16:21:57 +0200 Subject: [PATCH 03/18] chore: migration translation extension creator Signed-off-by: blam --- .../extensions/TranslationBlueprint.test.ts | 81 +++++++++++++++++++ .../src/extensions/TranslationBlueprint.ts | 33 ++++++++ 2 files changed, 114 insertions(+) create mode 100644 packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts create mode 100644 packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts diff --git a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts new file mode 100644 index 0000000000..fe7aeba59d --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright 2024 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 { createExtensionTester } from '@backstage/frontend-test-utils'; +import { + createTranslationMessages, + createTranslationRef, +} from '../translation'; +import { TranslationBlueprint } from './TranslationBlueprint'; + +describe('TranslationBlueprint', () => { + const translationRef = createTranslationRef({ + id: 'test', + messages: { + test: 'test', + }, + }); + + const messages = createTranslationMessages({ + ref: translationRef, + messages: { + test: 'test2', + }, + }); + + it('should return an extension instance with sane defaults', () => { + expect( + TranslationBlueprint.make({ + params: { + resource: messages, + }, + }), + ).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app", + "input": "translations", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "translation", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should output a translation data ref', () => { + const extension = TranslationBlueprint.make({ + params: { + resource: messages, + }, + }); + + expect( + createExtensionTester(extension).data( + TranslationBlueprint.dataRefs.translation, + ), + ).toBe(messages); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts new file mode 100644 index 0000000000..23cb5da6a6 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2024 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 { createExtensionBlueprint } from '../wiring'; +import { createTranslationExtension } from './createTranslationExtension'; +import { TranslationMessages, TranslationResource } from '../translation'; + +export const TranslationBlueprint = createExtensionBlueprint({ + kind: 'translation', + attachTo: { id: 'app', input: 'translations' }, + output: [createTranslationExtension.translationDataRef], + dataRefs: { + translation: createTranslationExtension.translationDataRef, + }, + factory: ({ + resource, + }: { + resource: TranslationResource | TranslationMessages; + }) => [createTranslationExtension.translationDataRef(resource)], +}); From 48b4304c46a62052ad2245b52def395ca0256aee Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 1 Aug 2024 16:28:33 +0200 Subject: [PATCH 04/18] chore: fix Signed-off-by: blam Signed-off-by: blam --- .../src/extensions/TranslationBlueprint.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts index fe7aeba59d..658c0c9709 100644 --- a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts @@ -38,6 +38,10 @@ describe('TranslationBlueprint', () => { it('should return an extension instance with sane defaults', () => { expect( TranslationBlueprint.make({ + // todo(blam): we can't set the namespace dynamically based of the ResourceType. + // work out if we should wrap this up or another solution. + namespace: messages.id, + name: 'test', params: { resource: messages, }, @@ -54,8 +58,8 @@ describe('TranslationBlueprint', () => { "factory": [Function], "inputs": {}, "kind": "translation", - "name": undefined, - "namespace": undefined, + "name": "test", + "namespace": "test", "output": [ [Function], ], From 1f4029b9d877885db10ba63a57afee368bb906a5 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 08:50:47 +0200 Subject: [PATCH 05/18] chore: refactor a little bit and use some new features to remove todos Signed-off-by: blam --- .../src/extensions/PageBlueprint.test.tsx | 4 ++-- .../src/extensions/PageBlueprint.tsx | 23 +++++++------------ .../src/extensions/ThemeBlueprint.test.ts | 10 +++----- .../src/extensions/ThemeBlueprint.ts | 1 + .../extensions/TranslationBlueprint.test.ts | 12 ++++------ .../src/extensions/TranslationBlueprint.ts | 1 + 6 files changed, 20 insertions(+), 31 deletions(-) diff --git a/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx index abe2d96a85..e020167f43 100644 --- a/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx @@ -94,7 +94,7 @@ describe('PageBlueprint', () => { const tester = createExtensionTester(myPage); - // TODO(blam): test for the routePath output doesn't work. + // TODO(blam): test for the routePath output doesn't work, due to the the way the test harness works // expect(tester.data(coreExtensionData.routePath)).toBe('/test'); expect(tester.data(coreExtensionData.routeRef)).toBe(mockRouteRef); @@ -111,7 +111,6 @@ describe('PageBlueprint', () => { loader: async ({ inputs }) => { return (
- {/* todo(blam): need to fix the typescript here, as inputs is not the right type */} {inputs.cards.map(c => c.get(coreExtensionData.reactElement))}
); @@ -119,6 +118,7 @@ describe('PageBlueprint', () => { defaultPath: '/test', routeRef: mockRouteRef, }, + /* todo(blam): need to fix the typescript here, as inputs is not the right type, wont let me merge without specifying parent opts */ inputs: { cards: createExtensionInput([coreExtensionData.reactElement], { optional: false, diff --git a/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx index 7ae26cf387..4ee26321b7 100644 --- a/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx @@ -31,7 +31,7 @@ export const PageBlueprint = createExtensionBlueprint({ path: z => z.string().optional(), }, }, - factory( + *factory( { defaultPath, loader, @@ -50,22 +50,15 @@ export const PageBlueprint = createExtensionBlueprint({ loader({ config, inputs }).then(element => ({ default: () => element })), ); - // TODO(blam): this is a little awkward for optional returns. - // I wonder if we should be using generators or yield instead - // for a better API here. - const outputs = [ - coreExtensionData.routePath(config.path ?? defaultPath!), - coreExtensionData.reactElement( - - - , - ), - ]; + yield coreExtensionData.routePath(config.path ?? defaultPath!); + yield coreExtensionData.reactElement( + + + , + ); if (routeRef) { - return [...outputs, coreExtensionData.routeRef(routeRef)]; + yield coreExtensionData.routeRef(routeRef); } - - return outputs; }, }); diff --git a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts index d3d05a5e68..5bc3a8f7c3 100644 --- a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts @@ -27,11 +27,7 @@ describe('ThemeBlueprint', () => { } as AppTheme; it('should create an extension with sensible defaults', () => { - expect( - // todo(blam): we can't inject theme.id as the name here like the old extension creator. - // Wonder if theres a better solution. - ThemeBlueprint.make({ name: 'blob', params: { theme } }), - ).toMatchInlineSnapshot(` + expect(ThemeBlueprint.make({ params: { theme } })).toMatchInlineSnapshot(` { "$$type": "@backstage/ExtensionDefinition", "attachTo": { @@ -43,7 +39,7 @@ describe('ThemeBlueprint', () => { "factory": [Function], "inputs": {}, "kind": "theme", - "name": "blob", + "name": "light", "namespace": "app", "output": [ [Function], @@ -55,7 +51,7 @@ describe('ThemeBlueprint', () => { }); it('should return the theme as an themeDataRef', async () => { - const extension = ThemeBlueprint.make({ name: 'blob', params: { theme } }); + const extension = ThemeBlueprint.make({ params: { theme } }); expect( createExtensionTester(extension).data(ThemeBlueprint.dataRefs.theme), diff --git a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts index 4ddb5a298a..500946713b 100644 --- a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts +++ b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts @@ -21,6 +21,7 @@ import { createThemeExtension } from './createThemeExtension'; export const ThemeBlueprint = createExtensionBlueprint({ kind: 'theme', namespace: 'app', + name: ({ theme }) => theme.id, attachTo: { id: 'app', input: 'themes' }, output: [createThemeExtension.themeDataRef], dataRefs: { diff --git a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts index 658c0c9709..a059189044 100644 --- a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts @@ -22,7 +22,7 @@ import { TranslationBlueprint } from './TranslationBlueprint'; describe('TranslationBlueprint', () => { const translationRef = createTranslationRef({ - id: 'test', + id: 'translationRefId', messages: { test: 'test', }, @@ -38,10 +38,7 @@ describe('TranslationBlueprint', () => { it('should return an extension instance with sane defaults', () => { expect( TranslationBlueprint.make({ - // todo(blam): we can't set the namespace dynamically based of the ResourceType. - // work out if we should wrap this up or another solution. - namespace: messages.id, - name: 'test', + name: 'blob', params: { resource: messages, }, @@ -58,8 +55,8 @@ describe('TranslationBlueprint', () => { "factory": [Function], "inputs": {}, "kind": "translation", - "name": "test", - "namespace": "test", + "name": "blob", + "namespace": "translationRefId", "output": [ [Function], ], @@ -71,6 +68,7 @@ describe('TranslationBlueprint', () => { it('should output a translation data ref', () => { const extension = TranslationBlueprint.make({ + name: 'blob', params: { resource: messages, }, diff --git a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts index 23cb5da6a6..581f4d176a 100644 --- a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts +++ b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts @@ -20,6 +20,7 @@ import { TranslationMessages, TranslationResource } from '../translation'; export const TranslationBlueprint = createExtensionBlueprint({ kind: 'translation', + namespace: ({ resource }) => resource.id, attachTo: { id: 'app', input: 'translations' }, output: [createTranslationExtension.translationDataRef], dataRefs: { From 593fdd4e007c072c37a7d3ba94734dad0ad115b0 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 09:14:05 +0200 Subject: [PATCH 06/18] chore: added SignInPageBlueprint Signed-off-by: blam --- .../extensions/SignInPageBlueprint.test.tsx | 67 +++++++++++++++++++ .../src/extensions/SignInPageBlueprint.tsx | 50 ++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx create mode 100644 packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx diff --git a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx new file mode 100644 index 0000000000..7bbeb4e5d7 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx @@ -0,0 +1,67 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { SignInPageBlueprint } from './SignInPageBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { waitFor } from '@testing-library/react'; + +describe('SignInPageBlueprint', () => { + it('should create an extension with sensible defaults', () => { + expect( + SignInPageBlueprint.make({ + params: { loader: async () => () =>
}, + }), + ).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/root", + "input": "signInPage", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "sign-in-page", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should return the component as the componentRef', async () => { + const MockSignInPage = () =>
MockSignInPage
; + + const extension = SignInPageBlueprint.make({ + params: { loader: async () => () => }, + }); + + const tester = createExtensionTester(extension); + expect(tester.data(SignInPageBlueprint.dataRefs.component)).toBeDefined(); + + const { getByText } = tester.render(); + + // todo(blam): need a better way to test this, currently fails. + await waitFor(() => { + expect(getByText('MockSignInPage')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx new file mode 100644 index 0000000000..01ba174efe --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx @@ -0,0 +1,50 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { ComponentType, lazy } from 'react'; +import { createExtensionBlueprint } from '../wiring'; +import { createSignInPageExtension } from './createSignInPageExtension'; +import { SignInPageProps } from '@backstage/core-plugin-api'; +import { ExtensionBoundary } from '../components'; + +export const SignInPageBlueprint = createExtensionBlueprint({ + kind: 'sign-in-page', + attachTo: { id: 'app/root', input: 'signInPage' }, + output: [createSignInPageExtension.componentDataRef], + dataRefs: { + component: createSignInPageExtension.componentDataRef, + }, + *factory( + { + loader, + }: { + loader: (opts: { + config: typeof config; + inputs: typeof inputs; + }) => Promise>; + }, + { config, inputs, node }, + ) { + const ExtensionComponent = lazy(() => + loader({ config, inputs }).then(component => ({ default: component })), + ); + + yield createSignInPageExtension.componentDataRef(props => ( + + + + )); + }, +}); From ac136771b2ca4331b6f54f32cea3657d9b52f827 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 09:34:12 +0200 Subject: [PATCH 07/18] feat: added RouterBlueprint Signed-off-by: blam --- .../src/extensions/RouterBlueprint.test.tsx | 167 ++++++++++++++++++ .../src/extensions/RouterBlueprint.tsx | 45 +++++ 2 files changed, 212 insertions(+) create mode 100644 packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx create mode 100644 packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx diff --git a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx new file mode 100644 index 0000000000..3abc967fd8 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx @@ -0,0 +1,167 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { RouterBlueprint } from './RouterBlueprint'; +import { MemoryRouter } from 'react-router-dom'; +import { render, waitFor } from '@testing-library/react'; +import { createSpecializedApp } from '@backstage/frontend-app-api'; +import { + coreExtensionData, + createExtension, + createExtensionInput, + createExtensionOverrides, +} from '../wiring'; +import { MockConfigApi } from '@backstage/test-utils'; +import { PageBlueprint } from './PageBlueprint'; + +describe('RouterBlueprint', () => { + it('should return an extension when calling make with sensible defaults', () => { + const extension = RouterBlueprint.make({ + params: { + Component: props =>
{props.children}
, + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/root", + "input": "router", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "app-router-component", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should work with simple options', async () => { + const extension = RouterBlueprint.make({ + namespace: 'test', + params: { + Component: ({ children }) => ( + +
{children}
+
+ ), + }, + }); + + const app = createSpecializedApp({ + features: [ + createExtensionOverrides({ + extensions: [ + extension, + PageBlueprint.make({ + namespace: 'test', + params: { + defaultPath: '/', + loader: async () =>
, + }, + }), + ], + }), + ], + }); + + const { getByTestId } = render(app.createRoot()); + + await waitFor(() => { + expect(getByTestId('test-contents')).toBeInTheDocument(); + expect(getByTestId('test-router')).toBeInTheDocument(); + }); + }); + + it('should work with complex options and props', async () => { + const extension = RouterBlueprint.make({ + namespace: 'test', + name: 'test', + config: { + schema: { + name: z => z.string(), + }, + }, + inputs: { + children: createExtensionInput([coreExtensionData.reactElement]), + }, + params: { + Component: ({ inputs, children, config }) => ( + +
+ {children} +
+
+ ), + }, + }); + + const app = createSpecializedApp({ + features: [ + createExtensionOverrides({ + extensions: [ + extension, + createExtension({ + namespace: 'test', + attachTo: { + id: 'app-router-component:test/test', + input: 'children', + }, + output: [coreExtensionData.reactElement], + *factory() { + yield coreExtensionData.reactElement(
); + }, + }), + PageBlueprint.make({ + namespace: 'test', + params: { + defaultPath: '/', + loader: async () =>
, + }, + }), + ], + }), + ], + config: new MockConfigApi({ + app: { + extensions: [ + { + 'app-router-component:test/test': { config: { name: 'Robin' } }, + }, + ], + }, + }), + }); + + const { getByTestId } = render(app.createRoot()); + + await waitFor(() => { + expect(getByTestId('test-contents')).toBeInTheDocument(); + expect(getByTestId('test-router-Robin-1')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx new file mode 100644 index 0000000000..9e9cddbdd1 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx @@ -0,0 +1,45 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { ComponentType, PropsWithChildren } from 'react'; +import { createExtensionBlueprint } from '../wiring'; +import { createRouterExtension } from './createRouterExtension'; + +export const RouterBlueprint = createExtensionBlueprint({ + kind: 'app-router-component', + attachTo: { id: 'app/root', input: 'router' }, + output: [createRouterExtension.componentDataRef], + *factory( + { + Component, + }: { + Component: ComponentType< + PropsWithChildren<{ + inputs: typeof inputs; + config: typeof config; + }> + >; + }, + { config, inputs }, + ) { + const Wrapper = (props: PropsWithChildren<{}>) => ( + + {props.children} + + ); + + yield createRouterExtension.componentDataRef(Wrapper); + }, +}); From b7506f214ea83a5e833261de20e30ec8e92bbe7b Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 09:53:02 +0200 Subject: [PATCH 08/18] feat: added NavLogoBlueprint Signed-off-by: blam --- .../src/extensions/NavLogoBlueprint.test.tsx | 71 +++++++++++++++++++ .../src/extensions/NavLogoBlueprint.ts | 39 ++++++++++ .../src/extensions/RouterBlueprint.tsx | 3 + .../extensions/SignInPageBlueprint.test.tsx | 6 +- 4 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx create mode 100644 packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.ts diff --git a/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx new file mode 100644 index 0000000000..fa01862785 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { NavLogoBlueprint } from './NavLogoBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; + +describe('NavLogoBlueprint', () => { + it('should create an extension with sensible defaults', () => { + const extension = NavLogoBlueprint.make({ + params: { + logoFull:
Logo Full
, + logoIcon:
Logo Icon
, + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/nav", + "input": "logos", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "nav-logo", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should return a valid component ref', () => { + const logoFull =
Logo Full
; + const logoIcon =
Logo Icon
; + + const extension = NavLogoBlueprint.make({ + name: 'test', + params: { + logoFull, + logoIcon, + }, + }); + + const tester = createExtensionTester(extension); + + expect(tester.data(NavLogoBlueprint.dataRefs.logoElements)).toEqual({ + logoFull, + logoIcon, + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.ts b/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.ts new file mode 100644 index 0000000000..d119287f29 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2024 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 { createExtensionBlueprint } from '../wiring'; +import { createNavLogoExtension } from './createNavLogoExtension'; + +export const NavLogoBlueprint = createExtensionBlueprint({ + kind: 'nav-logo', + attachTo: { id: 'app/nav', input: 'logos' }, + output: [createNavLogoExtension.logoElementsDataRef], + dataRefs: { + logoElements: createNavLogoExtension.logoElementsDataRef, + }, + *factory({ + logoIcon, + logoFull, + }: { + logoIcon: JSX.Element; + logoFull: JSX.Element; + }) { + yield createNavLogoExtension.logoElementsDataRef({ + logoIcon, + logoFull, + }); + }, +}); diff --git a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx index 9e9cddbdd1..bf910eb944 100644 --- a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx +++ b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx @@ -21,6 +21,9 @@ export const RouterBlueprint = createExtensionBlueprint({ kind: 'app-router-component', attachTo: { id: 'app/root', input: 'router' }, output: [createRouterExtension.componentDataRef], + dataRefs: { + component: createRouterExtension.componentDataRef, + }, *factory( { Component, diff --git a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx index 7bbeb4e5d7..11fe1420f1 100644 --- a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx @@ -48,7 +48,7 @@ describe('SignInPageBlueprint', () => { }); it('should return the component as the componentRef', async () => { - const MockSignInPage = () =>
MockSignInPage
; + const MockSignInPage = () =>
; const extension = SignInPageBlueprint.make({ params: { loader: async () => () => }, @@ -57,11 +57,11 @@ describe('SignInPageBlueprint', () => { const tester = createExtensionTester(extension); expect(tester.data(SignInPageBlueprint.dataRefs.component)).toBeDefined(); - const { getByText } = tester.render(); + const { getByTestId } = tester.render(); // todo(blam): need a better way to test this, currently fails. await waitFor(() => { - expect(getByText('MockSignInPage')).toBeInTheDocument(); + expect(getByTestId('mock-sign-in')).toBeInTheDocument(); }); }); }); From b284ecb010272dd725ad043e36cf39e1136e3793 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 11:33:26 +0200 Subject: [PATCH 09/18] chore: reworking how to override the page component Signed-off-by: blam --- .../src/extensions/NavItemBlueprint.ts | 51 +++++++++++++++++++ .../src/extensions/PageBlueprint.test.tsx | 23 ++++----- 2 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 packages/frontend-plugin-api/src/extensions/NavItemBlueprint.ts diff --git a/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.ts b/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.ts new file mode 100644 index 0000000000..568431c0f5 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2024 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 { IconComponent } from '@backstage/core-plugin-api'; +import { RouteRef } from '../routing'; +import { createExtensionBlueprint } from '../wiring'; +import { createNavItemExtension } from './createNavItemExtension'; + +export const NavItemBlueprint = createExtensionBlueprint({ + kind: 'nav-item', + attachTo: { id: 'app/nav', input: 'items' }, + output: [createNavItemExtension.targetDataRef], + dataRefs: { + target: createNavItemExtension.targetDataRef, + }, + factory: ( + { + icon, + routeRef, + }: { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }, + { config }, + ) => [ + createNavItemExtension.targetDataRef({ + title: config.title, + icon, + routeRef, + }), + ], + config: { + schema: ({ title }) => ({ + title: z => z.string().default(title), + }), + }, +}); diff --git a/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx index e020167f43..5e1d6b7601 100644 --- a/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx @@ -107,24 +107,23 @@ describe('PageBlueprint', () => { it('should allow defining additional inputs to the extension', async () => { const myPage = PageBlueprint.make({ name: 'test-page', - params: { - loader: async ({ inputs }) => { - return ( -
- {inputs.cards.map(c => c.get(coreExtensionData.reactElement))} -
- ); - }, - defaultPath: '/test', - routeRef: mockRouteRef, - }, - /* todo(blam): need to fix the typescript here, as inputs is not the right type, wont let me merge without specifying parent opts */ inputs: { cards: createExtensionInput([coreExtensionData.reactElement], { optional: false, singleton: false, }), }, + factory(originalFactory, { inputs }) { + return originalFactory({ + loader: async () => ( +
+ {inputs.cards.map(c => c.get(coreExtensionData.reactElement))} +
+ ), + defaultPath: '/test', + routeRef: mockRouteRef, + }); + }, }); const CardBlueprint = createExtensionBlueprint({ From 60d1832ce3138cd0e2769f8feaeb33940596ae6d Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 13:23:11 +0200 Subject: [PATCH 10/18] chore: added NavItemBlueprint Signed-off-by: blam --- .../src/extensions/NavItemBlueprint.test.tsx | 106 ++++++++++++++++++ .../extensions/SignInPageBlueprint.test.tsx | 2 +- 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx diff --git a/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx new file mode 100644 index 0000000000..ba7643eee7 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx @@ -0,0 +1,106 @@ +/* + * Copyright 2024 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 { createExtensionTester } from '@backstage/frontend-test-utils'; +import { createRouteRef } from '../routing'; +import { NavItemBlueprint } from './NavItemBlueprint'; + +describe('NavItemBlueprint', () => { + const mockRouteRef = createRouteRef(); + const MockIcon = () => null; + + it('should return an extension with sensible defaults', () => { + const extension = NavItemBlueprint.make({ + params: { + icon: MockIcon, + routeRef: mockRouteRef, + title: 'TEST', + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/nav", + "input": "items", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "title": { + "default": "TEST", + "type": "string", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "nav-item", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should return the correct extension data', () => { + const extension = NavItemBlueprint.make({ + params: { + icon: MockIcon, + routeRef: mockRouteRef, + title: 'TEST', + }, + }); + + const tester = createExtensionTester(extension); + + expect(tester.data(NavItemBlueprint.dataRefs.target)).toEqual({ + title: 'TEST', + icon: MockIcon, + routeRef: mockRouteRef, + }); + }); + + it('should allow overriding of the title using config', () => { + const extension = NavItemBlueprint.make({ + params: { + icon: MockIcon, + routeRef: mockRouteRef, + title: 'TEST', + }, + }); + + const tester = createExtensionTester(extension, { + config: { title: 'OVERRIDDEN' }, + }); + + expect(tester.data(NavItemBlueprint.dataRefs.target)).toEqual({ + title: 'OVERRIDDEN', + icon: MockIcon, + routeRef: mockRouteRef, + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx index 11fe1420f1..5ffdd3175b 100644 --- a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx @@ -48,7 +48,7 @@ describe('SignInPageBlueprint', () => { }); it('should return the component as the componentRef', async () => { - const MockSignInPage = () =>
; + const MockSignInPage = () =>
; const extension = SignInPageBlueprint.make({ params: { loader: async () => () => }, From 8d5049b5a5515ec4415fac6d7c4847fe6107f1b2 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 7 Aug 2024 13:44:04 +0200 Subject: [PATCH 11/18] chore: wip Signed-off-by: blam --- .../src/extensions/ComponentBlueprint.tsx | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 packages/frontend-plugin-api/src/extensions/ComponentBlueprint.tsx diff --git a/packages/frontend-plugin-api/src/extensions/ComponentBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/ComponentBlueprint.tsx new file mode 100644 index 0000000000..614196fc09 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/ComponentBlueprint.tsx @@ -0,0 +1,69 @@ +/* + * Copyright 2024 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 { ComponentRef } from '../components'; +import { createExtensionBlueprint } from '../wiring'; +import { createComponentExtension } from './createComponentExtension'; +import { lazy, ComponentType } from 'react'; + +// this is hard to do with blueprints... no TProps for the elements +export const ComponentBlueprint = createExtensionBlueprint({ + kind: 'component', + attachTo: { id: 'app', input: 'components' }, + output: [createComponentExtension.componentDataRef], + dataRefs: { + component: createComponentExtension.componentDataRef, + }, + factory( + { + ref, + loader, + }: { + ref: ComponentRef; + loader: + | { + lazy: (values: any) => Promise>; + } + | { + sync: (values: any) => ComponentType; + }; + }, + { config, inputs }, + ) { + if ('sync' in loader) { + return [ + createComponentExtension.componentDataRef({ + ref, + impl: loader.sync({ config, inputs }), + }), + ]; + } + + const lazyLoader = loader.lazy; + const ExtensionComponent = lazy(() => + lazyLoader({ config, inputs }).then(Component => ({ + default: Component, + })), + ); + + return [ + createComponentExtension.componentDataRef({ + ref, + impl: ExtensionComponent, + }), + ]; + }, +}); From 7a4eb9bcb5e213d1b5b018caf751ad14ef025387 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 8 Aug 2024 11:30:24 +0200 Subject: [PATCH 12/18] chore: more work for migrating Signed-off-by: blam --- .../src/extensions/ApiBlueprint.test.ts | 66 +++++++++++++++++++ .../src/extensions/ApiBlueprint.ts | 50 ++++++++++++++ .../src/extensions/PageBlueprint.tsx | 9 +-- 3 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts create mode 100644 packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts diff --git a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts new file mode 100644 index 0000000000..99369c6acd --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright 2024 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 { createExtensionInput } from '../wiring'; +import { ApiBlueprint } from './ApiBlueprint'; +import { createApiFactory, createApiRef } from '@backstage/core-plugin-api'; + +describe('ApiBlueprint', () => { + it('should create an extension with sensible defaults', () => { + const api = createApiRef<{ foo: string }>({ id: 'test' }); + const factory = createApiFactory({ + api, + deps: {}, + factory: () => ({ foo: 'bar' }), + }); + + const extension = ApiBlueprint.make({ + params: { + factory, + }, + }); + + expect(extension).toMatchInlineSnapshot(); + }); + + it('should create an extension with custom factory', () => { + const api = createApiRef<{ foo: string }>({ id: 'test' }); + const factory = jest.fn(() => ({ foo: 'bar' })); + + const extension = ApiBlueprint.make({ + config: { + schema: { + test: z => z.string().default('test'), + }, + }, + inputs: { + test: createExtensionInput([ApiBlueprint.dataRefs.factory]), + }, + factory(originalFactory, { config: _config, inputs: _inputs }) { + return originalFactory({ + api, + factory: () => + createApiFactory({ + api, + deps: {}, + factory, + }), + }); + }, + }); + + expect(extension).toMatchInlineSnapshot(); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts new file mode 100644 index 0000000000..dd2130f2cb --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2024 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 { createExtensionBlueprint } from '../wiring'; +import { createApiExtension } from './createApiExtension'; +import { AnyApiFactory, AnyApiRef } from '@backstage/core-plugin-api'; + +export const ApiBlueprint = createExtensionBlueprint({ + kind: 'api', + attachTo: { id: 'app', input: 'apis' }, + output: [createApiExtension.factoryDataRef], + dataRefs: { + factory: createApiExtension.factoryDataRef, + }, + *factory( + params: // remove this form. + | { + api: AnyApiRef; + factory: (params: unknown) => AnyApiFactory; + } + | { + factory: AnyApiFactory; + }, + { config, inputs }, + ) { + yield createApiExtension.factoryDataRef( + typeof params.factory === 'function' + ? params.factory({ config, inputs }) + : params.factory, + ); + }, + namespace: params => { + const apiRef = + 'api' in params ? params.api : (params.factory as { api: AnyApiRef }).api; + + return apiRef.id; + }, +}); diff --git a/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx index 4ee26321b7..506301ffdf 100644 --- a/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx @@ -38,16 +38,13 @@ export const PageBlueprint = createExtensionBlueprint({ routeRef, }: { defaultPath?: string; - loader: (opts: { - config: typeof config; - inputs: typeof inputs; - }) => Promise; + loader: () => Promise; routeRef?: RouteRef; }, - { config, inputs, node }, + { config, node }, ) { const ExtensionComponent = lazy(() => - loader({ config, inputs }).then(element => ({ default: () => element })), + loader().then(element => ({ default: () => element })), ); yield coreExtensionData.routePath(config.path ?? defaultPath!); From d3cfdc6e4dcda5393d61c7db87a388068db50086 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 8 Aug 2024 13:15:56 +0200 Subject: [PATCH 13/18] chore: updating API blueprint Signed-off-by: blam Signed-off-by: blam --- .../src/extensions/ApiBlueprint.test.ts | 81 ++++++++++++++++--- .../src/extensions/ApiBlueprint.ts | 27 +------ .../AppRootElementBlueprint.test.tsx | 48 +++++++++++ .../src/extensions/AppRootElementBlueprint.ts | 27 +++++++ .../src/extensions/ComponentBlueprint.tsx | 69 ---------------- .../src/extensions/RouterBlueprint.test.tsx | 22 ++--- .../src/extensions/RouterBlueprint.tsx | 24 +----- .../src/extensions/SignInPageBlueprint.tsx | 9 +-- .../src/wiring/createExtensionBlueprint.ts | 14 ++-- 9 files changed, 176 insertions(+), 145 deletions(-) create mode 100644 packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx create mode 100644 packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.ts delete mode 100644 packages/frontend-plugin-api/src/extensions/ComponentBlueprint.tsx diff --git a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts index 99369c6acd..c85c021c08 100644 --- a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts @@ -32,7 +32,27 @@ describe('ApiBlueprint', () => { }, }); - expect(extension).toMatchInlineSnapshot(); + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app", + "input": "apis", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "api", + "name": undefined, + "namespace": "test", + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); }); it('should create an extension with custom factory', () => { @@ -48,19 +68,62 @@ describe('ApiBlueprint', () => { inputs: { test: createExtensionInput([ApiBlueprint.dataRefs.factory]), }, + namespace: api.id, factory(originalFactory, { config: _config, inputs: _inputs }) { return originalFactory({ - api, - factory: () => - createApiFactory({ - api, - deps: {}, - factory, - }), + factory: createApiFactory({ + api, + deps: {}, + factory, + }), }); }, }); - expect(extension).toMatchInlineSnapshot(); + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app", + "input": "apis", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "test": { + "default": "test", + "type": "string", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": { + "test": { + "$$type": "@backstage/ExtensionInput", + "config": { + "optional": false, + "singleton": false, + }, + "extensionData": [ + [Function], + ], + }, + }, + "kind": "api", + "name": undefined, + "namespace": "test", + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); }); }); diff --git a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts index dd2130f2cb..16a20b5130 100644 --- a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts +++ b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts @@ -15,7 +15,7 @@ */ import { createExtensionBlueprint } from '../wiring'; import { createApiExtension } from './createApiExtension'; -import { AnyApiFactory, AnyApiRef } from '@backstage/core-plugin-api'; +import { AnyApiFactory } from '@backstage/core-plugin-api'; export const ApiBlueprint = createExtensionBlueprint({ kind: 'api', @@ -24,27 +24,8 @@ export const ApiBlueprint = createExtensionBlueprint({ dataRefs: { factory: createApiExtension.factoryDataRef, }, - *factory( - params: // remove this form. - | { - api: AnyApiRef; - factory: (params: unknown) => AnyApiFactory; - } - | { - factory: AnyApiFactory; - }, - { config, inputs }, - ) { - yield createApiExtension.factoryDataRef( - typeof params.factory === 'function' - ? params.factory({ config, inputs }) - : params.factory, - ); - }, - namespace: params => { - const apiRef = - 'api' in params ? params.api : (params.factory as { api: AnyApiRef }).api; - - return apiRef.id; + *factory(params: { factory: AnyApiFactory }) { + yield createApiExtension.factoryDataRef(params.factory); }, + namespace: ({ factory }) => factory.api.id, }); diff --git a/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx new file mode 100644 index 0000000000..d9c7d296cc --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { AppRootElementBlueprint } from './AppRootElementBlueprint'; + +describe('AppRootElementBlueprint', () => { + it('should create an extension with sensible defaults', () => { + const extension = AppRootElementBlueprint.make({ + params: { + element:
, + }, + }); + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/root", + "input": "elements", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "app-root-element", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.ts b/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.ts new file mode 100644 index 0000000000..1048d531ab --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2024 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 { coreExtensionData, createExtensionBlueprint } from '../wiring'; + +export const AppRootElementBlueprint = createExtensionBlueprint({ + kind: 'app-root-element', + attachTo: { id: 'app/root', input: 'elements' }, + output: [coreExtensionData.reactElement], + *factory(params: { element: JSX.Element | (() => JSX.Element) }) { + yield coreExtensionData.reactElement( + typeof params.element === 'function' ? params.element() : params.element, + ); + }, +}); diff --git a/packages/frontend-plugin-api/src/extensions/ComponentBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/ComponentBlueprint.tsx deleted file mode 100644 index 614196fc09..0000000000 --- a/packages/frontend-plugin-api/src/extensions/ComponentBlueprint.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2024 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 { ComponentRef } from '../components'; -import { createExtensionBlueprint } from '../wiring'; -import { createComponentExtension } from './createComponentExtension'; -import { lazy, ComponentType } from 'react'; - -// this is hard to do with blueprints... no TProps for the elements -export const ComponentBlueprint = createExtensionBlueprint({ - kind: 'component', - attachTo: { id: 'app', input: 'components' }, - output: [createComponentExtension.componentDataRef], - dataRefs: { - component: createComponentExtension.componentDataRef, - }, - factory( - { - ref, - loader, - }: { - ref: ComponentRef; - loader: - | { - lazy: (values: any) => Promise>; - } - | { - sync: (values: any) => ComponentType; - }; - }, - { config, inputs }, - ) { - if ('sync' in loader) { - return [ - createComponentExtension.componentDataRef({ - ref, - impl: loader.sync({ config, inputs }), - }), - ]; - } - - const lazyLoader = loader.lazy; - const ExtensionComponent = lazy(() => - lazyLoader({ config, inputs }).then(Component => ({ - default: Component, - })), - ); - - return [ - createComponentExtension.componentDataRef({ - ref, - impl: ExtensionComponent, - }), - ]; - }, -}); diff --git a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx index 3abc967fd8..ba4123138f 100644 --- a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx @@ -107,16 +107,18 @@ describe('RouterBlueprint', () => { inputs: { children: createExtensionInput([coreExtensionData.reactElement]), }, - params: { - Component: ({ inputs, children, config }) => ( - -
- {children} -
-
- ), + *factory(originalFactory, { inputs, config }) { + yield* originalFactory({ + Component: ({ children }) => ( + +
+ {children} +
+
+ ), + }); }, }); diff --git a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx index bf910eb944..e584a5654c 100644 --- a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx +++ b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { ComponentType, PropsWithChildren } from 'react'; +import { ComponentType, PropsWithChildren } from 'react'; import { createExtensionBlueprint } from '../wiring'; import { createRouterExtension } from './createRouterExtension'; @@ -24,25 +24,7 @@ export const RouterBlueprint = createExtensionBlueprint({ dataRefs: { component: createRouterExtension.componentDataRef, }, - *factory( - { - Component, - }: { - Component: ComponentType< - PropsWithChildren<{ - inputs: typeof inputs; - config: typeof config; - }> - >; - }, - { config, inputs }, - ) { - const Wrapper = (props: PropsWithChildren<{}>) => ( - - {props.children} - - ); - - yield createRouterExtension.componentDataRef(Wrapper); + *factory({ Component }: { Component: ComponentType> }) { + yield createRouterExtension.componentDataRef(Component); }, }); diff --git a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx index 01ba174efe..fc779f58ab 100644 --- a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx @@ -30,15 +30,12 @@ export const SignInPageBlueprint = createExtensionBlueprint({ { loader, }: { - loader: (opts: { - config: typeof config; - inputs: typeof inputs; - }) => Promise>; + loader: () => Promise>; }, - { config, inputs, node }, + { node }, ) { const ExtensionComponent = lazy(() => - loader({ config, inputs }).then(component => ({ default: component })), + loader().then(component => ({ default: component })), ); yield createSignInPageExtension.componentDataRef(props => ( diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index 81137f47e2..f749f7d412 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -312,9 +312,9 @@ class ExtensionBlueprintImpl< > > > { - const optionsSchema = - typeof this.options.config?.schema === 'function' - ? this.options.config?.schema(args.params!) + const optionsSchema = // can remove this args.params check with the split apart of .make + typeof this.options.config?.schema === 'function' && args.params + ? this.options.config?.schema(args.params) : this.options.config?.schema; const schema = { @@ -323,13 +323,13 @@ class ExtensionBlueprintImpl< } as TConfigSchema & TExtensionConfigSchema; const namespace = - typeof this.options.namespace === 'function' - ? this.options.namespace(args.params!) + typeof this.options.namespace === 'function' && args.params + ? this.options.namespace(args.params) : this.options.namespace; const name = - typeof this.options.name === 'function' - ? this.options.name(args.params!) + typeof this.options.name === 'function' && args.params + ? this.options.name(args.params) : this.options.name; return createExtension({ From ab70dc33d7d313729c69f849c0bdae6c2d20abfe Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 9 Aug 2024 08:11:00 +0200 Subject: [PATCH 14/18] chore: implementing the last of the blueprints Signed-off-by: blam --- .../AppRootWrapperBlueprint.test.tsx | 127 ++++++++++++++++++ .../extensions/AppRootWrapperBlueprint.tsx | 36 +++++ 2 files changed, 163 insertions(+) create mode 100644 packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx create mode 100644 packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx diff --git a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx new file mode 100644 index 0000000000..b67fc3664d --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx @@ -0,0 +1,127 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { PageBlueprint } from './PageBlueprint'; +import { waitFor } from '@testing-library/react'; +import { + coreExtensionData, + createExtension, + createExtensionInput, +} from '../wiring'; +import { each } from 'lodash'; + +describe('AppRootWrapperBlueprint', () => { + it('should return an extension with sensible defaults', () => { + const extension = AppRootWrapperBlueprint.make({ + params: { + Component: () =>
Hello
, + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/root", + "input": "elements", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "app-root-wrapper", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should render the simple component wrapper', async () => { + const extension = AppRootWrapperBlueprint.make({ + params: { + Component: () =>
Hello
, + }, + }); + + const { getByText } = createExtensionTester( + PageBlueprint.make({ + params: { + defaultPath: '/', + loader: async () =>
, + }, + }), + ) + .add(extension) + .render(); + + await waitFor(() => expect(getByText('Hello')).toBeInTheDocument()); + }); + + it('should render the complex component wrapper', async () => { + const extension = AppRootWrapperBlueprint.make({ + namespace: 'ns', + name: 'test', + config: { + schema: { + name: z => z.string(), + }, + }, + inputs: { + children: createExtensionInput([coreExtensionData.reactElement]), + }, + *factory(originalFactory, { inputs, config }) { + yield* originalFactory({ + Component: ({ children }) => ( +
+ {children} +
+ ), + }); + }, + }); + + const { getByText, getByTestId } = createExtensionTester( + PageBlueprint.make({ + params: { + defaultPath: '/', + loader: async () =>
Hi
, + }, + }), + ) + .add(extension, { config: { name: 'Robin' } }) + .add( + createExtension({ + attachTo: { id: 'app-root-wrapper:ns/test', input: 'children' }, + output: [coreExtensionData.reactElement], + factory: () => [coreExtensionData.reactElement(
Its Me
)], + }), + ) + .render(); + + await waitFor(() => { + expect(getByText('Its Me')).toBeInTheDocument(); + expect(getByText('Hi')).toBeInTheDocument(); + expect(getByTestId('Robin-1')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx new file mode 100644 index 0000000000..5bcbaa49bd --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx @@ -0,0 +1,36 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { ComponentType, PropsWithChildren } from 'react'; +import { createExtensionBlueprint } from '../wiring'; +import { createAppRootWrapperExtension } from './createAppRootWrapperExtension'; + +export const AppRootWrapperBlueprint = createExtensionBlueprint({ + kind: 'app-root-wrapper', + attachTo: { id: 'app/root', input: 'elements' }, + output: [createAppRootWrapperExtension.componentDataRef], + dataRefs: { + component: createAppRootWrapperExtension.componentDataRef, + }, + *factory(params: { Component: ComponentType> }) { + // todo(blam): not sure that this wrapping is even necessary anymore. + const Component = (props: PropsWithChildren<{}>) => { + return {props.children}; + }; + + yield createAppRootWrapperExtension.componentDataRef(Component); + }, +}); From 4bc224cc832e8bc9958b585d45b96b0e2bd4d376 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 9 Aug 2024 11:21:16 +0200 Subject: [PATCH 15/18] chore: updating tests and making them work properly Signed-off-by: blam --- .../src/extensions/ApiBlueprint.test.ts | 2 ++ .../AppRootElementBlueprint.test.tsx | 1 + .../AppRootWrapperBlueprint.test.tsx | 4 ++-- .../extensions/AppRootWrapperBlueprint.tsx | 3 ++- .../src/extensions/NavItemBlueprint.test.tsx | 1 + .../src/extensions/NavLogoBlueprint.test.tsx | 1 + .../src/extensions/PageBlueprint.test.tsx | 1 + .../src/extensions/RouterBlueprint.test.tsx | 1 + .../extensions/SignInPageBlueprint.test.tsx | 23 +++++++++++++++---- .../src/extensions/ThemeBlueprint.test.ts | 1 + .../extensions/TranslationBlueprint.test.ts | 1 + 11 files changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts index c85c021c08..f070956e96 100644 --- a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts @@ -49,6 +49,7 @@ describe('ApiBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } @@ -121,6 +122,7 @@ describe('ApiBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } diff --git a/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx index d9c7d296cc..3fb8d7b37a 100644 --- a/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx @@ -40,6 +40,7 @@ describe('AppRootElementBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } diff --git a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx index b67fc3664d..2ff46f0785 100644 --- a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx @@ -23,7 +23,6 @@ import { createExtension, createExtensionInput, } from '../wiring'; -import { each } from 'lodash'; describe('AppRootWrapperBlueprint', () => { it('should return an extension with sensible defaults', () => { @@ -38,7 +37,7 @@ describe('AppRootWrapperBlueprint', () => { "$$type": "@backstage/ExtensionDefinition", "attachTo": { "id": "app/root", - "input": "elements", + "input": "wrappers", }, "configSchema": undefined, "disabled": false, @@ -50,6 +49,7 @@ describe('AppRootWrapperBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } diff --git a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx b/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx index 5bcbaa49bd..4e8cab8f1b 100644 --- a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx +++ b/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { ComponentType, PropsWithChildren } from 'react'; import { createExtensionBlueprint } from '../wiring'; @@ -20,7 +21,7 @@ import { createAppRootWrapperExtension } from './createAppRootWrapperExtension'; export const AppRootWrapperBlueprint = createExtensionBlueprint({ kind: 'app-root-wrapper', - attachTo: { id: 'app/root', input: 'elements' }, + attachTo: { id: 'app/root', input: 'wrappers' }, output: [createAppRootWrapperExtension.componentDataRef], dataRefs: { component: createAppRootWrapperExtension.componentDataRef, diff --git a/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx index ba7643eee7..90c9cdf9b4 100644 --- a/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx @@ -60,6 +60,7 @@ describe('NavItemBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } diff --git a/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx index fa01862785..4a96475cf5 100644 --- a/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx @@ -43,6 +43,7 @@ describe('NavLogoBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } diff --git a/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx index 5e1d6b7601..c1350b3ddb 100644 --- a/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx @@ -76,6 +76,7 @@ describe('PageBlueprint', () => { "toString": [Function], }, ], + "override": [Function], "toString": [Function], "version": "v2", } diff --git a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx index ba4123138f..018c3fc570 100644 --- a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx @@ -52,6 +52,7 @@ describe('RouterBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } diff --git a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx index 5ffdd3175b..f97798a64c 100644 --- a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { SignInPageBlueprint } from './SignInPageBlueprint'; import { createExtensionTester } from '@backstage/frontend-test-utils'; -import { waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; +import { coreExtensionData, createExtension } from '../wiring'; describe('SignInPageBlueprint', () => { it('should create an extension with sensible defaults', () => { @@ -41,6 +43,7 @@ describe('SignInPageBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } @@ -51,17 +54,29 @@ describe('SignInPageBlueprint', () => { const MockSignInPage = () =>
; const extension = SignInPageBlueprint.make({ + name: 'test', params: { loader: async () => () => }, }); const tester = createExtensionTester(extension); + expect(tester.data(SignInPageBlueprint.dataRefs.component)).toBeDefined(); - const { getByTestId } = tester.render(); + createExtensionTester( + createExtension({ + name: 'dummy', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { + element: coreExtensionData.reactElement, + }, + factory: () => ({ element:
}), + }), + ) + .add(extension) + .render(); - // todo(blam): need a better way to test this, currently fails. await waitFor(() => { - expect(getByTestId('mock-sign-in')).toBeInTheDocument(); + expect(screen.getByTestId('mock-sign-in')).toBeInTheDocument(); }); }); }); diff --git a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts index 5bc3a8f7c3..92acd8eeba 100644 --- a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts @@ -44,6 +44,7 @@ describe('ThemeBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } diff --git a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts index a059189044..e2a979c097 100644 --- a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts @@ -60,6 +60,7 @@ describe('TranslationBlueprint', () => { "output": [ [Function], ], + "override": [Function], "toString": [Function], "version": "v2", } From e6c763f76b6d19865a320c86a361fbb3ef5035f9 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 9 Aug 2024 15:12:47 +0200 Subject: [PATCH 16/18] chore: getting the tests working MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Co-authored-by: Fredrik Adelöw Co-authored-by: Camila Belo Signed-off-by: blam --- packages/frontend-plugin-api/api-report.md | 410 +++++++++++++++--- .../ApiBlueprint.test.ts | 3 +- .../ApiBlueprint.ts | 4 +- .../AppRootElementBlueprint.test.tsx | 0 .../AppRootElementBlueprint.ts | 1 + .../AppRootWrapperBlueprint.test.tsx | 7 +- .../AppRootWrapperBlueprint.tsx | 3 +- .../IconBundleBlueprint.ts | 0 .../NavItemBlueprint.test.tsx | 1 - .../NavItemBlueprint.ts | 12 +- .../NavLogoBlueprint.test.tsx | 0 .../NavLogoBlueprint.ts | 3 +- .../PageBlueprint.test.tsx | 2 +- .../PageBlueprint.tsx | 5 +- .../RouterBlueprint.test.tsx | 2 +- .../RouterBlueprint.tsx | 3 +- .../SignInPageBlueprint.test.tsx | 0 .../SignInPageBlueprint.tsx | 3 +- .../ThemeBlueprint.test.ts | 3 +- .../ThemeBlueprint.ts | 4 +- .../TranslationBlueprint.test.ts | 2 +- .../TranslationBlueprint.ts | 4 +- .../src/blueprints/index.ts | 27 ++ .../src/extensions/createApiExtension.ts | 5 +- .../createAppRootElementExtension.ts | 1 + .../createAppRootWrapperExtension.tsx | 1 + .../src/extensions/createNavItemExtension.tsx | 2 + .../src/extensions/createNavLogoExtension.tsx | 2 + .../src/extensions/createPageExtension.tsx | 1 + .../src/extensions/createRouterExtension.tsx | 1 + .../extensions/createSignInPageExtension.tsx | 1 + .../src/extensions/createThemeExtension.ts | 10 +- .../extensions/createTranslationExtension.ts | 10 +- .../src/extensions/index.ts | 1 - packages/frontend-plugin-api/src/index.ts | 1 + .../wiring/createExtensionBlueprint.test.tsx | 95 +--- .../src/wiring/createExtensionBlueprint.ts | 224 +++++----- .../src/app/createExtensionTester.tsx | 5 +- 38 files changed, 578 insertions(+), 281 deletions(-) rename packages/frontend-plugin-api/src/{extensions => blueprints}/ApiBlueprint.test.ts (97%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/ApiBlueprint.ts (91%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/AppRootElementBlueprint.test.tsx (100%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/AppRootElementBlueprint.ts (98%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/AppRootWrapperBlueprint.test.tsx (95%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/AppRootWrapperBlueprint.tsx (92%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/IconBundleBlueprint.ts (100%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/NavItemBlueprint.test.tsx (98%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/NavItemBlueprint.ts (86%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/NavLogoBlueprint.test.tsx (100%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/NavLogoBlueprint.ts (92%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/PageBlueprint.test.tsx (98%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/PageBlueprint.tsx (97%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/RouterBlueprint.test.tsx (98%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/RouterBlueprint.tsx (92%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/SignInPageBlueprint.test.tsx (100%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/SignInPageBlueprint.tsx (93%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/ThemeBlueprint.test.ts (94%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/ThemeBlueprint.ts (91%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/TranslationBlueprint.test.ts (98%) rename packages/frontend-plugin-api/src/{extensions => blueprints}/TranslationBlueprint.ts (90%) create mode 100644 packages/frontend-plugin-api/src/blueprints/index.ts diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index a8f8ce591b..2d3f1d3278 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -189,6 +189,27 @@ export type AnyRoutes = { [name in string]: RouteRef | SubRouteRef; }; +// @public (undocumented) +export const ApiBlueprint: ExtensionBlueprint< + 'api', + undefined, + undefined, + { + factory: AnyApiFactory; + }, + ConfigurableExtensionDataRef, + {}, + {}, + {}, + { + factory: ConfigurableExtensionDataRef< + AnyApiFactory, + 'core.api.factory', + {} + >; + } +>; + export { ApiFactory }; export { ApiHolder }; @@ -240,6 +261,50 @@ export interface AppNodeSpec { readonly source?: BackstagePlugin; } +// @public (undocumented) +export const AppRootElementBlueprint: ExtensionBlueprint< + 'app-root-element', + undefined, + undefined, + { + element: JSX.Element | (() => JSX.Element); + }, + ConfigurableExtensionDataRef, + {}, + {}, + {}, + never +>; + +// @public (undocumented) +export const AppRootWrapperBlueprint: ExtensionBlueprint< + 'app-root-wrapper', + undefined, + undefined, + { + Component: ComponentType>; + }, + ConfigurableExtensionDataRef< + React_2.ComponentType<{ + children?: React_2.ReactNode; + }>, + 'app.root.wrapper', + {} + >, + {}, + {}, + {}, + { + component: ConfigurableExtensionDataRef< + React_2.ComponentType<{ + children?: React_2.ReactNode; + }>, + 'app.root.wrapper', + {} + >; + } +>; + export { AppTheme }; export { AppThemeApi }; @@ -381,7 +446,7 @@ export type CoreNotFoundErrorPageProps = { // @public (undocumented) export type CoreProgressProps = {}; -// @public (undocumented) +// @public @deprecated (undocumented) export function createApiExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -425,7 +490,7 @@ export { createApiFactory }; export { createApiRef }; -// @public +// @public @deprecated export function createAppRootElementExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -447,7 +512,7 @@ export function createAppRootElementExtension< }) => JSX_2.Element); }): ExtensionDefinition; -// @public +// @public @deprecated export function createAppRootWrapperExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -675,7 +740,7 @@ export type CreateExtensionBlueprintOptions< }, > = { kind: TKind; - namespace?: TNamespace | ((params: TParams) => TNamespace); + namespace?: TNamespace; attachTo: { id: string; input: string; @@ -683,9 +748,9 @@ export type CreateExtensionBlueprintOptions< disabled?: boolean; inputs?: TInputs; output: Array; - name?: TName | ((params: TParams) => TName); + name?: TName; config?: { - schema: TConfigSchema | ((params: TParams) => TConfigSchema); + schema: TConfigSchema; }; factory( params: TParams, @@ -824,7 +889,7 @@ export function createExternalRouteRef< } >; -// @public +// @public @deprecated export function createNavItemExtension(options: { namespace?: string; name?: string; @@ -859,7 +924,7 @@ export namespace createNavItemExtension { >; } -// @public +// @public @deprecated export function createNavLogoExtension(options: { name?: string; namespace?: string; @@ -888,7 +953,7 @@ export namespace createNavLogoExtension { >; } -// @public +// @public @deprecated export function createPageExtension< TConfig extends { path: string; @@ -958,7 +1023,7 @@ export function createRouteRef< } >; -// @public +// @public @deprecated export function createRouterExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -997,7 +1062,7 @@ export function createSchemaFromZod( schemaCreator: (zImpl: typeof z) => ZodSchema, ): PortableSchema; -// @public (undocumented) +// @public @deprecated (undocumented) export function createSignInPageExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -1036,7 +1101,7 @@ export function createSubRouteRef< parent: RouteRef; }): MakeSubRouteRef, ParentParams>; -// @public (undocumented) +// @public @deprecated (undocumented) export function createThemeExtension( theme: AppTheme, ): ExtensionDefinition< @@ -1049,7 +1114,7 @@ export function createThemeExtension( string | undefined >; -// @public (undocumented) +// @public @deprecated (undocumented) export namespace createThemeExtension { const // (undocumented) themeDataRef: ConfigurableExtensionDataRef< @@ -1059,7 +1124,7 @@ export namespace createThemeExtension { >; } -// @public (undocumented) +// @public @deprecated (undocumented) export function createTranslationExtension(options: { name?: string; resource: TranslationResource | TranslationMessages; @@ -1073,7 +1138,7 @@ export function createTranslationExtension(options: { string | undefined >; -// @public (undocumented) +// @public @deprecated (undocumented) export namespace createTranslationExtension { const // (undocumented) translationDataRef: ConfigurableExtensionDataRef< @@ -1153,9 +1218,31 @@ export interface ExtensionBlueprint< > { // (undocumented) dataRefs: TDataRefs; + // (undocumented) make< TNewNamespace extends string | undefined, TNewName extends string | undefined, + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { + id: string; + input: string; + }; + disabled?: boolean; + params: TParams; + }): ExtensionDefinition< + TConfig, + TConfigInput, + UOutput, + TInputs, + TKind, + string | undefined extends TNewNamespace ? TNamespace : TNewNamespace, + string | undefined extends TNewName ? TName : TNewName + >; + makeWithOverrides< + TNewNamespace extends string | undefined, + TNewName extends string | undefined, TExtensionConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, @@ -1170,55 +1257,48 @@ export interface ExtensionBlueprint< } >; }, - >( - args: { - namespace?: TNewNamespace; - name?: TNewName; - attachTo?: { - id: string; - input: string; + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { + id: string; + input: string; + }; + disabled?: boolean; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; + output?: Array; + config?: { + schema: TExtensionConfigSchema & { + [KName in keyof TConfig]?: `Error: Config key '${KName & + string}' is already defined in parent schema`; }; - disabled?: boolean; - inputs?: TExtraInputs & { - [KName in keyof TInputs]?: `Error: Input '${KName & - string}' is already defined in parent definition`; - }; - output?: Array; - config?: { - schema: TExtensionConfigSchema & { - [KName in keyof TConfig]?: `Error: Config key '${KName & - string}' is already defined in parent schema`; + }; + factory( + originalFactory: ( + params: TParams, + context?: { + config?: TConfig; + inputs?: Expand>; + }, + ) => ExtensionDataContainer, + context: { + node: AppNode; + config: TConfig & { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; }; - }; - } & ( - | ({ - factory( - originalFactory: ( - params: TParams, - context?: { - config?: TConfig; - inputs?: Expand>; - }, - ) => ExtensionDataContainer, - context: { - node: AppNode; - config: TConfig & { - [key in keyof TExtensionConfigSchema]: z.infer< - ReturnType - >; - }; - inputs: Expand>; - }, - ): Iterable; - } & VerifyExtensionFactoryOutput< - AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, - UFactoryOutput - >) - | { - params: TParams; - } - ), - ): ExtensionDefinition< + inputs: Expand>; + }, + ): Iterable & + VerifyExtensionFactoryOutput< + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + UFactoryOutput + >; + }): ExtensionDefinition< { [key in keyof TExtensionConfigSchema]: z.infer< ReturnType @@ -1616,6 +1696,73 @@ export interface LegacyExtensionInput< export { microsoftAuthApiRef }; +// @public (undocumented) +export const NavItemBlueprint: ExtensionBlueprint< + 'nav-item', + undefined, + undefined, + { + title: string; + icon: IconComponent_2; + routeRef: RouteRef; + }, + ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent_2; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >, + {}, + {}, + {}, + { + target: ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent_2; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >; + } +>; + +// @public (undocumented) +export const NavLogoBlueprint: ExtensionBlueprint< + 'nav-logo', + undefined, + undefined, + { + logoIcon: JSX.Element; + logoFull: JSX.Element; + }, + ConfigurableExtensionDataRef< + { + logoIcon?: JSX.Element | undefined; + logoFull?: JSX.Element | undefined; + }, + 'core.nav-logo.logo-elements', + {} + >, + {}, + {}, + {}, + { + logoElements: ConfigurableExtensionDataRef< + { + logoIcon?: JSX.Element | undefined; + logoFull?: JSX.Element | undefined; + }, + 'core.nav-logo.logo-elements', + {} + >; + } +>; + export { OAuthApi }; export { OAuthRequestApi }; @@ -1634,6 +1781,35 @@ export { oneloginAuthApiRef }; export { OpenIdConnectApi }; +// @public (undocumented) +export const PageBlueprint: ExtensionBlueprint< + 'page', + undefined, + undefined, + { + defaultPath: string; + loader: () => Promise; + routeRef?: RouteRef | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + RouteRef & { + optional: true; + } + >, + {}, + { + path: string | undefined; + }, + { + path?: string | undefined; + }, + never +>; + export { PendingOAuthRequest }; // @public (undocumented) @@ -1699,6 +1875,35 @@ export type RouteFunc = ( : readonly [params: TParams] ) => string; +// @public (undocumented) +export const RouterBlueprint: ExtensionBlueprint< + 'app-router-component', + undefined, + undefined, + { + Component: ComponentType>; + }, + ConfigurableExtensionDataRef< + ComponentType<{ + children?: ReactNode; + }>, + 'app.router.wrapper', + {} + >, + {}, + {}, + {}, + { + component: ConfigurableExtensionDataRef< + ComponentType<{ + children?: ReactNode; + }>, + 'app.router.wrapper', + {} + >; + } +>; + // @public export interface RouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, @@ -1733,6 +1938,31 @@ export { SessionApi }; export { SessionState }; +// @public (undocumented) +export const SignInPageBlueprint: ExtensionBlueprint< + 'sign-in-page', + undefined, + undefined, + { + loader: () => Promise>; + }, + ConfigurableExtensionDataRef< + React_2.ComponentType, + 'core.sign-in-page.component', + {} + >, + {}, + {}, + {}, + { + component: ConfigurableExtensionDataRef< + React_2.ComponentType, + 'core.sign-in-page.component', + {} + >; + } +>; + export { StorageApi }; export { storageApiRef }; @@ -1751,6 +1981,62 @@ export interface SubRouteRef< readonly T: TParams; } +// @public (undocumented) +export const ThemeBlueprint: ExtensionBlueprint< + 'theme', + 'app', + undefined, + { + theme: AppTheme; + }, + ConfigurableExtensionDataRef, + {}, + {}, + {}, + { + theme: ConfigurableExtensionDataRef; + } +>; + +// @public (undocumented) +export const TranslationBlueprint: ExtensionBlueprint< + 'translation', + undefined, + undefined, + { + resource: TranslationResource | TranslationMessages; + }, + ConfigurableExtensionDataRef< + | TranslationResource + | TranslationMessages< + string, + { + [x: string]: string; + }, + boolean + >, + 'core.translation.translation', + {} + >, + {}, + {}, + {}, + { + translation: ConfigurableExtensionDataRef< + | TranslationResource + | TranslationMessages< + string, + { + [x: string]: string; + }, + boolean + >, + 'core.translation.translation', + {} + >; + } +>; + export { TranslationMessages }; export { TranslationMessagesOptions }; diff --git a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts similarity index 97% rename from packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts rename to packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts index f070956e96..3b865a78bb 100644 --- a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts @@ -30,6 +30,7 @@ describe('ApiBlueprint', () => { params: { factory, }, + namespace: 'test', }); expect(extension).toMatchInlineSnapshot(` @@ -60,7 +61,7 @@ describe('ApiBlueprint', () => { const api = createApiRef<{ foo: string }>({ id: 'test' }); const factory = jest.fn(() => ({ foo: 'bar' })); - const extension = ApiBlueprint.make({ + const extension = ApiBlueprint.makeWithOverrides({ config: { schema: { test: z => z.string().default('test'), diff --git a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts similarity index 91% rename from packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts rename to packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts index 16a20b5130..da12faef1a 100644 --- a/packages/frontend-plugin-api/src/extensions/ApiBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts @@ -14,9 +14,10 @@ * limitations under the License. */ import { createExtensionBlueprint } from '../wiring'; -import { createApiExtension } from './createApiExtension'; +import { createApiExtension } from '../extensions/createApiExtension'; import { AnyApiFactory } from '@backstage/core-plugin-api'; +/** @public */ export const ApiBlueprint = createExtensionBlueprint({ kind: 'api', attachTo: { id: 'app', input: 'apis' }, @@ -27,5 +28,4 @@ export const ApiBlueprint = createExtensionBlueprint({ *factory(params: { factory: AnyApiFactory }) { yield createApiExtension.factoryDataRef(params.factory); }, - namespace: ({ factory }) => factory.api.id, }); diff --git a/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.test.tsx similarity index 100% rename from packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.test.tsx rename to packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.test.tsx diff --git a/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts similarity index 98% rename from packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.ts rename to packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts index 1048d531ab..e44942979d 100644 --- a/packages/frontend-plugin-api/src/extensions/AppRootElementBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts @@ -15,6 +15,7 @@ */ import { coreExtensionData, createExtensionBlueprint } from '../wiring'; +/** @public */ export const AppRootElementBlueprint = createExtensionBlueprint({ kind: 'app-root-element', attachTo: { id: 'app/root', input: 'elements' }, diff --git a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.test.tsx similarity index 95% rename from packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx rename to packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.test.tsx index 2ff46f0785..f7feab1d46 100644 --- a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.test.tsx @@ -78,7 +78,7 @@ describe('AppRootWrapperBlueprint', () => { }); it('should render the complex component wrapper', async () => { - const extension = AppRootWrapperBlueprint.make({ + const extension = AppRootWrapperBlueprint.makeWithOverrides({ namespace: 'ns', name: 'test', config: { @@ -94,6 +94,9 @@ describe('AppRootWrapperBlueprint', () => { Component: ({ children }) => (
{children} + {inputs.children.flatMap(c => + c.get(coreExtensionData.reactElement), + )}
), }); @@ -119,9 +122,9 @@ describe('AppRootWrapperBlueprint', () => { .render(); await waitFor(() => { - expect(getByText('Its Me')).toBeInTheDocument(); expect(getByText('Hi')).toBeInTheDocument(); expect(getByTestId('Robin-1')).toBeInTheDocument(); + expect(getByText('Its Me')).toBeInTheDocument(); }); }); }); diff --git a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx similarity index 92% rename from packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx rename to packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx index 4e8cab8f1b..db63550880 100644 --- a/packages/frontend-plugin-api/src/extensions/AppRootWrapperBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx @@ -17,8 +17,9 @@ import React from 'react'; import { ComponentType, PropsWithChildren } from 'react'; import { createExtensionBlueprint } from '../wiring'; -import { createAppRootWrapperExtension } from './createAppRootWrapperExtension'; +import { createAppRootWrapperExtension } from '../extensions/createAppRootWrapperExtension'; +/** @public */ export const AppRootWrapperBlueprint = createExtensionBlueprint({ kind: 'app-root-wrapper', attachTo: { id: 'app/root', input: 'wrappers' }, diff --git a/packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts similarity index 100% rename from packages/frontend-plugin-api/src/extensions/IconBundleBlueprint.ts rename to packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts diff --git a/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.test.tsx similarity index 98% rename from packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx rename to packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.test.tsx index 90c9cdf9b4..94b9e7b8e6 100644 --- a/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.test.tsx @@ -44,7 +44,6 @@ describe('NavItemBlueprint', () => { "additionalProperties": false, "properties": { "title": { - "default": "TEST", "type": "string", }, }, diff --git a/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts similarity index 86% rename from packages/frontend-plugin-api/src/extensions/NavItemBlueprint.ts rename to packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts index 568431c0f5..5391d0dec2 100644 --- a/packages/frontend-plugin-api/src/extensions/NavItemBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts @@ -17,8 +17,9 @@ import { IconComponent } from '@backstage/core-plugin-api'; import { RouteRef } from '../routing'; import { createExtensionBlueprint } from '../wiring'; -import { createNavItemExtension } from './createNavItemExtension'; +import { createNavItemExtension } from '../extensions/createNavItemExtension'; +/** @public */ export const NavItemBlueprint = createExtensionBlueprint({ kind: 'nav-item', attachTo: { id: 'app/nav', input: 'items' }, @@ -30,6 +31,7 @@ export const NavItemBlueprint = createExtensionBlueprint({ { icon, routeRef, + title, }: { title: string; icon: IconComponent; @@ -38,14 +40,14 @@ export const NavItemBlueprint = createExtensionBlueprint({ { config }, ) => [ createNavItemExtension.targetDataRef({ - title: config.title, + title: config.title ?? title, icon, routeRef, }), ], config: { - schema: ({ title }) => ({ - title: z => z.string().default(title), - }), + schema: { + title: z => z.string().optional(), + }, }, }); diff --git a/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.test.tsx similarity index 100% rename from packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.test.tsx rename to packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.test.tsx diff --git a/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts similarity index 92% rename from packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.ts rename to packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts index d119287f29..f31b3cec9b 100644 --- a/packages/frontend-plugin-api/src/extensions/NavLogoBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts @@ -15,8 +15,9 @@ */ import { createExtensionBlueprint } from '../wiring'; -import { createNavLogoExtension } from './createNavLogoExtension'; +import { createNavLogoExtension } from '../extensions/createNavLogoExtension'; +/** @public */ export const NavLogoBlueprint = createExtensionBlueprint({ kind: 'nav-logo', attachTo: { id: 'app/nav', input: 'logos' }, diff --git a/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx similarity index 98% rename from packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx rename to packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx index c1350b3ddb..c4bcb4b467 100644 --- a/packages/frontend-plugin-api/src/extensions/PageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx @@ -106,7 +106,7 @@ describe('PageBlueprint', () => { }); it('should allow defining additional inputs to the extension', async () => { - const myPage = PageBlueprint.make({ + const myPage = PageBlueprint.makeWithOverrides({ name: 'test-page', inputs: { cards: createExtensionInput([coreExtensionData.reactElement], { diff --git a/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx similarity index 97% rename from packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx rename to packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx index 506301ffdf..bfcf4a7934 100644 --- a/packages/frontend-plugin-api/src/extensions/PageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx @@ -18,6 +18,7 @@ import { RouteRef } from '../routing'; import { coreExtensionData, createExtensionBlueprint } from '../wiring'; import { ExtensionBoundary } from '../components'; +/** @public */ export const PageBlueprint = createExtensionBlueprint({ kind: 'page', attachTo: { id: 'app/routes', input: 'routes' }, @@ -37,7 +38,7 @@ export const PageBlueprint = createExtensionBlueprint({ loader, routeRef, }: { - defaultPath?: string; + defaultPath: string; loader: () => Promise; routeRef?: RouteRef; }, @@ -47,7 +48,7 @@ export const PageBlueprint = createExtensionBlueprint({ loader().then(element => ({ default: () => element })), ); - yield coreExtensionData.routePath(config.path ?? defaultPath!); + yield coreExtensionData.routePath(config.path ?? defaultPath); yield coreExtensionData.reactElement( diff --git a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.test.tsx similarity index 98% rename from packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx rename to packages/frontend-plugin-api/src/blueprints/RouterBlueprint.test.tsx index 018c3fc570..fe40aaa8d7 100644 --- a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.test.tsx @@ -97,7 +97,7 @@ describe('RouterBlueprint', () => { }); it('should work with complex options and props', async () => { - const extension = RouterBlueprint.make({ + const extension = RouterBlueprint.makeWithOverrides({ namespace: 'test', name: 'test', config: { diff --git a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx similarity index 92% rename from packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx rename to packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx index e584a5654c..24ee805659 100644 --- a/packages/frontend-plugin-api/src/extensions/RouterBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx @@ -15,8 +15,9 @@ */ import { ComponentType, PropsWithChildren } from 'react'; import { createExtensionBlueprint } from '../wiring'; -import { createRouterExtension } from './createRouterExtension'; +import { createRouterExtension } from '../extensions/createRouterExtension'; +/** @public */ export const RouterBlueprint = createExtensionBlueprint({ kind: 'app-router-component', attachTo: { id: 'app/root', input: 'router' }, diff --git a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx similarity index 100% rename from packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.test.tsx rename to packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx diff --git a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx similarity index 93% rename from packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx rename to packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx index fc779f58ab..2727335d8a 100644 --- a/packages/frontend-plugin-api/src/extensions/SignInPageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx @@ -15,10 +15,11 @@ */ import React, { ComponentType, lazy } from 'react'; import { createExtensionBlueprint } from '../wiring'; -import { createSignInPageExtension } from './createSignInPageExtension'; +import { createSignInPageExtension } from '../extensions/createSignInPageExtension'; import { SignInPageProps } from '@backstage/core-plugin-api'; import { ExtensionBoundary } from '../components'; +/** @public */ export const SignInPageBlueprint = createExtensionBlueprint({ kind: 'sign-in-page', attachTo: { id: 'app/root', input: 'signInPage' }, diff --git a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts similarity index 94% rename from packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts rename to packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts index 92acd8eeba..40323b6a31 100644 --- a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts @@ -27,7 +27,8 @@ describe('ThemeBlueprint', () => { } as AppTheme; it('should create an extension with sensible defaults', () => { - expect(ThemeBlueprint.make({ params: { theme } })).toMatchInlineSnapshot(` + expect(ThemeBlueprint.make({ name: 'light', params: { theme } })) + .toMatchInlineSnapshot(` { "$$type": "@backstage/ExtensionDefinition", "attachTo": { diff --git a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts similarity index 91% rename from packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts rename to packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts index 500946713b..cd97e0437f 100644 --- a/packages/frontend-plugin-api/src/extensions/ThemeBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts @@ -16,12 +16,12 @@ import { AppTheme } from '@backstage/core-plugin-api'; import { createExtensionBlueprint } from '../wiring'; -import { createThemeExtension } from './createThemeExtension'; +import { createThemeExtension } from '../extensions/createThemeExtension'; +/** @public */ export const ThemeBlueprint = createExtensionBlueprint({ kind: 'theme', namespace: 'app', - name: ({ theme }) => theme.id, attachTo: { id: 'app', input: 'themes' }, output: [createThemeExtension.themeDataRef], dataRefs: { diff --git a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts similarity index 98% rename from packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts rename to packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts index e2a979c097..dfb918053a 100644 --- a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts @@ -56,7 +56,7 @@ describe('TranslationBlueprint', () => { "inputs": {}, "kind": "translation", "name": "blob", - "namespace": "translationRefId", + "namespace": undefined, "output": [ [Function], ], diff --git a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts similarity index 90% rename from packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts rename to packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts index 581f4d176a..c70ca56f6b 100644 --- a/packages/frontend-plugin-api/src/extensions/TranslationBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts @@ -15,12 +15,12 @@ */ import { createExtensionBlueprint } from '../wiring'; -import { createTranslationExtension } from './createTranslationExtension'; +import { createTranslationExtension } from '../extensions/createTranslationExtension'; import { TranslationMessages, TranslationResource } from '../translation'; +/** @public */ export const TranslationBlueprint = createExtensionBlueprint({ kind: 'translation', - namespace: ({ resource }) => resource.id, attachTo: { id: 'app', input: 'translations' }, output: [createTranslationExtension.translationDataRef], dataRefs: { diff --git a/packages/frontend-plugin-api/src/blueprints/index.ts b/packages/frontend-plugin-api/src/blueprints/index.ts new file mode 100644 index 0000000000..85f7a99a2c --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ApiBlueprint } from './ApiBlueprint'; +export { AppRootElementBlueprint } from './AppRootElementBlueprint'; +export { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint'; +export { IconBundleBlueprint } from './IconBundleBlueprint'; +export { NavItemBlueprint } from './NavItemBlueprint'; +export { NavLogoBlueprint } from './NavLogoBlueprint'; +export { PageBlueprint } from './PageBlueprint'; +export { RouterBlueprint } from './RouterBlueprint'; +export { SignInPageBlueprint } from './SignInPageBlueprint'; +export { ThemeBlueprint } from './ThemeBlueprint'; +export { TranslationBlueprint } from './TranslationBlueprint'; diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts index 93346f1ec9..b1db2a0106 100644 --- a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts @@ -24,7 +24,10 @@ import { import { AnyExtensionInputMap } from '../wiring/createExtension'; import { Expand } from '../types'; -/** @public */ +/** + * @public + * @deprecated Use {@link ApiBlueprint} instead. + */ export function createApiExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts index 8be82d5540..c8f7bdc366 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts @@ -30,6 +30,7 @@ import { * the app layout. This is useful for example for shared popups and similar. * * @public + * @deprecated Use {@link AppRootElementBlueprint} instead. */ export function createAppRootElementExtension< TConfig extends {}, diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx index ce2eb89f25..4f70f29470 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx @@ -31,6 +31,7 @@ import { Expand } from '../types'; * and similar. * * @public + * @deprecated Use {@link AppRootWrapperBlueprint} instead. */ export function createAppRootWrapperExtension< TConfig extends {}, diff --git a/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx b/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx index 2a2c47835a..d694440427 100644 --- a/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx @@ -21,7 +21,9 @@ import { RouteRef } from '../routing'; /** * Helper for creating extensions for a nav item. + * * @public + * @deprecated Use {@link NavItemBlueprint} instead. */ export function createNavItemExtension(options: { namespace?: string; diff --git a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx index 1f2419bd02..02c32e7f34 100644 --- a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx @@ -18,7 +18,9 @@ import { createExtension, createExtensionDataRef } from '../wiring'; /** * Helper for creating extensions for a nav logos. + * * @public + * @deprecated Use {@link NavLogoBlueprint} instead. */ export function createNavLogoExtension(options: { name?: string; diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index cd434a3ffc..bef642e465 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -31,6 +31,7 @@ import { ExtensionDefinition } from '../wiring/createExtension'; * Helper for creating extensions for a routable React page component. * * @public + * @deprecated Use {@link PageBlueprint} instead. */ export function createPageExtension< TConfig extends { path: string }, diff --git a/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx b/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx index 12f946e8a7..943711f5e0 100644 --- a/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx @@ -31,6 +31,7 @@ import { Expand } from '../types'; * MemoryRouter in tests, or to add additional props to a BrowserRouter. * * @public + * @deprecated Use {@link RouterBlueprint} instead. */ export function createRouterExtension< TConfig extends {}, diff --git a/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx index 312d30c78c..3f41202f11 100644 --- a/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx @@ -30,6 +30,7 @@ import { SignInPageProps } from '@backstage/core-plugin-api'; /** * * @public + * @deprecated Use {@link SignInPageBlueprint} instead. */ export function createSignInPageExtension< TConfig extends {}, diff --git a/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts index 1740f05a5a..e523fd9c05 100644 --- a/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts @@ -17,7 +17,10 @@ import { createExtension, createExtensionDataRef } from '../wiring'; import { AppTheme } from '@backstage/core-plugin-api'; -/** @public */ +/** + * @public + * @deprecated Use {@link ThemeBlueprint} instead. + */ export function createThemeExtension(theme: AppTheme) { return createExtension({ kind: 'theme', @@ -31,7 +34,10 @@ export function createThemeExtension(theme: AppTheme) { }); } -/** @public */ +/** + * @public + * @deprecated Use {@link ThemeBlueprint} instead. + */ export namespace createThemeExtension { export const themeDataRef = createExtensionDataRef().with({ id: 'core.theme.theme', diff --git a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts index 9445e32e24..c4d2f6037d 100644 --- a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts @@ -17,7 +17,10 @@ import { TranslationMessages, TranslationResource } from '../translation'; import { createExtension, createExtensionDataRef } from '../wiring'; -/** @public */ +/** + * @public + * @deprecated Use {@link TranslationBlueprint} instead. + */ export function createTranslationExtension(options: { name?: string; resource: TranslationResource | TranslationMessages; @@ -34,7 +37,10 @@ export function createTranslationExtension(options: { }); } -/** @public */ +/** + * @public + * @deprecated Use {@link TranslationBlueprint} instead. + */ export namespace createTranslationExtension { export const translationDataRef = createExtensionDataRef< TranslationResource | TranslationMessages diff --git a/packages/frontend-plugin-api/src/extensions/index.ts b/packages/frontend-plugin-api/src/extensions/index.ts index 267201db81..562cb728cb 100644 --- a/packages/frontend-plugin-api/src/extensions/index.ts +++ b/packages/frontend-plugin-api/src/extensions/index.ts @@ -25,4 +25,3 @@ export { createSignInPageExtension } from './createSignInPageExtension'; export { createThemeExtension } from './createThemeExtension'; export { createComponentExtension } from './createComponentExtension'; export { createTranslationExtension } from './createTranslationExtension'; -export { IconBundleBlueprint } from './IconBundleBlueprint'; diff --git a/packages/frontend-plugin-api/src/index.ts b/packages/frontend-plugin-api/src/index.ts index 263380b1f8..957ca50bba 100644 --- a/packages/frontend-plugin-api/src/index.ts +++ b/packages/frontend-plugin-api/src/index.ts @@ -22,6 +22,7 @@ export * from './analytics'; export * from './apis'; +export * from './blueprints'; export * from './components'; export * from './extensions'; export * from './icons'; diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx index 924eba2cba..b9d1475074 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -127,7 +127,7 @@ describe('createExtensionBlueprint', () => { }, }); - const extension = TestExtensionBlueprint.make({ + const extension = TestExtensionBlueprint.makeWithOverrides({ name: 'my-extension', factory(origFactory) { return origFactory({ @@ -185,11 +185,8 @@ describe('createExtensionBlueprint', () => { }, }); - const extension = TestExtensionBlueprint.make({ + const extension = TestExtensionBlueprint.makeWithOverrides({ name: 'my-extension', - params: { - text: 'Hello, world!', - }, config: { schema: { something: z => z.string(), @@ -236,7 +233,7 @@ describe('createExtensionBlueprint', () => { }, }); - TestExtensionBlueprint.make({ + TestExtensionBlueprint.makeWithOverrides({ name: 'my-extension', params: { text: 'Hello, world!', @@ -266,11 +263,8 @@ describe('createExtensionBlueprint', () => { }, }); - const extension = TestExtensionBlueprint.make({ + const extension = TestExtensionBlueprint.makeWithOverrides({ name: 'my-extension', - params: { - text: 'Hello, world!', - }, config: { schema: { something: z => z.string(), @@ -341,65 +335,6 @@ describe('createExtensionBlueprint', () => { expect(true).toBe(true); }); - it('should allow providing callback for properties to set with params', () => { - type TestParams = { test: string }; - - const Blueprint = createExtensionBlueprint({ - kind: 'test-extension', - attachTo: { id: 'test', input: 'default' }, - name: (params: TestParams) => `${params.test}-name`, - output: [coreExtensionData.reactElement], - namespace: (props: TestParams) => props.test, - config: { - schema: (props: TestParams) => ({ - test: z => z.string().default(props.test), - }), - }, - factory(params: TestParams) { - return [coreExtensionData.reactElement(
{params.test}
)]; - }, - }); - - expect(Blueprint.make({ params: { test: 'hello' } })) - .toMatchInlineSnapshot(` - { - "$$type": "@backstage/ExtensionDefinition", - "attachTo": { - "id": "test", - "input": "default", - }, - "configSchema": { - "parse": [Function], - "schema": { - "$schema": "http://json-schema.org/draft-07/schema#", - "additionalProperties": false, - "properties": { - "test": { - "default": "hello", - "type": "string", - }, - }, - "type": "object", - }, - }, - "disabled": false, - "factory": [Function], - "inputs": {}, - "kind": "test-extension", - "name": "hello-name", - "namespace": "hello", - "output": [ - [Function], - ], - "override": [Function], - "toString": [Function], - "version": "v2", - } - `); - - expect(true).toBe(true); - }); - it('should allow merging of inputs', () => { const blueprint = createExtensionBlueprint({ kind: 'test-extension', @@ -418,7 +353,7 @@ describe('createExtensionBlueprint', () => { }, }); - blueprint.make({ + blueprint.makeWithOverrides({ inputs: { test2: createExtensionInput([coreExtensionData.reactElement], { singleton: true, @@ -453,7 +388,7 @@ describe('createExtensionBlueprint', () => { }, }); - blueprint.make({ + blueprint.makeWithOverrides({ inputs: { // @ts-expect-error test: createExtensionInput([]), // Overrides are not allowed @@ -480,7 +415,7 @@ describe('createExtensionBlueprint', () => { }); const ext = toInternalExtensionDefinition( - blueprint.make({ + blueprint.makeWithOverrides({ output: [testDataRef2], factory(origFactory) { const parent = origFactory({}); @@ -509,7 +444,7 @@ describe('createExtensionBlueprint', () => { expect( factoryOutput( - blueprint.make({ + blueprint.makeWithOverrides({ output: [testDataRef1, testDataRef2], *factory(origFactory) { yield* origFactory({}); @@ -521,7 +456,7 @@ describe('createExtensionBlueprint', () => { expect( factoryOutput( - blueprint.make({ + blueprint.makeWithOverrides({ output: [testDataRef1, testDataRef2], factory(origFactory) { return [...origFactory({}), testDataRef2('bar')]; @@ -544,9 +479,9 @@ describe('createExtensionBlueprint', () => { }, }); - // @ts-expect-error - blueprint.make({ + blueprint.makeWithOverrides({ output: [testDataRef2.optional()], + // @ts-expect-error *factory() { yield testDataRef1('foo'); yield testDataRef2('bar'); @@ -555,7 +490,7 @@ describe('createExtensionBlueprint', () => { expect( factoryOutput( - blueprint.make({ + blueprint.makeWithOverrides({ output: [testDataRef2.optional()], *factory() { yield testDataRef2('bar'); @@ -578,9 +513,9 @@ describe('createExtensionBlueprint', () => { }, }); - // @ts-expect-error - blueprint.make({ + blueprint.makeWithOverrides({ output: [testDataRef1, testDataRef2], + // @ts-expect-error *factory(origFactory) { yield* origFactory({}); }, @@ -588,7 +523,7 @@ describe('createExtensionBlueprint', () => { expect( factoryOutput( - blueprint.make({ + blueprint.makeWithOverrides({ output: [testDataRef1, testDataRef2], *factory(origFactory) { yield* origFactory({}); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts index f749f7d412..656e681a2b 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -52,14 +52,14 @@ export type CreateExtensionBlueprintOptions< TDataRefs extends { [name in string]: AnyExtensionDataRef }, > = { kind: TKind; - namespace?: TNamespace | ((params: TParams) => TNamespace); + namespace?: TNamespace; attachTo: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; output: Array; - name?: TName | ((params: TParams) => TName); + name?: TName; config?: { - schema: TConfigSchema | ((params: TParams) => TConfigSchema); + schema: TConfigSchema; }; factory( params: TParams, @@ -96,13 +96,32 @@ export interface ExtensionBlueprint< > { dataRefs: TDataRefs; + make< + TNewNamespace extends string | undefined, + TNewName extends string | undefined, + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { id: string; input: string }; + disabled?: boolean; + params: TParams; + }): ExtensionDefinition< + TConfig, + TConfigInput, + UOutput, + TInputs, + TKind, + string | undefined extends TNewNamespace ? TNamespace : TNewNamespace, + string | undefined extends TNewName ? TName : TNewName + >; + /** * Creates a new extension from the blueprint. * * You must either pass `params` directly, or define a `factory` that can * optionally call the original factory with the same params. */ - make< + makeWithOverrides< TNewNamespace extends string | undefined, TNewName extends string | undefined, TExtensionConfigSchema extends { @@ -116,52 +135,45 @@ export interface ExtensionBlueprint< { optional: boolean; singleton: boolean } >; }, - >( - args: { - namespace?: TNewNamespace; - name?: TNewName; - attachTo?: { id: string; input: string }; - disabled?: boolean; - inputs?: TExtraInputs & { - [KName in keyof TInputs]?: `Error: Input '${KName & - string}' is already defined in parent definition`; + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; + output?: Array; + config?: { + schema: TExtensionConfigSchema & { + [KName in keyof TConfig]?: `Error: Config key '${KName & + string}' is already defined in parent schema`; }; - output?: Array; - config?: { - schema: TExtensionConfigSchema & { - [KName in keyof TConfig]?: `Error: Config key '${KName & - string}' is already defined in parent schema`; + }; + factory( + originalFactory: ( + params: TParams, + context?: { + config?: TConfig; + inputs?: Expand>; + }, + ) => ExtensionDataContainer, + context: { + node: AppNode; + config: TConfig & { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; }; - }; - } & ( - | ({ - factory( - originalFactory: ( - params: TParams, - context?: { - config?: TConfig; - inputs?: Expand>; - }, - ) => ExtensionDataContainer, - context: { - node: AppNode; - config: TConfig & { - [key in keyof TExtensionConfigSchema]: z.infer< - ReturnType - >; - }; - inputs: Expand>; - }, - ): Iterable; - } & VerifyExtensionFactoryOutput< - AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, - UFactoryOutput - >) - | { - params: TParams; - } - ), - ): ExtensionDefinition< + inputs: Expand>; + }, + ): Iterable & + VerifyExtensionFactoryOutput< + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + UFactoryOutput + >; + }): ExtensionDefinition< { [key in keyof TExtensionConfigSchema]: z.infer< ReturnType @@ -243,7 +255,7 @@ class ExtensionBlueprintImpl< dataRefs: TDataRefs; - public make< + public makeWithOverrides< TExtensionConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType; }, @@ -264,11 +276,10 @@ class ExtensionBlueprintImpl< disabled?: boolean; inputs?: TExtraInputs; output?: Array; - params?: TParams; config?: { schema: TExtensionConfigSchema; }; - factory?( + factory( originalFactory: ( params: TParams, context?: { @@ -312,77 +323,82 @@ class ExtensionBlueprintImpl< > > > { - const optionsSchema = // can remove this args.params check with the split apart of .make - typeof this.options.config?.schema === 'function' && args.params - ? this.options.config?.schema(args.params) - : this.options.config?.schema; - const schema = { - ...optionsSchema, + ...this.options.config?.schema, ...args.config?.schema, } as TConfigSchema & TExtensionConfigSchema; - const namespace = - typeof this.options.namespace === 'function' && args.params - ? this.options.namespace(args.params) - : this.options.namespace; - - const name = - typeof this.options.name === 'function' && args.params - ? this.options.name(args.params) - : this.options.name; - return createExtension({ kind: this.options.kind, - namespace: args.namespace ?? namespace, - name: args.name ?? name, + namespace: args.namespace ?? this.options.namespace, + name: args.name ?? this.options.name, attachTo: args.attachTo ?? this.options.attachTo, disabled: args.disabled ?? this.options.disabled, inputs: { ...args.inputs, ...this.options.inputs }, output: args.output ?? this.options.output, config: Object.keys(schema).length === 0 ? undefined : { schema }, factory: ({ node, config, inputs }) => { - if (args.factory) { - return args.factory( - ( - innerParams: TParams, - innerContext?: { - config?: { - [key in keyof TConfigSchema]: z.infer< - ReturnType - >; - }; - inputs?: Expand>; - }, - ): ExtensionDataContainer => { - return createDataContainer( - this.options.factory(innerParams, { - node, - config: innerContext?.config ?? config, - inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden - }), - ); + return args.factory( + ( + innerParams: TParams, + innerContext?: { + config?: { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }; + inputs?: Expand>; }, - { - node, - config, - inputs, - }, - ); - } else if (args.params) { - return this.options.factory(args.params, { + ): ExtensionDataContainer => { + return createDataContainer( + this.options.factory(innerParams, { + node, + config: innerContext?.config ?? config, + inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden + }), + ); + }, + { node, config, - // TODO: Figure out types once legacy data map input type is gone - inputs: inputs as unknown as Expand< - ResolvedExtensionInputs - >, - }); - } - throw new Error('Either params or factory must be provided'); + inputs, + }, + ); }, } as CreateExtensionOptions); } + + public make< + TNewNamespace extends string | undefined = undefined, + TNewName extends string | undefined = undefined, + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { id: string; input: string }; + disabled?: boolean; + params: TParams; + }): ExtensionDefinition< + { + [key in keyof TConfigSchema]: z.infer>; + }, + z.input< + z.ZodObject<{ + [key in keyof TConfigSchema]: ReturnType; + }> + > + > { + return createExtension({ + kind: this.options.kind, + namespace: args.namespace ?? this.options.namespace, + name: args.name ?? this.options.name, + attachTo: args.attachTo ?? this.options.attachTo, + disabled: args.disabled ?? this.options.disabled, + inputs: this.options.inputs, + output: this.options.output, + config: this.options.config, + factory: ctx => this.options.factory(args.params, ctx), + } as CreateExtensionOptions); + } } /** diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index ce7da3701d..4bfc8392cb 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -19,7 +19,6 @@ import { MemoryRouter, Link } from 'react-router-dom'; import { RenderResult, render } from '@testing-library/react'; import { createSpecializedApp } from '@backstage/frontend-app-api'; import { - ExtensionDataValue, AppNode, AppTree, Extension, @@ -169,9 +168,7 @@ export class ExtensionTester { : [...internal.output, coreExtensionData.routePath], factory: params => { const parentOutput = Array.from( - internal.factory(params as any) as Iterable< - ExtensionDataValue - >, + internal.factory(params as any), ).filter(val => val.id !== coreExtensionData.routePath.id); return [...parentOutput, coreExtensionData.routePath('/')]; From 264e10f9cd1ae5cf0666f5e5f207d17e91a5b476 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 9 Aug 2024 15:43:33 +0200 Subject: [PATCH 17/18] chore: added changeset Signed-off-by: blam --- .changeset/tiny-dodos-prove-2.md | 5 +++++ .changeset/tiny-dodos-prove.md | 9 +++++++++ 2 files changed, 14 insertions(+) create mode 100644 .changeset/tiny-dodos-prove-2.md create mode 100644 .changeset/tiny-dodos-prove.md diff --git a/.changeset/tiny-dodos-prove-2.md b/.changeset/tiny-dodos-prove-2.md new file mode 100644 index 0000000000..91ac021c67 --- /dev/null +++ b/.changeset/tiny-dodos-prove-2.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Deprecate existing `ExtensionCreators` in favour of their new Blueprint counterparts. diff --git a/.changeset/tiny-dodos-prove.md b/.changeset/tiny-dodos-prove.md new file mode 100644 index 0000000000..6392e5050e --- /dev/null +++ b/.changeset/tiny-dodos-prove.md @@ -0,0 +1,9 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Refactor `.make` method on Blueprints into two different methods, `.make` and `.makeWithOverrides`. + +When using `createExtensionBlueprint` you can define parameters for the factory function, if you wish to take advantage of these parameters you should use `.make` when creating an extension instance of a Blueprint. If you wish to override more things other than the standard `attachTo`, `name`, `namespace` then you should use `.makeWithOverrides` instead. + +`.make` is reserved for simple creation of extension instances from Blueprints using higher level parameters, whereas `.makeWithOverrides` is lower level and you have more control over the final extension. From 0e615076857067573e03b8d13ea0e250083ae54e Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 9 Aug 2024 15:45:57 +0200 Subject: [PATCH 18/18] chore: updating api-reports and documentation for exported blueprints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Co-authored-by: Fredrik Adelöw Co-authored-by: Camila Belo Signed-off-by: blam --- packages/frontend-plugin-api/api-report.md | 18 +++++++++--------- .../src/blueprints/ApiBlueprint.ts | 7 ++++++- .../src/blueprints/AppRootElementBlueprint.ts | 7 ++++++- .../src/blueprints/AppRootWrapperBlueprint.tsx | 8 +++++++- .../src/blueprints/NavItemBlueprint.ts | 6 +++++- .../src/blueprints/NavLogoBlueprint.ts | 6 +++++- .../src/blueprints/PageBlueprint.tsx | 6 +++++- .../src/blueprints/SignInPageBlueprint.tsx | 6 +++++- .../src/blueprints/ThemeBlueprint.ts | 6 +++++- .../src/blueprints/TranslationBlueprint.ts | 6 +++++- 10 files changed, 58 insertions(+), 18 deletions(-) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 2d3f1d3278..f39add924a 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -189,7 +189,7 @@ export type AnyRoutes = { [name in string]: RouteRef | SubRouteRef; }; -// @public (undocumented) +// @public export const ApiBlueprint: ExtensionBlueprint< 'api', undefined, @@ -261,7 +261,7 @@ export interface AppNodeSpec { readonly source?: BackstagePlugin; } -// @public (undocumented) +// @public export const AppRootElementBlueprint: ExtensionBlueprint< 'app-root-element', undefined, @@ -276,7 +276,7 @@ export const AppRootElementBlueprint: ExtensionBlueprint< never >; -// @public (undocumented) +// @public export const AppRootWrapperBlueprint: ExtensionBlueprint< 'app-root-wrapper', undefined, @@ -1696,7 +1696,7 @@ export interface LegacyExtensionInput< export { microsoftAuthApiRef }; -// @public (undocumented) +// @public export const NavItemBlueprint: ExtensionBlueprint< 'nav-item', undefined, @@ -1731,7 +1731,7 @@ export const NavItemBlueprint: ExtensionBlueprint< } >; -// @public (undocumented) +// @public export const NavLogoBlueprint: ExtensionBlueprint< 'nav-logo', undefined, @@ -1781,7 +1781,7 @@ export { oneloginAuthApiRef }; export { OpenIdConnectApi }; -// @public (undocumented) +// @public export const PageBlueprint: ExtensionBlueprint< 'page', undefined, @@ -1938,7 +1938,7 @@ export { SessionApi }; export { SessionState }; -// @public (undocumented) +// @public export const SignInPageBlueprint: ExtensionBlueprint< 'sign-in-page', undefined, @@ -1981,7 +1981,7 @@ export interface SubRouteRef< readonly T: TParams; } -// @public (undocumented) +// @public export const ThemeBlueprint: ExtensionBlueprint< 'theme', 'app', @@ -1998,7 +1998,7 @@ export const ThemeBlueprint: ExtensionBlueprint< } >; -// @public (undocumented) +// @public export const TranslationBlueprint: ExtensionBlueprint< 'translation', undefined, diff --git a/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts index da12faef1a..b5ba9e05f6 100644 --- a/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts @@ -13,11 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { createExtensionBlueprint } from '../wiring'; import { createApiExtension } from '../extensions/createApiExtension'; import { AnyApiFactory } from '@backstage/core-plugin-api'; -/** @public */ +/** + * Creates utility API extensions. + * + * @public + */ export const ApiBlueprint = createExtensionBlueprint({ kind: 'api', attachTo: { id: 'app', input: 'apis' }, diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts index e44942979d..bc6304923a 100644 --- a/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts @@ -15,7 +15,12 @@ */ import { coreExtensionData, createExtensionBlueprint } from '../wiring'; -/** @public */ +/** + * Creates extensions that render a React element at the app root, outside of + * the app layout. This is useful for example for shared popups and similar. + * + * @public + */ export const AppRootElementBlueprint = createExtensionBlueprint({ kind: 'app-root-element', attachTo: { id: 'app/root', input: 'elements' }, diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx index db63550880..c54686f54f 100644 --- a/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx @@ -19,7 +19,13 @@ import { ComponentType, PropsWithChildren } from 'react'; import { createExtensionBlueprint } from '../wiring'; import { createAppRootWrapperExtension } from '../extensions/createAppRootWrapperExtension'; -/** @public */ +/** + * Creates a extensions that render a React wrapper at the app root, enclosing + * the app layout. This is useful for example for adding global React contexts + * and similar. + * + * @public + */ export const AppRootWrapperBlueprint = createExtensionBlueprint({ kind: 'app-root-wrapper', attachTo: { id: 'app/root', input: 'wrappers' }, diff --git a/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts index 5391d0dec2..20f57421c7 100644 --- a/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts @@ -19,7 +19,11 @@ import { RouteRef } from '../routing'; import { createExtensionBlueprint } from '../wiring'; import { createNavItemExtension } from '../extensions/createNavItemExtension'; -/** @public */ +/** + * Creates extensions that make up the items of the nav bar. + * + * @public + */ export const NavItemBlueprint = createExtensionBlueprint({ kind: 'nav-item', attachTo: { id: 'app/nav', input: 'items' }, diff --git a/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts index f31b3cec9b..8066646ca3 100644 --- a/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts @@ -17,7 +17,11 @@ import { createExtensionBlueprint } from '../wiring'; import { createNavLogoExtension } from '../extensions/createNavLogoExtension'; -/** @public */ +/** + * Creates an extension that replaces the logo in the nav bar with your own. + * + * @public + */ export const NavLogoBlueprint = createExtensionBlueprint({ kind: 'nav-logo', attachTo: { id: 'app/nav', input: 'logos' }, diff --git a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx index bfcf4a7934..b224ea491e 100644 --- a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx @@ -18,7 +18,11 @@ import { RouteRef } from '../routing'; import { coreExtensionData, createExtensionBlueprint } from '../wiring'; import { ExtensionBoundary } from '../components'; -/** @public */ +/** + * Createx extensions that are routable React page components. + * + * @public + */ export const PageBlueprint = createExtensionBlueprint({ kind: 'page', attachTo: { id: 'app/routes', input: 'routes' }, diff --git a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx index 2727335d8a..f87ae91452 100644 --- a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx @@ -19,7 +19,11 @@ import { createSignInPageExtension } from '../extensions/createSignInPageExtensi import { SignInPageProps } from '@backstage/core-plugin-api'; import { ExtensionBoundary } from '../components'; -/** @public */ +/** + * Creates an extension that replaces the sign in page. + * + * @public + */ export const SignInPageBlueprint = createExtensionBlueprint({ kind: 'sign-in-page', attachTo: { id: 'app/root', input: 'signInPage' }, diff --git a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts index cd97e0437f..686c498c06 100644 --- a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts @@ -18,7 +18,11 @@ import { AppTheme } from '@backstage/core-plugin-api'; import { createExtensionBlueprint } from '../wiring'; import { createThemeExtension } from '../extensions/createThemeExtension'; -/** @public */ +/** + * Creates an extension that adds/replaces an app theme. + * + * @public + */ export const ThemeBlueprint = createExtensionBlueprint({ kind: 'theme', namespace: 'app', diff --git a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts index c70ca56f6b..bdb36e2191 100644 --- a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts @@ -18,7 +18,11 @@ import { createExtensionBlueprint } from '../wiring'; import { createTranslationExtension } from '../extensions/createTranslationExtension'; import { TranslationMessages, TranslationResource } from '../translation'; -/** @public */ +/** + * Creates an extension that adds translations to your app. + * + * @public + */ export const TranslationBlueprint = createExtensionBlueprint({ kind: 'translation', attachTo: { id: 'app', input: 'translations' },