diff --git a/packages/app-next/src/examples/notFoundErrorPageExtension.tsx b/packages/app-next/src/examples/notFoundErrorPageExtension.tsx index 93807a1757..0e1fbd40f8 100644 --- a/packages/app-next/src/examples/notFoundErrorPageExtension.tsx +++ b/packages/app-next/src/examples/notFoundErrorPageExtension.tsx @@ -15,7 +15,10 @@ */ import React from 'react'; -import { createNotFoundErrorPageExtension } from '@backstage/frontend-plugin-api'; +import { + createComponentExtension, + coreNotFoundErrorPageComponentRef, +} from '@backstage/frontend-plugin-api'; import { Box, Typography } from '@material-ui/core'; import { Button } from '@backstage/core-components'; @@ -53,6 +56,7 @@ function CustomNotFoundErrorPage() { ); } -export default createNotFoundErrorPageExtension({ +export default createComponentExtension({ + ref: coreNotFoundErrorPageComponentRef, component: async () => CustomNotFoundErrorPage, }); diff --git a/packages/frontend-app-api/src/extensions/Core.tsx b/packages/frontend-app-api/src/extensions/Core.tsx index 84407d1f09..441cb5bcf0 100644 --- a/packages/frontend-app-api/src/extensions/Core.tsx +++ b/packages/frontend-app-api/src/extensions/Core.tsx @@ -14,8 +14,6 @@ * limitations under the License. */ -import React from 'react'; - import { coreExtensionData, createExtension, @@ -32,12 +30,9 @@ export const Core = createExtension({ themes: createExtensionInput({ theme: coreExtensionData.theme, }), - components: createExtensionInput( - { - provider: coreExtensionData.reactComponent, - }, - { singleton: true }, - ), + components: createExtensionInput({ + component: coreExtensionData.component, + }), root: createExtensionInput( { element: coreExtensionData.reactElement, @@ -50,11 +45,7 @@ export const Core = createExtension({ }, factory({ inputs }) { return { - root: ( - - {inputs.root.element} - - ), + root: inputs.root.element, }; }, }); diff --git a/packages/frontend-app-api/src/extensions/CoreComponents.tsx b/packages/frontend-app-api/src/extensions/CoreComponents.tsx deleted file mode 100644 index d951983331..0000000000 --- a/packages/frontend-app-api/src/extensions/CoreComponents.tsx +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2023 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, { PropsWithChildren, useMemo } from 'react'; -import { - createExtension, - coreExtensionData, - createExtensionInput, - createProgressExtension, - createBootErrorPageExtension, - createNotFoundErrorPageExtension, - createErrorBoundaryFallbackExtension, -} from '@backstage/frontend-plugin-api'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { components as defaultComponents } from '../../../app-defaults/src/defaults'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { AppContextProvider } from '../../../core-app-api/src/app/AppContext'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { useApp } from '../../../core-plugin-api/src/app/useApp'; - -export const DefaultProgressComponent = createProgressExtension({ - component: async () => defaultComponents.Progress, -}); - -export const DefaultBootErrorPageComponent = createBootErrorPageExtension({ - component: async () => defaultComponents.BootErrorPage, -}); - -export const DefaultNotFoundErrorPageComponent = - createNotFoundErrorPageExtension({ - component: async () => defaultComponents.NotFoundErrorPage, - }); - -export const DefaultErrorBoundaryComponent = - createErrorBoundaryFallbackExtension({ - component: async () => defaultComponents.ErrorBoundaryFallback, - }); - -export const CoreComponents = createExtension({ - id: 'core.components', - attachTo: { id: 'core', input: 'components' }, - inputs: { - progress: createExtensionInput( - { - component: coreExtensionData.components.progress, - }, - { - optional: true, - singleton: true, - }, - ), - bootErrorPage: createExtensionInput( - { - component: coreExtensionData.components.bootErrorPage, - }, - { - optional: true, - singleton: true, - }, - ), - notFoundErrorPage: createExtensionInput( - { - component: coreExtensionData.components.notFoundErrorPage, - }, - { - optional: true, - singleton: true, - }, - ), - errorBoundaryFallback: createExtensionInput( - { - component: coreExtensionData.components.errorBoundaryFallback, - }, - { - optional: true, - singleton: true, - }, - ), - }, - output: { - provider: coreExtensionData.reactComponent, - }, - factory({ bind, inputs }) { - bind({ - provider: function Provider(props: PropsWithChildren<{}>) { - const { children } = props; - const app = useApp(); - - const context = useMemo( - () => ({ - ...app, - // Only override components - getComponents: () => { - const { - progress, - bootErrorPage, - notFoundErrorPage, - errorBoundaryFallback, - } = inputs; - - const { - Progress, - BootErrorPage, - NotFoundErrorPage, - ErrorBoundaryFallback, - ...components - } = app.getComponents(); - - return { - ...components, - Progress: progress?.component ?? Progress, - BootErrorPage: bootErrorPage?.component ?? BootErrorPage, - NotFoundErrorPage: - notFoundErrorPage?.component ?? NotFoundErrorPage, - ErrorBoundaryFallback: - errorBoundaryFallback?.component ?? ErrorBoundaryFallback, - }; - }, - }), - [app], - ); - - return ( - - {children} - - ); - }, - }); - }, -}); diff --git a/packages/frontend-app-api/src/extensions/components.tsx b/packages/frontend-app-api/src/extensions/components.tsx new file mode 100644 index 0000000000..5bd4431a28 --- /dev/null +++ b/packages/frontend-app-api/src/extensions/components.tsx @@ -0,0 +1,46 @@ +/* + * Copyright 2023 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 { + createComponentExtension, + coreProgressComponentRef, + coreBootErrorPageComponentRef, + coreNotFoundErrorPageComponentRef, + coreErrorBoundaryFallbackComponentRef, +} from '@backstage/frontend-plugin-api'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { components as defaultComponents } from '../../../app-defaults/src/defaults'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports + +export const DefaultProgressComponent = createComponentExtension({ + ref: coreProgressComponentRef, + component: async () => defaultComponents.Progress, +}); + +export const DefaultBootErrorPageComponent = createComponentExtension({ + ref: coreBootErrorPageComponentRef, + component: async () => defaultComponents.BootErrorPage, +}); + +export const DefaultNotFoundErrorPageComponent = createComponentExtension({ + ref: coreNotFoundErrorPageComponentRef, + component: async () => defaultComponents.NotFoundErrorPage, +}); + +export const DefaultErrorBoundaryComponent = createComponentExtension({ + ref: coreErrorBoundaryFallbackComponentRef, + component: async () => defaultComponents.ErrorBoundaryFallback, +}); diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts index ffab978217..a06d236b77 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts @@ -30,12 +30,7 @@ import { } from '@backstage/frontend-plugin-api'; import { MockConfigApi } from '@backstage/test-utils'; import { createAppTree } from '../tree'; -import { Core } from '../extensions/Core'; -import { CoreRoutes } from '../extensions/CoreRoutes'; -import { CoreNav } from '../extensions/CoreNav'; -import { CoreLayout } from '../extensions/CoreLayout'; -import { CoreRouter } from '../extensions/CoreRouter'; -import { CoreComponents } from '../extensions/CoreComponents'; +import { builtinExtensions } from '../wiring/createApp'; const ref1 = createRouteRef(); const ref2 = createRouteRef(); @@ -81,15 +76,8 @@ function routeInfoFromExtensions(extensions: Extension[]) { extensions, }); const tree = createAppTree({ + builtinExtensions, config: new MockConfigApi({}), - builtinExtensions: [ - Core, - CoreRoutes, - CoreNav, - CoreLayout, - CoreRouter, - CoreComponents, - ], features: [plugin], }); diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index deaeded6f1..b22d258eae 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -20,7 +20,12 @@ import { AppTree, appTreeApiRef, BackstagePlugin, + ComponentRef, + coreBootErrorPageComponentRef, + coreErrorBoundaryFallbackComponentRef, coreExtensionData, + coreNotFoundErrorPageComponentRef, + coreProgressComponentRef, ExtensionDataRef, ExtensionOverrides, RouteRef, @@ -90,12 +95,11 @@ import { resolveRouteBindings } from '../routing/resolveRouteBindings'; import { collectRouteIds } from '../routing/collectRouteIds'; import { createAppTree } from '../tree'; import { - CoreComponents, DefaultProgressComponent, DefaultErrorBoundaryComponent, DefaultBootErrorPageComponent, DefaultNotFoundErrorPageComponent, -} from '../extensions/CoreComponents'; +} from '../extensions/components'; import { AppNode } from '@backstage/frontend-plugin-api'; import { toLegacyPlugin } from '../routing/toLegacyPlugin'; import { InternalAppContext } from './InternalAppContext'; @@ -105,13 +109,12 @@ import { toInternalBackstagePlugin } from '../../../frontend-plugin-api/src/wiri // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides'; -const builtinExtensions = [ +export const builtinExtensions = [ Core, CoreRouter, CoreRoutes, CoreNav, CoreLayout, - CoreComponents, DefaultProgressComponent, DefaultErrorBoundaryComponent, DefaultBootErrorPageComponent, @@ -316,6 +319,7 @@ export function createSpecializedApp(options?: { features.filter( (f): f is BackstagePlugin => f.$$type === '@backstage/BackstagePlugin', ), + tree.root, ); const appIdentityProxy = new AppIdentityProxy(); @@ -371,7 +375,49 @@ export function createSpecializedApp(options?: { }; } -function createLegacyAppContext(plugins: BackstagePlugin[]): AppContext { +function createLegacyAppContext( + plugins: BackstagePlugin[], + core: AppNode, +): AppContext { + const components = core.edges.attachments + .get('components') + ?.map(e => e.instance?.getData(coreExtensionData.component)); + + function getComponent>( + ref: TRef, + ): TRef['T'] | undefined { + return components?.find(component => component?.ref.id === ref.id)?.impl; + } + + const { + Progress: DefaultProgress, + BootErrorPage: DefaultBootErrorPage, + NotFoundErrorPage: DefaultNotFoundErrorPage, + ErrorBoundaryFallback: DefaultErrorBoundaryFallback, + ...rest + } = defaultComponents; + + const CustomProgress = getComponent(coreProgressComponentRef); + + const CustomBootErrorPage = getComponent(coreBootErrorPageComponentRef); + + const CustomNotFoundErrorPage = getComponent( + coreNotFoundErrorPageComponentRef, + ); + + const CustomErrorBoundaryFallback = getComponent( + coreErrorBoundaryFallbackComponentRef, + ); + + const overriddenComponents = { + ...rest, + Progress: CustomProgress ?? DefaultProgress, + BootErrorPage: CustomBootErrorPage ?? DefaultBootErrorPage, + NotFoundErrorPage: CustomNotFoundErrorPage ?? DefaultNotFoundErrorPage, + ErrorBoundaryFallback: + CustomErrorBoundaryFallback ?? DefaultErrorBoundaryFallback, + }; + return { getPlugins(): LegacyBackstagePlugin[] { return plugins.map(toLegacyPlugin); @@ -388,14 +434,7 @@ function createLegacyAppContext(plugins: BackstagePlugin[]): AppContext { }, getComponents(): AppComponents { - return { - ...defaultComponents, - // The default nullable components are overridden by the CoreComponents built-in extension - Progress: () => null, - BootErrorPage: () => null, - NotFoundErrorPage: () => null, - ErrorBoundaryFallback: () => null, - }; + return overriddenComponents; }, }; } diff --git a/packages/frontend-plugin-api/src/components/ComponentRef.tsx b/packages/frontend-plugin-api/src/components/ComponentRef.tsx new file mode 100644 index 0000000000..13588ba779 --- /dev/null +++ b/packages/frontend-plugin-api/src/components/ComponentRef.tsx @@ -0,0 +1,63 @@ +/* + * Copyright 2023 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 { + CoreBootErrorPageComponent, + CoreErrorBoundaryFallbackComponent, + CoreNotFoundErrorPageComponent, + CoreProgressComponent, +} from '../types'; + +/** @public */ +export type ComponentRef = { + id: string; + T: T; +}; + +/** @public */ +export function createComponentRef(options: { + id: string; +}): ComponentRef { + const { id } = options; + return { + id, + get T(): T { + throw new Error(`tried to read ComponentRef.T of ${id}`); + }, + }; +} + +/** @public */ +export const coreProgressComponentRef = + createComponentRef({ id: 'core.components.progress' }); + +/** @public */ +export const coreBootErrorPageComponentRef = + createComponentRef({ + id: 'core.components.bootErrorPage', + }); + +/** @public */ +export const coreNotFoundErrorPageComponentRef = + createComponentRef({ + id: 'core.components.notFoundErrorPage', + }); + +/** @public */ +export const coreErrorBoundaryFallbackComponentRef = + createComponentRef({ + id: 'core.components.errorBoundaryFallback', + }); diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx index 8fad8d8336..c9fa75c7fa 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx @@ -14,10 +14,14 @@ * limitations under the License. */ -import React, { PropsWithChildren, ReactNode, useEffect } from 'react'; +import React, { + PropsWithChildren, + ReactNode, + Suspense, + useEffect, +} from 'react'; import { AnalyticsContext, useAnalytics } from '@backstage/core-plugin-api'; import { ErrorBoundary } from './ErrorBoundary'; -import { ExtensionSuspense } from './ExtensionSuspense'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { routableExtensionRenderedEvent } from '../../../core-plugin-api/src/analytics/Tracker'; import { AppNode } from '../apis'; @@ -60,12 +64,12 @@ export function ExtensionBoundary(props: ExtensionBoundaryProps) { }; return ( - + {children} - + ); } diff --git a/packages/frontend-plugin-api/src/components/index.ts b/packages/frontend-plugin-api/src/components/index.ts index fec315fed6..4bee5c5f7b 100644 --- a/packages/frontend-plugin-api/src/components/index.ts +++ b/packages/frontend-plugin-api/src/components/index.ts @@ -14,6 +14,14 @@ * limitations under the License. */ +export { + coreProgressComponentRef, + coreBootErrorPageComponentRef, + coreErrorBoundaryFallbackComponentRef, + coreNotFoundErrorPageComponentRef, + type ComponentRef, +} from './ComponentRef'; + export { ExtensionError } from './ExtensionError'; export { diff --git a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx index cd86bbdf22..678d05049d 100644 --- a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx @@ -21,40 +21,36 @@ import { coreExtensionData, createExtension, } from '../wiring'; -import { - Expand, - CoreProgressComponent, - CoreBootErrorPageComponent, - CoreNotFoundErrorPageComponent, - CoreErrorBoundaryFallbackComponent, -} from '../types'; +import { Expand } from '../types'; import { PortableSchema } from '../schema'; -import { ExtensionError, ExtensionBoundary } from '../components'; +import { ExtensionError, ExtensionBoundary, ComponentRef } from '../components'; /** @public */ -export function createProgressExtension< +export function createComponentExtension< + TRef extends ComponentRef, TConfig extends {}, TInputs extends AnyExtensionInputMap, >(options: { + ref: TRef; disabled?: boolean; inputs?: TInputs; configSchema?: PortableSchema; - component: (options: { + component: (values: { config: TConfig; inputs: Expand>; - }) => Promise; + }) => Promise; }) { - const id = 'core.components.progress'; + const id = options.ref.id; return createExtension({ id, - attachTo: { id: 'core.components', input: 'progress' }, + attachTo: { id: 'core', input: 'components' }, inputs: options.inputs, disabled: options.disabled, configSchema: options.configSchema, output: { - component: coreExtensionData.components.progress, + component: coreExtensionData.component, }, - factory({ bind, config, inputs, source }) { + factory({ config, inputs, source }) { const ExtensionComponent = lazy(() => options .component({ config, inputs }) @@ -64,145 +60,16 @@ export function createProgressExtension< })), ); - bind({ - component: props => ( - - - - ), - }); - }, - }); -} - -/** @public */ -export function createBootErrorPageExtension< - TConfig extends {}, - TInputs extends AnyExtensionInputMap, ->(options: { - disabled?: boolean; - inputs?: TInputs; - configSchema?: PortableSchema; - component: (options: { - config: TConfig; - inputs: Expand>; - }) => Promise; -}) { - const id = 'core.components.bootErrorPage'; - return createExtension({ - id, - attachTo: { id: 'core.components', input: 'bootErrorPage' }, - inputs: options.inputs, - disabled: options.disabled, - configSchema: options.configSchema, - output: { - component: coreExtensionData.components.bootErrorPage, - }, - factory({ bind, config, inputs, source }) { - const ExtensionComponent = lazy(() => - options - .component({ config, inputs }) - .then(component => ({ default: component })) - .catch(error => ({ - default: () => , - })), - ); - - bind({ - component: props => ( - - - - ), - }); - }, - }); -} - -/** @public */ -export function createNotFoundErrorPageExtension< - TConfig extends {}, - TInputs extends AnyExtensionInputMap, ->(options: { - disabled?: boolean; - inputs?: TInputs; - configSchema?: PortableSchema; - component: (options: { - config: TConfig; - inputs: Expand>; - }) => Promise; -}) { - const id = 'core.components.notFoundErrorPage'; - return createExtension({ - id, - attachTo: { id: 'core.components', input: 'notFoundErrorPage' }, - inputs: options.inputs, - disabled: options.disabled, - configSchema: options.configSchema, - output: { - component: coreExtensionData.components.notFoundErrorPage, - }, - factory({ bind, config, inputs, source }) { - const ExtensionComponent = lazy(() => - options - .component({ config, inputs }) - .then(component => ({ default: component })) - .catch(error => ({ - default: () => , - })), - ); - - bind({ - component: props => ( - - - - ), - }); - }, - }); -} - -/** @public */ -export function createErrorBoundaryFallbackExtension< - TConfig extends {}, - TInputs extends AnyExtensionInputMap, ->(options: { - disabled?: boolean; - inputs?: TInputs; - configSchema?: PortableSchema; - component: (options: { - config: TConfig; - inputs: Expand>; - }) => Promise; -}) { - const id = 'core.components.errorBoundaryFallback'; - return createExtension({ - id, - attachTo: { id: 'core.components', input: 'errorBoundaryFallback' }, - inputs: options.inputs, - disabled: options.disabled, - configSchema: options.configSchema, - output: { - component: coreExtensionData.components.errorBoundaryFallback, - }, - factory({ bind, config, inputs, source }) { - const ExtensionComponent = lazy(() => - options - .component({ config, inputs }) - .then(component => ({ default: component })) - .catch(error => ({ - default: () => , - })), - ); - - bind({ - component: props => ( - - - - ), - }); + return { + component: { + ref: options.ref, + impl: props => ( + + + + ), + }, + }; }, }); } diff --git a/packages/frontend-plugin-api/src/extensions/index.ts b/packages/frontend-plugin-api/src/extensions/index.ts index d036cb5dda..a3be81f2a3 100644 --- a/packages/frontend-plugin-api/src/extensions/index.ts +++ b/packages/frontend-plugin-api/src/extensions/index.ts @@ -19,9 +19,4 @@ export { createPageExtension } from './createPageExtension'; export { createNavItemExtension } from './createNavItemExtension'; export { createSignInPageExtension } from './createSignInPageExtension'; export { createThemeExtension } from './createThemeExtension'; -export { - createProgressExtension, - createBootErrorPageExtension, - createNotFoundErrorPageExtension, - createErrorBoundaryFallbackExtension, -} from './createComponentExtension'; +export { createComponentExtension } from './createComponentExtension'; diff --git a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts index d2432b51ae..9bdb4b23eb 100644 --- a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts +++ b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts @@ -14,19 +14,14 @@ * limitations under the License. */ -import { JSX } from 'react'; +import { ComponentType, JSX } from 'react'; import { AnyApiFactory, AppTheme, IconComponent, } from '@backstage/core-plugin-api'; import { RouteRef } from '../routing'; -import { - CoreProgressComponent, - CoreBootErrorPageComponent, - CoreNotFoundErrorPageComponent, - CoreErrorBoundaryFallbackComponent, -} from '../types'; +import { ComponentRef } from '../components'; import { createExtensionDataRef } from './createExtensionDataRef'; /** @public */ @@ -44,9 +39,6 @@ export type LogoElements = { /** @public */ export const coreExtensionData = { - reactComponent: createExtensionDataRef< - (props: T) => JSX.Element - >('core.reactComponent'), reactElement: createExtensionDataRef('core.reactElement'), routePath: createExtensionDataRef('core.routing.path'), apiFactory: createExtensionDataRef('core.api.factory'), @@ -54,19 +46,8 @@ export const coreExtensionData = { navTarget: createExtensionDataRef('core.nav.target'), theme: createExtensionDataRef('core.theme'), logoElements: createExtensionDataRef('core.logos'), - components: { - progress: createExtensionDataRef( - 'core.components.progress', - ), - bootErrorPage: createExtensionDataRef( - 'core.components.bootErrorPage', - ), - notFoundErrorPage: createExtensionDataRef( - 'core.components.notFoundErrorPage', - ), - errorBoundaryFallback: - createExtensionDataRef( - 'core.components.errorBoundary', - ), - }, + component: createExtensionDataRef<{ + ref: ComponentRef>; + impl: ComponentType; + }>('component.ref'), };