diff --git a/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx b/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx index 7d62103727..e2c7581e63 100644 --- a/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx +++ b/packages/frontend-app-api/src/apis/implementations/ComponentsApi/DefaultComponentsApi.test.tsx @@ -40,14 +40,8 @@ const baseOverrides = createExtensionOverrides({ namespace: 'app', name: 'root', attachTo: { id: 'app', input: 'root' }, - output: { - element: coreExtensionData.reactElement, - }, - factory() { - return { - element:
root
, - }; - }, + output: [coreExtensionData.reactElement], + factory: () => [coreExtensionData.reactElement(
root
)], }), ], }); diff --git a/packages/frontend-app-api/src/extensions/App.tsx b/packages/frontend-app-api/src/extensions/App.tsx index 7794aceb79..ba6c5f65f1 100644 --- a/packages/frontend-app-api/src/extensions/App.tsx +++ b/packages/frontend-app-api/src/extensions/App.tsx @@ -18,51 +18,38 @@ import React from 'react'; import { ExtensionBoundary, coreExtensionData, - createApiExtension, createComponentExtension, createExtension, createExtensionInput, - createThemeExtension, - createTranslationExtension, IconBundleBlueprint, + ThemeBlueprint, + ApiBlueprint, + TranslationBlueprint, } from '@backstage/frontend-plugin-api'; export const App = createExtension({ namespace: 'app', attachTo: { id: 'root', input: 'default' }, // ignored inputs: { - apis: createExtensionInput({ - api: createApiExtension.factoryDataRef, + apis: createExtensionInput([ApiBlueprint.dataRefs.factory]), + themes: createExtensionInput([ThemeBlueprint.dataRefs.theme]), + components: createExtensionInput([ + createComponentExtension.componentDataRef, + ]), + translations: createExtensionInput([ + TranslationBlueprint.dataRefs.translation, + ]), + icons: createExtensionInput([IconBundleBlueprint.dataRefs.icons]), + root: createExtensionInput([coreExtensionData.reactElement], { + singleton: true, }), - themes: createExtensionInput({ - theme: createThemeExtension.themeDataRef, - }), - components: createExtensionInput({ - component: createComponentExtension.componentDataRef, - }), - translations: createExtensionInput({ - translation: createTranslationExtension.translationDataRef, - }), - icons: createExtensionInput({ - icon: IconBundleBlueprint.dataRefs.icons, - }), - root: createExtensionInput( - { - element: coreExtensionData.reactElement, - }, - { singleton: true }, + }, + output: [coreExtensionData.reactElement], + factory: ({ node, inputs }) => [ + coreExtensionData.reactElement( + + {inputs.root.get(coreExtensionData.reactElement)} + , ), - }, - output: { - root: coreExtensionData.reactElement, - }, - factory({ node, inputs }) { - return { - root: ( - - {inputs.root.output.element} - - ), - }; - }, + ], }); diff --git a/packages/frontend-app-api/src/extensions/AppLayout.tsx b/packages/frontend-app-api/src/extensions/AppLayout.tsx index 7be0029c25..9fadba8a93 100644 --- a/packages/frontend-app-api/src/extensions/AppLayout.tsx +++ b/packages/frontend-app-api/src/extensions/AppLayout.tsx @@ -27,30 +27,20 @@ export const AppLayout = createExtension({ name: 'layout', attachTo: { id: 'app/root', input: 'children' }, inputs: { - nav: createExtensionInput( - { - element: coreExtensionData.reactElement, - }, - { singleton: true }, + nav: createExtensionInput([coreExtensionData.reactElement], { + singleton: true, + }), + content: createExtensionInput([coreExtensionData.reactElement], { + singleton: true, + }), + }, + output: [coreExtensionData.reactElement], + factory: ({ inputs }) => [ + coreExtensionData.reactElement( + + {inputs.nav.get(coreExtensionData.reactElement)} + {inputs.content.get(coreExtensionData.reactElement)} + , ), - content: createExtensionInput( - { - element: coreExtensionData.reactElement, - }, - { singleton: true }, - ), - }, - output: { - element: coreExtensionData.reactElement, - }, - factory({ inputs }) { - return { - element: ( - - {inputs.nav.output.element} - {inputs.content.output.element} - - ), - }; - }, + ], }); diff --git a/packages/frontend-app-api/src/extensions/AppNav.tsx b/packages/frontend-app-api/src/extensions/AppNav.tsx index ffef1c7c61..49c95e84f2 100644 --- a/packages/frontend-app-api/src/extensions/AppNav.tsx +++ b/packages/frontend-app-api/src/extensions/AppNav.tsx @@ -86,33 +86,27 @@ export const AppNav = createExtension({ name: 'nav', attachTo: { id: 'app/layout', input: 'nav' }, inputs: { - items: createExtensionInput({ - target: createNavItemExtension.targetDataRef, + items: createExtensionInput([createNavItemExtension.targetDataRef]), + logos: createExtensionInput([createNavLogoExtension.logoElementsDataRef], { + singleton: true, + optional: true, }), - logos: createExtensionInput( - { - elements: createNavLogoExtension.logoElementsDataRef, - }, - { - singleton: true, - optional: true, - }, + }, + output: [coreExtensionData.reactElement], + factory: ({ inputs }) => [ + coreExtensionData.reactElement( + + + + {inputs.items.map((item, index) => ( + + ))} + , ), - }, - output: { - element: coreExtensionData.reactElement, - }, - factory({ inputs }) { - return { - element: ( - - - - {inputs.items.map((item, index) => ( - - ))} - - ), - }; - }, + ], }); diff --git a/packages/frontend-app-api/src/extensions/AppRoot.tsx b/packages/frontend-app-api/src/extensions/AppRoot.tsx index f3a962dd10..a158386d04 100644 --- a/packages/frontend-app-api/src/extensions/AppRoot.tsx +++ b/packages/frontend-app-api/src/extensions/AppRoot.tsx @@ -23,12 +23,12 @@ import React, { useState, } from 'react'; import { + AppRootWrapperBlueprint, + RouterBlueprint, + SignInPageBlueprint, coreExtensionData, - createAppRootWrapperExtension, createExtension, createExtensionInput, - createRouterExtension, - createSignInPageExtension, } from '@backstage/frontend-plugin-api'; import { IdentityApi, @@ -48,52 +48,54 @@ export const AppRoot = createExtension({ name: 'root', attachTo: { id: 'app', input: 'root' }, inputs: { - router: createExtensionInput( - { component: createRouterExtension.componentDataRef }, - { singleton: true, optional: true }, - ), - signInPage: createExtensionInput( - { component: createSignInPageExtension.componentDataRef }, - { singleton: true, optional: true }, - ), - children: createExtensionInput( - { element: coreExtensionData.reactElement }, - { singleton: true }, - ), - elements: createExtensionInput({ - element: coreExtensionData.reactElement, + router: createExtensionInput([RouterBlueprint.dataRefs.component], { + singleton: true, + optional: true, }), - wrappers: createExtensionInput({ - component: createAppRootWrapperExtension.componentDataRef, + signInPage: createExtensionInput([SignInPageBlueprint.dataRefs.component], { + singleton: true, + optional: true, }), + children: createExtensionInput([coreExtensionData.reactElement], { + singleton: true, + }), + elements: createExtensionInput([coreExtensionData.reactElement]), + wrappers: createExtensionInput([ + AppRootWrapperBlueprint.dataRefs.component, + ]), }, - output: { - element: coreExtensionData.reactElement, - }, + output: [coreExtensionData.reactElement], factory({ inputs }) { let content: React.ReactNode = ( <> {inputs.elements.map(el => ( - {el.output.element} + + {el.get(coreExtensionData.reactElement)} + ))} - {inputs.children.output.element} + {inputs.children.get(coreExtensionData.reactElement)} ); for (const wrapper of inputs.wrappers) { - content = {content}; + const Component = wrapper.get(AppRootWrapperBlueprint.dataRefs.component); + content = {content}; } - return { - element: ( + return [ + coreExtensionData.reactElement( {content} - + , ), - }; + ]; }, }); diff --git a/packages/frontend-app-api/src/extensions/AppRoutes.tsx b/packages/frontend-app-api/src/extensions/AppRoutes.tsx index 38a9291755..6ef7e2a271 100644 --- a/packages/frontend-app-api/src/extensions/AppRoutes.tsx +++ b/packages/frontend-app-api/src/extensions/AppRoutes.tsx @@ -29,15 +29,13 @@ export const AppRoutes = createExtension({ name: 'routes', attachTo: { id: 'app/layout', input: 'content' }, inputs: { - routes: createExtensionInput({ - path: coreExtensionData.routePath, - ref: coreExtensionData.routeRef.optional(), - element: coreExtensionData.reactElement, - }), - }, - output: { - element: coreExtensionData.reactElement, + routes: createExtensionInput([ + coreExtensionData.routePath, + coreExtensionData.routeRef.optional(), + coreExtensionData.reactElement, + ]), }, + output: [coreExtensionData.reactElement], factory({ inputs }) { const Routes = () => { const NotFoundErrorPage = useComponentRef( @@ -46,8 +44,8 @@ export const AppRoutes = createExtension({ const element = useRoutes([ ...inputs.routes.map(route => ({ - path: `${route.output.path}/*`, - element: route.output.element, + path: `${route.get(coreExtensionData.routePath)}/*`, + element: route.get(coreExtensionData.reactElement), })), { path: '*', @@ -58,8 +56,6 @@ export const AppRoutes = createExtension({ return element; }; - return { - element: , - }; + return [coreExtensionData.reactElement()]; }, }); diff --git a/packages/frontend-app-api/src/extensions/elements.tsx b/packages/frontend-app-api/src/extensions/elements.tsx index 0a764583bb..1ab7f97ba2 100644 --- a/packages/frontend-app-api/src/extensions/elements.tsx +++ b/packages/frontend-app-api/src/extensions/elements.tsx @@ -15,31 +15,36 @@ */ import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; -import { - createAppRootElementExtension, - createSchemaFromZod, -} from '@backstage/frontend-plugin-api'; +import { AppRootElementBlueprint } from '@backstage/frontend-plugin-api'; import React from 'react'; -export const oauthRequestDialogAppRootElement = createAppRootElementExtension({ +export const oauthRequestDialogAppRootElement = AppRootElementBlueprint.make({ namespace: 'app', name: 'oauth-request-dialog', - element: , + params: { + element: , + }, }); -export const alertDisplayAppRootElement = createAppRootElementExtension({ - namespace: 'app', - name: 'alert-display', - configSchema: createSchemaFromZod(z => - z.object({ - transientTimeoutMs: z.number().default(5000), - anchorOrigin: z - .object({ - vertical: z.enum(['top', 'bottom']).default('top'), - horizontal: z.enum(['left', 'center', 'right']).default('center'), - }) - .default({}), - }), - ), - element: ({ config }) => , -}); +export const alertDisplayAppRootElement = + AppRootElementBlueprint.makeWithOverrides({ + namespace: 'app', + name: 'alert-display', + config: { + schema: { + transientTimeoutMs: z => z.number().default(5000), + anchorOrigin: z => + z + .object({ + vertical: z.enum(['top', 'bottom']).default('top'), + horizontal: z.enum(['left', 'center', 'right']).default('center'), + }) + .default({}), + }, + }, + factory: (originalFactory, { config }) => { + return originalFactory({ + element: () => , + }); + }, + }); diff --git a/packages/frontend-app-api/src/extensions/themes.tsx b/packages/frontend-app-api/src/extensions/themes.tsx index ea11b6a638..a4fd0b253e 100644 --- a/packages/frontend-app-api/src/extensions/themes.tsx +++ b/packages/frontend-app-api/src/extensions/themes.tsx @@ -21,24 +21,34 @@ import { } from '@backstage/theme'; import DarkIcon from '@material-ui/icons/Brightness2'; import LightIcon from '@material-ui/icons/WbSunny'; -import { createThemeExtension } from '@backstage/frontend-plugin-api'; +import { ThemeBlueprint } from '@backstage/frontend-plugin-api'; -export const LightTheme = createThemeExtension({ - id: 'light', - title: 'Light Theme', - variant: 'light', - icon: , - Provider: ({ children }) => ( - - ), +export const LightTheme = ThemeBlueprint.make({ + name: 'light', + params: { + theme: { + id: 'light', + title: 'Light Theme', + variant: 'light', + icon: , + Provider: ({ children }) => ( + + ), + }, + }, }); -export const DarkTheme = createThemeExtension({ - id: 'dark', - title: 'Dark Theme', - variant: 'dark', - icon: , - Provider: ({ children }) => ( - - ), +export const DarkTheme = ThemeBlueprint.make({ + name: 'dark', + params: { + theme: { + id: 'dark', + title: 'Dark Theme', + variant: 'dark', + icon: , + Provider: ({ children }) => ( + + ), + }, + }, }); diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts index c22cb6ff1d..8a54f3e73e 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts @@ -50,22 +50,23 @@ function createTestExtension(options: { attachTo: options.parent ? { id: `test/${options.parent}`, input: 'children' } : { id: 'app/routes', input: 'routes' }, - output: { - element: coreExtensionData.reactElement, - path: coreExtensionData.routePath.optional(), - routeRef: coreExtensionData.routeRef.optional(), - }, + output: [ + coreExtensionData.reactElement, + coreExtensionData.routePath.optional(), + coreExtensionData.routeRef.optional(), + ], inputs: { - children: createExtensionInput({ - element: coreExtensionData.reactElement, - }), + children: createExtensionInput([coreExtensionData.reactElement]), }, - factory() { - return { - path: options.path, - routeRef: options.routeRef, - element: React.createElement('div'), - }; + *factory() { + if (options.path) { + yield coreExtensionData.routePath(options.path); + } + if (options.routeRef) { + yield coreExtensionData.routeRef(options.routeRef); + } + + yield coreExtensionData.reactElement(React.createElement('div')); }, }); } diff --git a/packages/frontend-app-api/src/tree/createAppTree.test.ts b/packages/frontend-app-api/src/tree/createAppTree.test.ts index f24f199d5d..f5df5270cd 100644 --- a/packages/frontend-app-api/src/tree/createAppTree.test.ts +++ b/packages/frontend-app-api/src/tree/createAppTree.test.ts @@ -25,8 +25,8 @@ import { createAppTree } from './createAppTree'; const extBase = { id: 'test', attachTo: { id: 'app', input: 'root' }, - output: {}, - factory: () => ({}), + output: [], + factory: () => [], }; describe('createAppTree', () => { @@ -60,8 +60,8 @@ describe('createAppTree', () => { name: 'app', attachTo: { id: 'app/routes', input: 'route' }, inputs: {}, - output: {}, - factory: () => ({}), + output: [], + factory: () => [], }), ], }), diff --git a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts index d9b65555fe..740f05d5f7 100644 --- a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts @@ -23,8 +23,8 @@ const extension = resolveExtensionDefinition( createExtension({ name: 'test', attachTo: { id: 'nonexistent', input: 'nonexistent' }, - output: {}, - factory: () => ({}), + output: [], + factory: () => [], }), ) as Extension; diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx index ead9422a13..c79d8eed76 100644 --- a/packages/frontend-app-api/src/wiring/createApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx @@ -20,9 +20,9 @@ import { coreExtensionData, createExtension, createExtensionOverrides, - createPageExtension, + PageBlueprint, createFrontendPlugin, - createThemeExtension, + ThemeBlueprint, } from '@backstage/frontend-plugin-api'; import { screen, waitFor } from '@testing-library/react'; import { CreateAppFeatureLoader, createApp } from './createApp'; @@ -47,11 +47,16 @@ describe('createApp', () => { createFrontendPlugin({ id: 'test', extensions: [ - createThemeExtension({ - id: 'derp', - title: 'Derp', - variant: 'dark', - Provider: () =>
Derp
, + ThemeBlueprint.make({ + name: 'derp', + params: { + theme: { + id: 'derp', + title: 'Derp', + variant: 'dark', + Provider: () =>
Derp
, + }, + }, }), ], }), @@ -71,18 +76,22 @@ describe('createApp', () => { createFrontendPlugin({ id: duplicatedFeatureId, extensions: [ - createPageExtension({ - defaultPath: '/', - loader: async () =>
First Page
, + PageBlueprint.make({ + params: { + defaultPath: '/', + loader: async () =>
First Page
, + }, }), ], }), createFrontendPlugin({ id: duplicatedFeatureId, extensions: [ - createPageExtension({ - defaultPath: '/', - loader: async () =>
Last Page
, + PageBlueprint.make({ + params: { + defaultPath: '/', + loader: async () =>
Last Page
, + }, }), ], }), @@ -110,9 +119,11 @@ describe('createApp', () => { createFrontendPlugin({ id: 'test', extensions: [ - createPageExtension({ - defaultPath: '/', - loader: async () =>
{config.getString('key')}
, + PageBlueprint.make({ + params: { + defaultPath: '/', + loader: async () =>
{config.getString('key')}
, + }, }), ], }), @@ -170,7 +181,7 @@ describe('createApp', () => { createExtension({ name: 'first', attachTo: { id: 'app', input: 'root' }, - output: { element: coreExtensionData.reactElement }, + output: [coreExtensionData.reactElement], factory() { const Component = () => { const flagsApi = useApi(featureFlagsApiRef); @@ -184,7 +195,7 @@ describe('createApp', () => { ); }; - return { element: }; + return [coreExtensionData.reactElement()]; }, }), ], @@ -197,8 +208,8 @@ describe('createApp', () => { name: 'root', attachTo: { id: 'app', input: 'root' }, disabled: true, - output: {}, - factory: () => ({}), + output: [], + factory: () => [], }), ], }), @@ -221,14 +232,16 @@ describe('createApp', () => { createFrontendPlugin({ id: 'my-plugin', extensions: [ - createPageExtension({ - defaultPath: '/', - loader: async () => { - const Component = () => { - appTreeApi = useApi(appTreeApiRef); - return
My Plugin Page
; - }; - return ; + PageBlueprint.make({ + params: { + defaultPath: '/', + loader: async () => { + const Component = () => { + appTreeApi = useApi(appTreeApiRef); + return
My Plugin Page
; + }; + return ; + }, }, }), ], @@ -250,7 +263,7 @@ describe('createApp', () => { content [ routes [ - + ] ] diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index 6f23f4cf2c..ca42dce3e4 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -17,18 +17,18 @@ import React, { JSX, ReactNode } from 'react'; import { ConfigReader } from '@backstage/config'; import { + ApiBlueprint, AppTree, appTreeApiRef, componentsApiRef, coreExtensionData, - createApiExtension, - createThemeExtension, - createTranslationExtension, FrontendFeature, IconBundleBlueprint, iconsApiRef, RouteResolutionApi, routeResolutionApiRef, + ThemeBlueprint, + TranslationBlueprint, } from '@backstage/frontend-plugin-api'; import { App } from '../extensions/App'; import { AppRoutes } from '../extensions/AppRoutes'; @@ -110,7 +110,9 @@ import { stringifyError } from '@backstage/errors'; import { icons as defaultIcons } from '../../../app-defaults/src/defaults'; import { getBasePath } from '../routing/getBasePath'; -const DefaultApis = defaultApis.map(factory => createApiExtension({ factory })); +const DefaultApis = defaultApis.map(factory => + ApiBlueprint.make({ namespace: factory.api.id, params: { factory } }), +); export const builtinExtensions = [ App, @@ -357,23 +359,21 @@ function createApiHolder( const pluginApis = tree.root.edges.attachments .get('apis') - ?.map(e => e.instance?.getData(createApiExtension.factoryDataRef)) + ?.map(e => e.instance?.getData(ApiBlueprint.dataRefs.factory)) .filter((x): x is AnyApiFactory => !!x) ?? []; const themeExtensions = tree.root.edges.attachments .get('themes') - ?.map(e => e.instance?.getData(createThemeExtension.themeDataRef)) + ?.map(e => e.instance?.getData(ThemeBlueprint.dataRefs.theme)) .filter((x): x is AppTheme => !!x) ?? []; const translationResources = tree.root.edges.attachments .get('translations') - ?.map(e => - e.instance?.getData(createTranslationExtension.translationDataRef), - ) + ?.map(e => e.instance?.getData(TranslationBlueprint.dataRefs.translation)) .filter( - (x): x is typeof createTranslationExtension.translationDataRef.T => !!x, + (x): x is typeof TranslationBlueprint.dataRefs.translation.T => !!x, ) ?? []; const extensionIcons = tree.root.edges.attachments