From 66523e8d01fbb57e35bcfd164c20b11ba7fcae26 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 30 Jul 2024 15:47:45 +0200 Subject: [PATCH] chore: wip Signed-off-by: blam --- packages/app-next/src/App.tsx | 48 ++++++- .../src/extensions/createPageExtension.tsx | 131 ++++++++++++++++++ plugins/home/src/alpha.tsx | 58 +++++++- 3 files changed, 223 insertions(+), 14 deletions(-) diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index b57c680474..25d1aacf8f 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -20,6 +20,7 @@ import { pagesPlugin } from './examples/pagesPlugin'; import notFoundErrorPage from './examples/notFoundErrorPageExtension'; import userSettingsPlugin from '@backstage/plugin-user-settings/alpha'; import homePlugin, { + extensions, titleExtensionDataRef, } from '@backstage/plugin-home/alpha'; @@ -74,13 +75,46 @@ TODO: /* app.tsx */ -const homePageExtension = createExtension({ - name: 'myhomepage', - attachTo: { id: 'page:home', input: 'props' }, - output: { - children: coreExtensionData.reactElement, - title: titleExtensionDataRef, - }, +// const homePageExtension = createExtension({ +// name: 'myhomepage', +// attachTo: { id: 'page:home', input: 'props' }, +// output: { +// children: coreExtensionData.reactElement, +// title: titleExtensionDataRef, +// }, +// factory() { +// return { children: homePage, title: 'just a title' }; +// }, +// }); + +// const homePage = createPageExtension({ +// defaultPath: '/home', +// routeRef: rootRouteRef, +// inputs: { +// props: createExtensionInput( +// { +// children: coreExtensionData.reactElement.optional(), +// title: titleExtensionDataRef.optional(), +// }, + +// { +// singleton: true, +// optional: true, +// }, +// ), +// }, +// loader: ({ inputs }) => +// import('./components/').then(m => +// compatWrapper( +// , +// ), +// ), +// }); + +const homePageExtension = extensions.homePage.override({ factory() { return { children: homePage, title: 'just a title' }; }, diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index cd434a3ffc..b10901f1bd 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -22,6 +22,9 @@ import { createExtension, ResolvedExtensionInputs, AnyExtensionInputMap, + createExtensionBlueprint, + ExtensionInput, + AnyExtensionDataRef, } from '../wiring'; import { RouteRef } from '../routing'; import { Expand } from '../types'; @@ -95,3 +98,131 @@ export function createPageExtension< }, }); } + +export function createNewPageExtension< + TConfig extends { path: string }, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, +>( + options: ( + | { + defaultPath: string; + } + | { + configSchema: PortableSchema; + } + ) & { + namespace?: string; + name?: string; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + routeRef?: RouteRef; + loader: (options: { + config: TConfig; + inputs: Expand>; + }) => Promise; + }, +): ExtensionDefinition { + const configSchema = + 'configSchema' in options + ? options.configSchema + : (createSchemaFromZod(z => + z.object({ path: z.string().default(options.defaultPath) }), + ) as PortableSchema); + + return createExtension({ + kind: 'page', + namespace: options.namespace, + name: options.name, + attachTo: options.attachTo ?? { id: 'app/routes', input: 'routes' }, + configSchema, + inputs: options.inputs, + disabled: options.disabled, + output: [ + coreExtensionData.routePath, + coreExtensionData.reactElement, + coreExtensionData.routeRef.optional(), + ], + factory({ config, inputs, node }) { + const ExtensionComponent = lazy(() => + options + .loader({ config, inputs }) + .then(element => ({ default: () => element })), + ); + + const outputs = [ + coreExtensionData.routePath(config.path), + coreExtensionData.reactElement( + + + , + ), + ]; + + if (options.routeRef) { + return [...outputs, coreExtensionData.routeRef(options.routeRef)]; + } + + return outputs; + }, + }); +} +/** + * A blueprint for creating extensions for routable React page components. + * @public + */ +export const PageExtensionBlueprint = 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; + // TODO(blam) This type is impossible to type properly here as we don't have access + // to the input type generic. Maybe not a deal breaker though. + // It means we have to override the factory function in the `.make` method instead. + loader: (opts: unknown) => 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; + }, +}); diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index b13c2a2e0e..109eea333e 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -25,6 +25,10 @@ import { createRouteRef, } from '@backstage/frontend-plugin-api'; import { compatWrapper } from '@backstage/core-compat-api'; +import { + PageExtensionBlueprint, + createNewPageExtension, +} from '@backstage/frontend-plugin-api/src/extensions/createPageExtension'; const rootRouteRef = createRouteRef(); @@ -35,15 +39,15 @@ export const titleExtensionDataRef = createExtensionDataRef().with({ id: 'title', }); -const homePage = createPageExtension({ +const homePage = createNewPageExtension({ defaultPath: '/home', routeRef: rootRouteRef, inputs: { props: createExtensionInput( - { - children: coreExtensionData.reactElement.optional(), - title: titleExtensionDataRef.optional(), - }, + [ + coreExtensionData.reactElement.optional(), + titleExtensionDataRef.optional(), + ], { singleton: true, @@ -55,13 +59,53 @@ const homePage = createPageExtension({ import('./components/').then(m => compatWrapper( , ), ), }); +homePage.override({ + factory() {}, +}); + +// const homePage2 = PageExtensionBlueprint.make({ +// inputs: { +// props: createExtensionInput( +// [ +// coreExtensionData.reactElement.optional(), +// titleExtensionDataRef.optional(), +// ], +// { +// singleton: true, +// optional: true, +// }, +// ), +// }, +// factory(origFactory, { config, inputs, node }) { +// return origFactory({ +// defaultPath: '/home', +// routeRef: rootRouteRef, +// loader: () => +// import('./components/').then(m => +// compatWrapper( +// , +// ), +// }, +// }); + +/** + * @alpha + * This will not be the way to export extensions eventually, + * will be something like `homePlugin.extensions.homePage` or `homePlugin.extensions.get('page')` + * Just haven't worked out a nice way to fix the types yet + */ +export const extensions = { homePage }; + /** * @alpha */