From 7c86ff2b6b113e6f7e1ae61009080061842feebc Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 27 Oct 2023 08:45:39 +0200 Subject: [PATCH 01/16] feat: create core component extension Signed-off-by: Camila Belo --- .../frontend-app-api/src/extensions/Core.tsx | 14 ++- .../src/extensions/CoreComponents.tsx | 98 +++++++++++++++++++ .../frontend-app-api/src/wiring/createApp.tsx | 10 +- .../src/wiring/coreExtensionData.ts | 21 +++- 4 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 packages/frontend-app-api/src/extensions/CoreComponents.tsx diff --git a/packages/frontend-app-api/src/extensions/Core.tsx b/packages/frontend-app-api/src/extensions/Core.tsx index 60772d435c..84407d1f09 100644 --- a/packages/frontend-app-api/src/extensions/Core.tsx +++ b/packages/frontend-app-api/src/extensions/Core.tsx @@ -14,6 +14,8 @@ * limitations under the License. */ +import React from 'react'; + import { coreExtensionData, createExtension, @@ -30,6 +32,12 @@ export const Core = createExtension({ themes: createExtensionInput({ theme: coreExtensionData.theme, }), + components: createExtensionInput( + { + provider: coreExtensionData.reactComponent, + }, + { singleton: true }, + ), root: createExtensionInput( { element: coreExtensionData.reactElement, @@ -42,7 +50,11 @@ 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 new file mode 100644 index 0000000000..3e1bc8e8bc --- /dev/null +++ b/packages/frontend-app-api/src/extensions/CoreComponents.tsx @@ -0,0 +1,98 @@ +/* + * 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, +} from '@backstage/frontend-plugin-api'; +// 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 CoreComponents = createExtension({ + id: 'core.components', + attachTo: { id: 'core', input: 'components' }, + inputs: { + progress: createExtensionInput( + { + component: coreExtensionData.components.progress, + }, + { + singleton: true, + }, + ), + bootErrorPage: createExtensionInput( + { + component: coreExtensionData.components.bootErrorPage, + }, + { + singleton: true, + }, + ), + notFoundErrorPage: createExtensionInput( + { + component: coreExtensionData.components.notFoundErrorPage, + }, + { + singleton: true, + }, + ), + errorBoundaryFallback: createExtensionInput( + { + component: coreExtensionData.components.errorBoundaryFallback, + }, + { + singleton: true, + }, + ), + }, + output: { + provider: coreExtensionData.reactComponent, + }, + factory({ bind, inputs }) { + bind({ + provider: function Provider(props: PropsWithChildren<{}>) { + const { children } = props; + const parentContext = useApp(); + + const appContext = useMemo( + () => ({ + ...parentContext, + // Only override components + getComponents: () => ({ + // Skipping Router and SignInPage + ...parentContext.getComponents(), + Progress: inputs.progress.component, + BootErrorPage: inputs.bootErrorPage.component, + NotFoundErrorPage: inputs.notFoundErrorPage.component, + ErrorBoundaryFallback: inputs.errorBoundaryFallback.component, + }), + }), + [parentContext], + ); + + return ( + + {children} + + ); + }, + }); + }, +}); diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index a1412f1c3c..8d51df84c0 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -89,6 +89,7 @@ import { RoutingProvider } from '../routing/RoutingProvider'; import { resolveRouteBindings } from '../routing/resolveRouteBindings'; import { collectRouteIds } from '../routing/collectRouteIds'; import { createAppTree } from '../tree'; +import { CoreComponents } from '../extensions/CoreComponents'; import { AppNode } from '@backstage/frontend-plugin-api'; import { toLegacyPlugin } from '../routing/toLegacyPlugin'; import { InternalAppContext } from './InternalAppContext'; @@ -104,6 +105,7 @@ const builtinExtensions = [ CoreRoutes, CoreNav, CoreLayout, + CoreComponents, LightTheme, DarkTheme, ]; @@ -376,7 +378,13 @@ function createLegacyAppContext(plugins: BackstagePlugin[]): AppContext { }, getComponents(): AppComponents { - return defaultComponents; + return { + ...defaultComponents, + Progress: () => null, + BootErrorPage: () => null, + NotFoundErrorPage: () => null, + ErrorBoundaryFallback: () => null, + }; }, }; } diff --git a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts index 9ffdb835ba..64abfb74eb 100644 --- a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts +++ b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts @@ -20,8 +20,10 @@ import { AppTheme, IconComponent, } from '@backstage/core-plugin-api'; -import { createExtensionDataRef } from './createExtensionDataRef'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { AppComponents } from '../../../core-app-api/src/app/types'; import { RouteRef } from '../routing'; +import { createExtensionDataRef } from './createExtensionDataRef'; /** @public */ export type NavTarget = { @@ -38,6 +40,9 @@ 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'), @@ -45,4 +50,18 @@ 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< + AppComponents['NotFoundErrorPage'] + >('core.components.notFoundErrorPage'), + errorBoundaryFallback: createExtensionDataRef< + AppComponents['ErrorBoundaryFallback'] + >('core.components.errorBoundary'), + }, }; From c26c40956605409fd72ed63cd26ee32e8971b973 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 27 Oct 2023 08:47:24 +0200 Subject: [PATCH 02/16] feat: create core component factories Signed-off-by: Camila Belo --- packages/app-next/src/App.tsx | 38 ++++++++ .../src/extensions/CoreComponents.tsx | 24 +++++ .../src/extensions/CoreRoutes.tsx | 14 ++- .../frontend-app-api/src/wiring/createApp.tsx | 12 ++- .../extensions/createComponentExtension.tsx | 95 +++++++++++++++++++ .../src/extensions/index.ts | 6 ++ 6 files changed, 185 insertions(+), 4 deletions(-) create mode 100644 packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 67faf8d646..88dc4a7207 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -29,6 +29,7 @@ import { createExtension, createApiExtension, createExtensionOverrides, + createNotFoundErrorPageExtension, } from '@backstage/frontend-plugin-api'; import techdocsPlugin from '@backstage/plugin-techdocs/alpha'; import { homePage } from './HomePage'; @@ -48,6 +49,8 @@ import { } from '@backstage/integration-react'; import { createSignInPageExtension } from '@backstage/frontend-plugin-api'; import { SignInPage } from '@backstage/core-components'; +import { Box, Typography } from '@material-ui/core'; +import { Button } from '@backstage/core-components'; /* @@ -108,6 +111,40 @@ const scmIntegrationApi = createApiExtension({ }), }); +const customNotFoundErrorPage = createNotFoundErrorPageExtension({ + component: () => ( + + 404 + + Bowie was unable to locate this page. Please contact your support team + if this page used to exist. + + Backstage bowie + + + ), +}); + const collectedLegacyPlugins = collectLegacyRoutes( } /> @@ -129,6 +166,7 @@ const app = createApp({ scmAuthExtension, scmIntegrationApi, signInPage, + customNotFoundErrorPage, ], }), ], diff --git a/packages/frontend-app-api/src/extensions/CoreComponents.tsx b/packages/frontend-app-api/src/extensions/CoreComponents.tsx index 3e1bc8e8bc..a0651d6423 100644 --- a/packages/frontend-app-api/src/extensions/CoreComponents.tsx +++ b/packages/frontend-app-api/src/extensions/CoreComponents.tsx @@ -19,12 +19,36 @@ 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: defaultComponents.Progress, +}); + +export const DefaultBootErrorPageComponent = createBootErrorPageExtension({ + component: defaultComponents.BootErrorPage, +}); + +export const DefaultNotFoundErrorPageComponent = + createNotFoundErrorPageExtension({ + component: defaultComponents.NotFoundErrorPage, + }); + +export const DefaultErrorBoundaryComponent = + createErrorBoundaryFallbackExtension({ + component: defaultComponents.ErrorBoundaryFallback, + }); + export const CoreComponents = createExtension({ id: 'core.components', attachTo: { id: 'core', input: 'components' }, diff --git a/packages/frontend-app-api/src/extensions/CoreRoutes.tsx b/packages/frontend-app-api/src/extensions/CoreRoutes.tsx index 985fd07be9..e2798c091d 100644 --- a/packages/frontend-app-api/src/extensions/CoreRoutes.tsx +++ b/packages/frontend-app-api/src/extensions/CoreRoutes.tsx @@ -21,6 +21,8 @@ import { createExtensionInput, } from '@backstage/frontend-plugin-api'; import { useRoutes } from 'react-router-dom'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { useApp } from '../../../core-plugin-api/src/app/useApp'; export const CoreRoutes = createExtension({ id: 'core.routes', @@ -37,12 +39,18 @@ export const CoreRoutes = createExtension({ }, factory({ inputs }) { const Routes = () => { - const element = useRoutes( - inputs.routes.map(route => ({ + const app = useApp(); + const { NotFoundErrorPage } = app.getComponents(); + const element = useRoutes([ + ...inputs.routes.map(route => ({ path: `${route.path}/*`, element: route.element, })), - ); + { + path: '*', + element: , + }, + ]); return element; }; diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index 8d51df84c0..caa7064b2d 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -89,7 +89,13 @@ import { RoutingProvider } from '../routing/RoutingProvider'; import { resolveRouteBindings } from '../routing/resolveRouteBindings'; import { collectRouteIds } from '../routing/collectRouteIds'; import { createAppTree } from '../tree'; -import { CoreComponents } from '../extensions/CoreComponents'; +import { + CoreComponents, + DefaultProgressComponent, + DefaultErrorBoundaryComponent, + DefaultBootErrorPageComponent, + DefaultNotFoundErrorPageComponent, +} from '../extensions/CoreComponents'; import { AppNode } from '@backstage/frontend-plugin-api'; import { toLegacyPlugin } from '../routing/toLegacyPlugin'; import { InternalAppContext } from './InternalAppContext'; @@ -106,6 +112,10 @@ const builtinExtensions = [ CoreNav, CoreLayout, CoreComponents, + DefaultProgressComponent, + DefaultErrorBoundaryComponent, + DefaultBootErrorPageComponent, + DefaultNotFoundErrorPageComponent, LightTheme, DarkTheme, ]; diff --git a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx new file mode 100644 index 0000000000..5c882dd638 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx @@ -0,0 +1,95 @@ +/* + * 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. + */ + +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { AppComponents } from '../../../core-app-api/src/app/types'; +import { coreExtensionData, createExtension } from '../wiring'; + +/** @public */ +export function createBootErrorPageExtension(options: { + component: AppComponents['BootErrorPage']; +}) { + return createExtension({ + id: `core.components.bootErrorPage`, + attachTo: { id: 'core.components', input: 'bootErrorPage' }, + inputs: {}, + output: { + component: coreExtensionData.components.bootErrorPage, + }, + factory({ bind }) { + bind({ + component: options.component, + }); + }, + }); +} + +/** @public */ +export function createNotFoundErrorPageExtension(options: { + component: AppComponents['NotFoundErrorPage']; +}) { + return createExtension({ + id: `core.components.notFoundErrorPage`, + attachTo: { id: 'core.components', input: 'notFoundErrorPage' }, + inputs: {}, + output: { + component: coreExtensionData.components.notFoundErrorPage, + }, + factory({ bind }) { + bind({ + component: options.component, + }); + }, + }); +} + +/** @public */ +export function createErrorBoundaryFallbackExtension(options: { + component: AppComponents['ErrorBoundaryFallback']; +}) { + return createExtension({ + id: `core.components.errorBoundaryFallback`, + attachTo: { id: 'core.components', input: 'errorBoundaryFallback' }, + inputs: {}, + output: { + component: coreExtensionData.components.errorBoundaryFallback, + }, + factory({ bind }) { + bind({ + component: options.component, + }); + }, + }); +} + +/** @public */ +export function createProgressExtension(options: { + component: AppComponents['Progress']; +}) { + return createExtension({ + id: `core.components.progress`, + attachTo: { id: 'core.components', input: 'progress' }, + inputs: {}, + output: { + component: coreExtensionData.components.progress, + }, + factory({ bind }) { + bind({ + component: options.component, + }); + }, + }); +} diff --git a/packages/frontend-plugin-api/src/extensions/index.ts b/packages/frontend-plugin-api/src/extensions/index.ts index d696774272..d036cb5dda 100644 --- a/packages/frontend-plugin-api/src/extensions/index.ts +++ b/packages/frontend-plugin-api/src/extensions/index.ts @@ -19,3 +19,9 @@ export { createPageExtension } from './createPageExtension'; export { createNavItemExtension } from './createNavItemExtension'; export { createSignInPageExtension } from './createSignInPageExtension'; export { createThemeExtension } from './createThemeExtension'; +export { + createProgressExtension, + createBootErrorPageExtension, + createNotFoundErrorPageExtension, + createErrorBoundaryFallbackExtension, +} from './createComponentExtension'; From 04c8b48fb995a5cabaaa75fc6e596288c5850111 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 27 Oct 2023 09:48:12 +0200 Subject: [PATCH 03/16] refactor: lazy load core components Signed-off-by: Camila Belo --- packages/app-next/src/App.tsx | 38 +-- .../examples/notFoundErrorPageExtension.tsx | 58 +++++ .../src/extensions/CoreComponents.tsx | 8 +- .../frontend-app-api/src/wiring/createApp.tsx | 1 + .../src/components/ExtensionError.tsx | 27 ++ .../src/components/index.ts | 2 + .../extensions/createComponentExtension.tsx | 245 +++++++++++++----- .../src/extensions/createPageExtension.tsx | 7 +- packages/frontend-plugin-api/src/index.ts | 7 + packages/frontend-plugin-api/src/types.ts | 28 ++ .../src/wiring/coreExtensionData.ts | 25 +- 11 files changed, 327 insertions(+), 119 deletions(-) create mode 100644 packages/app-next/src/examples/notFoundErrorPageExtension.tsx create mode 100644 packages/frontend-plugin-api/src/components/ExtensionError.tsx diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 88dc4a7207..3fb91bd03e 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -17,6 +17,7 @@ import React from 'react'; import { createApp } from '@backstage/frontend-app-api'; import { pagesPlugin } from './examples/pagesPlugin'; +import customNotFoundErrorPage from './examples/notFoundErrorPageExtension'; import graphiqlPlugin from '@backstage/plugin-graphiql/alpha'; import techRadarPlugin from '@backstage/plugin-tech-radar/alpha'; import userSettingsPlugin from '@backstage/plugin-user-settings/alpha'; @@ -29,7 +30,6 @@ import { createExtension, createApiExtension, createExtensionOverrides, - createNotFoundErrorPageExtension, } from '@backstage/frontend-plugin-api'; import techdocsPlugin from '@backstage/plugin-techdocs/alpha'; import { homePage } from './HomePage'; @@ -49,8 +49,6 @@ import { } from '@backstage/integration-react'; import { createSignInPageExtension } from '@backstage/frontend-plugin-api'; import { SignInPage } from '@backstage/core-components'; -import { Box, Typography } from '@material-ui/core'; -import { Button } from '@backstage/core-components'; /* @@ -111,40 +109,6 @@ const scmIntegrationApi = createApiExtension({ }), }); -const customNotFoundErrorPage = createNotFoundErrorPageExtension({ - component: () => ( - - 404 - - Bowie was unable to locate this page. Please contact your support team - if this page used to exist. - - Backstage bowie - - - ), -}); - const collectedLegacyPlugins = collectLegacyRoutes( } /> diff --git a/packages/app-next/src/examples/notFoundErrorPageExtension.tsx b/packages/app-next/src/examples/notFoundErrorPageExtension.tsx new file mode 100644 index 0000000000..93807a1757 --- /dev/null +++ b/packages/app-next/src/examples/notFoundErrorPageExtension.tsx @@ -0,0 +1,58 @@ +/* + * 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 from 'react'; +import { createNotFoundErrorPageExtension } from '@backstage/frontend-plugin-api'; +import { Box, Typography } from '@material-ui/core'; +import { Button } from '@backstage/core-components'; + +function CustomNotFoundErrorPage() { + return ( + + 404 + + Bowie was unable to locate this page. Please contact your support team + if this page used to exist. + + Backstage bowie + + + ); +} + +export default createNotFoundErrorPageExtension({ + component: async () => CustomNotFoundErrorPage, +}); diff --git a/packages/frontend-app-api/src/extensions/CoreComponents.tsx b/packages/frontend-app-api/src/extensions/CoreComponents.tsx index a0651d6423..543baef3ad 100644 --- a/packages/frontend-app-api/src/extensions/CoreComponents.tsx +++ b/packages/frontend-app-api/src/extensions/CoreComponents.tsx @@ -32,21 +32,21 @@ import { AppContextProvider } from '../../../core-app-api/src/app/AppContext'; import { useApp } from '../../../core-plugin-api/src/app/useApp'; export const DefaultProgressComponent = createProgressExtension({ - component: defaultComponents.Progress, + component: async () => defaultComponents.Progress, }); export const DefaultBootErrorPageComponent = createBootErrorPageExtension({ - component: defaultComponents.BootErrorPage, + component: async () => defaultComponents.BootErrorPage, }); export const DefaultNotFoundErrorPageComponent = createNotFoundErrorPageExtension({ - component: defaultComponents.NotFoundErrorPage, + component: async () => defaultComponents.NotFoundErrorPage, }); export const DefaultErrorBoundaryComponent = createErrorBoundaryFallbackExtension({ - component: defaultComponents.ErrorBoundaryFallback, + component: async () => defaultComponents.ErrorBoundaryFallback, }); export const CoreComponents = createExtension({ diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index caa7064b2d..deaeded6f1 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -390,6 +390,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, diff --git a/packages/frontend-plugin-api/src/components/ExtensionError.tsx b/packages/frontend-plugin-api/src/components/ExtensionError.tsx new file mode 100644 index 0000000000..5aa2ae03bd --- /dev/null +++ b/packages/frontend-plugin-api/src/components/ExtensionError.tsx @@ -0,0 +1,27 @@ +/* + * 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 from 'react'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { useApp } from '../../../core-plugin-api/src/app/useApp'; + +/** @public */ +export function ExtensionError(props: { error: Error }) { + const { error } = props; + const app = useApp(); + const { BootErrorPage } = app.getComponents(); + return ; +} diff --git a/packages/frontend-plugin-api/src/components/index.ts b/packages/frontend-plugin-api/src/components/index.ts index 7056a59271..fec315fed6 100644 --- a/packages/frontend-plugin-api/src/components/index.ts +++ b/packages/frontend-plugin-api/src/components/index.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +export { ExtensionError } from './ExtensionError'; + export { ExtensionBoundary, type ExtensionBoundaryProps, diff --git a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx index 5c882dd638..cd86bbdf22 100644 --- a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx @@ -14,81 +14,194 @@ * limitations under the License. */ -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { AppComponents } from '../../../core-app-api/src/app/types'; -import { coreExtensionData, createExtension } from '../wiring'; +import React, { lazy } from 'react'; +import { + AnyExtensionInputMap, + ExtensionInputValues, + coreExtensionData, + createExtension, +} from '../wiring'; +import { + Expand, + CoreProgressComponent, + CoreBootErrorPageComponent, + CoreNotFoundErrorPageComponent, + CoreErrorBoundaryFallbackComponent, +} from '../types'; +import { PortableSchema } from '../schema'; +import { ExtensionError, ExtensionBoundary } from '../components'; /** @public */ -export function createBootErrorPageExtension(options: { - component: AppComponents['BootErrorPage']; +export function createProgressExtension< + TConfig extends {}, + TInputs extends AnyExtensionInputMap, +>(options: { + disabled?: boolean; + inputs?: TInputs; + configSchema?: PortableSchema; + component: (options: { + config: TConfig; + inputs: Expand>; + }) => Promise; }) { + const id = 'core.components.progress'; return createExtension({ - id: `core.components.bootErrorPage`, - attachTo: { id: 'core.components', input: 'bootErrorPage' }, - inputs: {}, - output: { - component: coreExtensionData.components.bootErrorPage, - }, - factory({ bind }) { - bind({ - component: options.component, - }); - }, - }); -} - -/** @public */ -export function createNotFoundErrorPageExtension(options: { - component: AppComponents['NotFoundErrorPage']; -}) { - return createExtension({ - id: `core.components.notFoundErrorPage`, - attachTo: { id: 'core.components', input: 'notFoundErrorPage' }, - inputs: {}, - output: { - component: coreExtensionData.components.notFoundErrorPage, - }, - factory({ bind }) { - bind({ - component: options.component, - }); - }, - }); -} - -/** @public */ -export function createErrorBoundaryFallbackExtension(options: { - component: AppComponents['ErrorBoundaryFallback']; -}) { - return createExtension({ - id: `core.components.errorBoundaryFallback`, - attachTo: { id: 'core.components', input: 'errorBoundaryFallback' }, - inputs: {}, - output: { - component: coreExtensionData.components.errorBoundaryFallback, - }, - factory({ bind }) { - bind({ - component: options.component, - }); - }, - }); -} - -/** @public */ -export function createProgressExtension(options: { - component: AppComponents['Progress']; -}) { - return createExtension({ - id: `core.components.progress`, + id, attachTo: { id: 'core.components', input: 'progress' }, - inputs: {}, + inputs: options.inputs, + disabled: options.disabled, + configSchema: options.configSchema, output: { component: coreExtensionData.components.progress, }, - factory({ bind }) { + factory({ bind, config, inputs, source }) { + const ExtensionComponent = lazy(() => + options + .component({ config, inputs }) + .then(component => ({ default: component })) + .catch(error => ({ + default: () => , + })), + ); + bind({ - component: options.component, + 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 => ( + + + + ), }); }, }); diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index 99733ba367..d05f051a67 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -15,7 +15,7 @@ */ import React, { lazy } from 'react'; -import { ExtensionBoundary } from '../components'; +import { ExtensionError, ExtensionBoundary } from '../components'; import { createSchemaFromZod, PortableSchema } from '../schema'; import { coreExtensionData, @@ -79,7 +79,10 @@ export function createPageExtension< const ExtensionComponent = lazy(() => options .loader({ config, inputs }) - .then(element => ({ default: () => element })), + .then(element => ({ default: () => element })) + .catch(error => ({ + default: () => , + })), ); return { diff --git a/packages/frontend-plugin-api/src/index.ts b/packages/frontend-plugin-api/src/index.ts index 10f89b81b5..a38a7991fb 100644 --- a/packages/frontend-plugin-api/src/index.ts +++ b/packages/frontend-plugin-api/src/index.ts @@ -29,3 +29,10 @@ export * from './routing'; export * from './schema'; export * from './apis/system'; export * from './wiring'; + +export type { + CoreProgressComponent, + CoreBootErrorPageComponent, + CoreNotFoundErrorPageComponent, + CoreErrorBoundaryFallbackComponent, +} from './types'; diff --git a/packages/frontend-plugin-api/src/types.ts b/packages/frontend-plugin-api/src/types.ts index 8b6f02a942..f3f68c362c 100644 --- a/packages/frontend-plugin-api/src/types.ts +++ b/packages/frontend-plugin-api/src/types.ts @@ -14,9 +14,37 @@ * limitations under the License. */ +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { ComponentType, PropsWithChildren } from 'react'; + // TODO(Rugvip): This might be a quite useful utility type, maybe add to @backstage/types? /** * Utility type to expand type aliases into their equivalent type. * @ignore */ export type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; + +/** @public */ +export type CoreProgressComponent = ComponentType>; + +/** @public */ +export type CoreBootErrorPageComponent = ComponentType< + PropsWithChildren<{ + step: 'load-config' | 'load-chunk'; + error: Error; + }> +>; + +/** @public */ +export type CoreNotFoundErrorPageComponent = ComponentType< + PropsWithChildren<{}> +>; + +/** @public */ +export type CoreErrorBoundaryFallbackComponent = ComponentType< + PropsWithChildren<{ + plugin?: BackstagePlugin; + error: Error; + resetError: () => void; + }> +>; diff --git a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts index 64abfb74eb..d2432b51ae 100644 --- a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts +++ b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts @@ -20,9 +20,13 @@ import { AppTheme, IconComponent, } from '@backstage/core-plugin-api'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { AppComponents } from '../../../core-app-api/src/app/types'; import { RouteRef } from '../routing'; +import { + CoreProgressComponent, + CoreBootErrorPageComponent, + CoreNotFoundErrorPageComponent, + CoreErrorBoundaryFallbackComponent, +} from '../types'; import { createExtensionDataRef } from './createExtensionDataRef'; /** @public */ @@ -51,17 +55,18 @@ export const coreExtensionData = { theme: createExtensionDataRef('core.theme'), logoElements: createExtensionDataRef('core.logos'), components: { - progress: createExtensionDataRef( + progress: createExtensionDataRef( 'core.components.progress', ), - bootErrorPage: createExtensionDataRef( + bootErrorPage: createExtensionDataRef( 'core.components.bootErrorPage', ), - notFoundErrorPage: createExtensionDataRef< - AppComponents['NotFoundErrorPage'] - >('core.components.notFoundErrorPage'), - errorBoundaryFallback: createExtensionDataRef< - AppComponents['ErrorBoundaryFallback'] - >('core.components.errorBoundary'), + notFoundErrorPage: createExtensionDataRef( + 'core.components.notFoundErrorPage', + ), + errorBoundaryFallback: + createExtensionDataRef( + 'core.components.errorBoundary', + ), }, }; From a159a0498e70a2d150f36cf199f962510c98ac43 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 27 Oct 2023 18:50:06 +0200 Subject: [PATCH 04/16] tests: update built-in extensions Signed-off-by: Camila Belo --- .../src/extensions/CoreComponents.tsx | 48 ++++++++++++++----- .../extractRouteInfoFromAppNode.test.ts | 10 +++- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/packages/frontend-app-api/src/extensions/CoreComponents.tsx b/packages/frontend-app-api/src/extensions/CoreComponents.tsx index 543baef3ad..d951983331 100644 --- a/packages/frontend-app-api/src/extensions/CoreComponents.tsx +++ b/packages/frontend-app-api/src/extensions/CoreComponents.tsx @@ -58,6 +58,7 @@ export const CoreComponents = createExtension({ component: coreExtensionData.components.progress, }, { + optional: true, singleton: true, }, ), @@ -66,6 +67,7 @@ export const CoreComponents = createExtension({ component: coreExtensionData.components.bootErrorPage, }, { + optional: true, singleton: true, }, ), @@ -74,6 +76,7 @@ export const CoreComponents = createExtension({ component: coreExtensionData.components.notFoundErrorPage, }, { + optional: true, singleton: true, }, ), @@ -82,6 +85,7 @@ export const CoreComponents = createExtension({ component: coreExtensionData.components.errorBoundaryFallback, }, { + optional: true, singleton: true, }, ), @@ -93,26 +97,44 @@ export const CoreComponents = createExtension({ bind({ provider: function Provider(props: PropsWithChildren<{}>) { const { children } = props; - const parentContext = useApp(); + const app = useApp(); - const appContext = useMemo( + const context = useMemo( () => ({ - ...parentContext, + ...app, // Only override components - getComponents: () => ({ - // Skipping Router and SignInPage - ...parentContext.getComponents(), - Progress: inputs.progress.component, - BootErrorPage: inputs.bootErrorPage.component, - NotFoundErrorPage: inputs.notFoundErrorPage.component, - ErrorBoundaryFallback: inputs.errorBoundaryFallback.component, - }), + 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, + }; + }, }), - [parentContext], + [app], ); return ( - + {children} ); diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts index c94a06a442..ffab978217 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts @@ -35,6 +35,7 @@ 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'; const ref1 = createRouteRef(); const ref2 = createRouteRef(); @@ -81,7 +82,14 @@ function routeInfoFromExtensions(extensions: Extension[]) { }); const tree = createAppTree({ config: new MockConfigApi({}), - builtinExtensions: [Core, CoreRoutes, CoreNav, CoreLayout, CoreRouter], + builtinExtensions: [ + Core, + CoreRoutes, + CoreNav, + CoreLayout, + CoreRouter, + CoreComponents, + ], features: [plugin], }); From 52cbee40255c86a3e15e42dfcb1cf78c9d2368ec Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 30 Oct 2023 12:13:38 +0100 Subject: [PATCH 05/16] refactor: use component ref Signed-off-by: Camila Belo --- .../examples/notFoundErrorPageExtension.tsx | 8 +- .../frontend-app-api/src/extensions/Core.tsx | 17 +- .../src/extensions/CoreComponents.tsx | 144 -------------- .../src/extensions/components.tsx | 46 +++++ .../extractRouteInfoFromAppNode.test.ts | 16 +- .../frontend-app-api/src/wiring/createApp.tsx | 65 +++++-- .../src/components/ComponentRef.tsx | 63 +++++++ .../src/components/ExtensionBoundary.tsx | 12 +- .../src/components/index.ts | 8 + .../extensions/createComponentExtension.tsx | 175 +++--------------- .../src/extensions/index.ts | 7 +- .../src/wiring/coreExtensionData.ts | 31 +--- 12 files changed, 217 insertions(+), 375 deletions(-) delete mode 100644 packages/frontend-app-api/src/extensions/CoreComponents.tsx create mode 100644 packages/frontend-app-api/src/extensions/components.tsx create mode 100644 packages/frontend-plugin-api/src/components/ComponentRef.tsx 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'), }; From 227c7394ea2b8939464190fb643b058b715b43f5 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 30 Oct 2023 15:18:33 +0100 Subject: [PATCH 06/16] feat: create collect legacy components Signed-off-by: Camila Belo --- packages/app-next/src/App.tsx | 13 +++- .../examples/notFoundErrorPageExtension.tsx | 2 +- .../src/collectLegacyComponents.test.tsx | 61 +++++++++++++++++++ .../src/collectLegacyComponents.tsx | 53 ++++++++++++++++ packages/core-compat-api/src/index.ts | 1 + 5 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 packages/core-compat-api/src/collectLegacyComponents.test.tsx create mode 100644 packages/core-compat-api/src/collectLegacyComponents.tsx diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 3fb91bd03e..791d2b38fa 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { createApp } from '@backstage/frontend-app-api'; import { pagesPlugin } from './examples/pagesPlugin'; -import customNotFoundErrorPage from './examples/notFoundErrorPageExtension'; +import { CustomNotFoundErrorPage } from './examples/notFoundErrorPageExtension'; import graphiqlPlugin from '@backstage/plugin-graphiql/alpha'; import techRadarPlugin from '@backstage/plugin-tech-radar/alpha'; import userSettingsPlugin from '@backstage/plugin-user-settings/alpha'; @@ -33,7 +33,10 @@ import { } from '@backstage/frontend-plugin-api'; import techdocsPlugin from '@backstage/plugin-techdocs/alpha'; import { homePage } from './HomePage'; -import { collectLegacyRoutes } from '@backstage/core-compat-api'; +import { + collectLegacyComponents, + collectLegacyRoutes, +} from '@backstage/core-compat-api'; import { FlatRoutes } from '@backstage/core-app-api'; import { Route } from 'react-router'; import { CatalogImportPage } from '@backstage/plugin-catalog-import'; @@ -115,6 +118,10 @@ const collectedLegacyPlugins = collectLegacyRoutes( , ); +const legacyAppComponents = collectLegacyComponents({ + NotFoundErrorPage: CustomNotFoundErrorPage, +}); + const app = createApp({ features: [ graphiqlPlugin, @@ -130,7 +137,7 @@ const app = createApp({ scmAuthExtension, scmIntegrationApi, signInPage, - customNotFoundErrorPage, + ...legacyAppComponents, ], }), ], diff --git a/packages/app-next/src/examples/notFoundErrorPageExtension.tsx b/packages/app-next/src/examples/notFoundErrorPageExtension.tsx index 0e1fbd40f8..c076b059f4 100644 --- a/packages/app-next/src/examples/notFoundErrorPageExtension.tsx +++ b/packages/app-next/src/examples/notFoundErrorPageExtension.tsx @@ -22,7 +22,7 @@ import { import { Box, Typography } from '@material-ui/core'; import { Button } from '@backstage/core-components'; -function CustomNotFoundErrorPage() { +export function CustomNotFoundErrorPage() { return ( { + const components = { + Progress: () =>
Progress
, + BootErrorPage: () =>
BootErrorPage
, + NotFoundErrorPage: () =>
NotFoundErrorPage
, + ErrorBoundaryFallback: () =>
ErrorBoundaryFallback
, + }; + + it('should collect legacy routes', () => { + const collected = collectLegacyComponents(components); + + expect( + collected.map(p => ({ + id: p.id, + attachTo: p.attachTo, + disabled: p.disabled, + })), + ).toEqual([ + { + id: 'core.components.progress', + attachTo: { id: 'core', input: 'components' }, + disabled: false, + }, + { + id: 'core.components.bootErrorPage', + attachTo: { id: 'core', input: 'components' }, + disabled: false, + }, + { + id: 'core.components.notFoundErrorPage', + attachTo: { id: 'core', input: 'components' }, + disabled: false, + }, + { + id: 'core.components.errorBoundaryFallback', + attachTo: { id: 'core', input: 'components' }, + disabled: false, + }, + ]); + }); +}); diff --git a/packages/core-compat-api/src/collectLegacyComponents.tsx b/packages/core-compat-api/src/collectLegacyComponents.tsx new file mode 100644 index 0000000000..97c3ee22ad --- /dev/null +++ b/packages/core-compat-api/src/collectLegacyComponents.tsx @@ -0,0 +1,53 @@ +/* + * 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 { + Extension, + ComponentRef, + createComponentExtension, + coreProgressComponentRef, + coreBootErrorPageComponentRef, + coreNotFoundErrorPageComponentRef, + coreErrorBoundaryFallbackComponentRef, +} from '@backstage/frontend-plugin-api'; +import { AppComponents } from '@backstage/core-plugin-api'; + +type ComponentTypes = T[keyof T]; + +const refs: Record> = { + Progress: coreProgressComponentRef, + BootErrorPage: coreBootErrorPageComponentRef, + NotFoundErrorPage: coreNotFoundErrorPageComponentRef, + ErrorBoundaryFallback: coreErrorBoundaryFallbackComponentRef, +}; + +/** @public */ +export function collectLegacyComponents(components: Partial) { + return Object.entries(components).reduce[]>( + (extensions, [componentName, componentFunction]) => { + const ref = refs[componentName]; + return ref + ? extensions.concat( + createComponentExtension({ + ref, + component: async () => componentFunction, + }), + ) + : extensions; + }, + [], + ); +} diff --git a/packages/core-compat-api/src/index.ts b/packages/core-compat-api/src/index.ts index 421ced7054..d31513e559 100644 --- a/packages/core-compat-api/src/index.ts +++ b/packages/core-compat-api/src/index.ts @@ -14,5 +14,6 @@ * limitations under the License. */ export { collectLegacyRoutes } from './collectLegacyRoutes'; +export { collectLegacyComponents } from './collectLegacyComponents'; export { convertLegacyApp } from './convertLegacyApp'; export { convertLegacyRouteRef } from './convertLegacyRouteRef'; From 8f80db46b7a7dab2caea73a727a986e422151275 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 6 Nov 2023 09:27:53 +0100 Subject: [PATCH 07/16] refactor: use new components context Signed-off-by: Camila Belo --- .../frontend-app-api/src/extensions/Core.tsx | 16 ++++- .../src/extensions/CoreRoutes.tsx | 8 +-- .../src/components/ComponentsContext.tsx | 58 +++++++++++++++++++ .../src/components/index.ts | 6 ++ 4 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 packages/frontend-plugin-api/src/components/ComponentsContext.tsx diff --git a/packages/frontend-app-api/src/extensions/Core.tsx b/packages/frontend-app-api/src/extensions/Core.tsx index 441cb5bcf0..4347eb1b45 100644 --- a/packages/frontend-app-api/src/extensions/Core.tsx +++ b/packages/frontend-app-api/src/extensions/Core.tsx @@ -18,7 +18,9 @@ import { coreExtensionData, createExtension, createExtensionInput, + ComponentsProvider, } from '@backstage/frontend-plugin-api'; +import React from 'react'; export const Core = createExtension({ id: 'core', @@ -44,8 +46,20 @@ export const Core = createExtension({ root: coreExtensionData.reactElement, }, factory({ inputs }) { + const components = inputs.components.reduce( + (rest, { component }) => ({ + ...rest, + [component.ref.id]: component.impl, + }), + {}, + ); + return { - root: inputs.root.element, + root: ( + + {inputs.root.element} + + ), }; }, }); diff --git a/packages/frontend-app-api/src/extensions/CoreRoutes.tsx b/packages/frontend-app-api/src/extensions/CoreRoutes.tsx index e2798c091d..b2918cfcc9 100644 --- a/packages/frontend-app-api/src/extensions/CoreRoutes.tsx +++ b/packages/frontend-app-api/src/extensions/CoreRoutes.tsx @@ -19,10 +19,10 @@ import { createExtension, coreExtensionData, createExtensionInput, + coreNotFoundErrorPageComponentRef, + useComponent, } from '@backstage/frontend-plugin-api'; import { useRoutes } from 'react-router-dom'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { useApp } from '../../../core-plugin-api/src/app/useApp'; export const CoreRoutes = createExtension({ id: 'core.routes', @@ -39,8 +39,8 @@ export const CoreRoutes = createExtension({ }, factory({ inputs }) { const Routes = () => { - const app = useApp(); - const { NotFoundErrorPage } = app.getComponents(); + const NotFoundErrorPage = useComponent(coreNotFoundErrorPageComponentRef); + const element = useRoutes([ ...inputs.routes.map(route => ({ path: `${route.path}/*`, diff --git a/packages/frontend-plugin-api/src/components/ComponentsContext.tsx b/packages/frontend-plugin-api/src/components/ComponentsContext.tsx new file mode 100644 index 0000000000..7b1058f83c --- /dev/null +++ b/packages/frontend-plugin-api/src/components/ComponentsContext.tsx @@ -0,0 +1,58 @@ +/* + * 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 { ComponentRef } from '@backstage/frontend-plugin-api'; +import { + createVersionedContext, + createVersionedValueMap, + useVersionedContext, +} from '@backstage/version-bridge'; +import React from 'react'; +import { ComponentType, PropsWithChildren } from 'react'; + +/** @public */ +export type ComponentsContextValue = Record>; + +const ComponentsContext = createVersionedContext<{ 1: ComponentsContextValue }>( + 'components-context', +); + +/** @public */ +export function ComponentsProvider( + props: PropsWithChildren<{ value: ComponentsContextValue }>, +) { + const { value, children } = props; + return ( + + {children} + + ); +} + +/** @public */ +export function useComponent< + P extends {}, + T extends ComponentRef>, +>(ref: T): T['T'] { + const components = useVersionedContext<{ 1: ComponentsContextValue }>( + 'components-context', + ); + const component = components?.atVersion(1)?.[ref.id]; + if (!component) { + throw new Error(`No implementation available for ${ref.id}`); + } + return component; +} diff --git a/packages/frontend-plugin-api/src/components/index.ts b/packages/frontend-plugin-api/src/components/index.ts index 4bee5c5f7b..575e5efbe0 100644 --- a/packages/frontend-plugin-api/src/components/index.ts +++ b/packages/frontend-plugin-api/src/components/index.ts @@ -14,6 +14,12 @@ * limitations under the License. */ +export { + ComponentsProvider, + useComponent, + type ComponentsContextValue, +} from './ComponentsContext'; + export { coreProgressComponentRef, coreBootErrorPageComponentRef, From 9c6b0ab576cbe66bc119d93d7057efe248dc9068 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Sat, 11 Nov 2023 11:17:38 +0100 Subject: [PATCH 08/16] test: update root tree snapshot Signed-off-by: Camila Belo --- packages/frontend-app-api/src/wiring/createApp.test.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx index 41d722a68e..232de74de4 100644 --- a/packages/frontend-app-api/src/wiring/createApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx @@ -199,6 +199,12 @@ describe('createApp', () => { ] ] + components [ + + + + + ] themes [ From cba5b3ba3bac128063014de768de44fc4b6dee28 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 27 Oct 2023 13:57:41 +0200 Subject: [PATCH 09/16] docs: update api reports Signed-off-by: Camila Belo --- packages/core-compat-api/api-report.md | 7 ++ packages/frontend-plugin-api/api-report.md | 88 +++++++++++++++++++ .../extensions/createComponentExtension.tsx | 4 +- 3 files changed, 97 insertions(+), 2 deletions(-) diff --git a/packages/core-compat-api/api-report.md b/packages/core-compat-api/api-report.md index 877aefa24f..866517b5c8 100644 --- a/packages/core-compat-api/api-report.md +++ b/packages/core-compat-api/api-report.md @@ -6,7 +6,9 @@ /// import { AnyRouteRefParams } from '@backstage/core-plugin-api'; +import { AppComponents } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { Extension } from '@backstage/frontend-plugin-api'; import { ExtensionOverrides } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { ExternalRouteRef as ExternalRouteRef_2 } from '@backstage/frontend-plugin-api'; @@ -16,6 +18,11 @@ import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api'; import { SubRouteRef } from '@backstage/core-plugin-api'; import { SubRouteRef as SubRouteRef_2 } from '@backstage/frontend-plugin-api'; +// @public (undocumented) +export function collectLegacyComponents( + components: Partial, +): Extension[]; + // @public (undocumented) export function collectLegacyRoutes( flatRoutesElement: JSX.Element, diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index c1c920559e..2255dafdd0 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -22,9 +22,11 @@ import { AuthProviderInfo } from '@backstage/core-plugin-api'; import { AuthRequestOptions } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { BackstageIdentityResponse } from '@backstage/core-plugin-api'; +import { BackstagePlugin as BackstagePlugin_2 } from '@backstage/core-plugin-api'; import { BackstageUserIdentity } from '@backstage/core-plugin-api'; import { bitbucketAuthApiRef } from '@backstage/core-plugin-api'; import { bitbucketServerAuthApiRef } from '@backstage/core-plugin-api'; +import { ComponentRef as ComponentRef_2 } from '@backstage/frontend-plugin-api'; import { ComponentType } from 'react'; import { ConfigApi } from '@backstage/core-plugin-api'; import { configApiRef } from '@backstage/core-plugin-api'; @@ -64,6 +66,7 @@ import { OpenIdConnectApi } from '@backstage/core-plugin-api'; import { PendingOAuthRequest } from '@backstage/core-plugin-api'; import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; +import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { SessionApi } from '@backstage/core-plugin-api'; @@ -284,6 +287,22 @@ export type CommonAnalyticsContext = { extensionId: string; }; +// @public (undocumented) +export type ComponentRef = { + id: string; + T: T; +}; + +// @public (undocumented) +export type ComponentsContextValue = Record>; + +// @public (undocumented) +export function ComponentsProvider( + props: PropsWithChildren<{ + value: ComponentsContextValue; + }>, +): React_2.JSX.Element; + export { ConfigApi }; export { configApiRef }; @@ -304,6 +323,29 @@ export interface ConfigurableExtensionDataRef< >; } +// @public (undocumented) +export type CoreBootErrorPageComponent = ComponentType< + PropsWithChildren<{ + step: 'load-config' | 'load-chunk'; + error: Error; + }> +>; + +// @public (undocumented) +export const coreBootErrorPageComponentRef: ComponentRef; + +// @public (undocumented) +export type CoreErrorBoundaryFallbackComponent = ComponentType< + PropsWithChildren<{ + plugin?: BackstagePlugin_2; + error: Error; + resetError: () => void; + }> +>; + +// @public (undocumented) +export const coreErrorBoundaryFallbackComponentRef: ComponentRef; + // @public (undocumented) export const coreExtensionData: { reactElement: ConfigurableExtensionDataRef; @@ -313,8 +355,29 @@ export const coreExtensionData: { navTarget: ConfigurableExtensionDataRef; theme: ConfigurableExtensionDataRef; logoElements: ConfigurableExtensionDataRef; + component: ConfigurableExtensionDataRef< + { + ref: ComponentRef>; + impl: ComponentType; + }, + {} + >; }; +// @public (undocumented) +export type CoreNotFoundErrorPageComponent = ComponentType< + PropsWithChildren<{}> +>; + +// @public (undocumented) +export const coreNotFoundErrorPageComponentRef: ComponentRef; + +// @public (undocumented) +export type CoreProgressComponent = ComponentType>; + +// @public (undocumented) +export const coreProgressComponentRef: ComponentRef; + // @public (undocumented) export function createApiExtension< TConfig extends {}, @@ -341,6 +404,22 @@ export { createApiFactory }; export { createApiRef }; +// @public (undocumented) +export function createComponentExtension< + TRef extends ComponentRef, + TConfig extends {}, + TInputs extends AnyExtensionInputMap, +>(options: { + ref: TRef; + disabled?: boolean; + inputs?: TInputs; + configSchema?: PortableSchema; + component: (values: { + config: TConfig; + inputs: Expand>; + }) => Promise; +}): Extension; + // @public (undocumented) export function createExtension< TOutput extends AnyExtensionDataMap, @@ -621,6 +700,9 @@ export type ExtensionDataValues = { : never]?: TExtensionData[DataName]['T']; }; +// @public (undocumented) +export function ExtensionError(props: { error: Error }): React_2.JSX.Element; + // @public (undocumented) export interface ExtensionInput< TExtensionData extends AnyExtensionDataMap, @@ -828,6 +910,12 @@ export { useApi }; export { useApiHolder }; +// @public (undocumented) +export function useComponent< + P extends {}, + T extends ComponentRef_2>, +>(ref: T): T['T']; + // @public export function useRouteRef< TOptional extends boolean, diff --git a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx index 678d05049d..376510312f 100644 --- a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx @@ -50,7 +50,7 @@ export function createComponentExtension< output: { component: coreExtensionData.component, }, - factory({ config, inputs, source }) { + factory({ config, inputs, node }) { const ExtensionComponent = lazy(() => options .component({ config, inputs }) @@ -64,7 +64,7 @@ export function createComponentExtension< component: { ref: options.ref, impl: props => ( - + ), From 1f12fb762c365d19f3824dee7351e7637f31047d Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 27 Oct 2023 10:07:33 +0200 Subject: [PATCH 10/16] docs: add changeset files Signed-off-by: Camila Belo --- .changeset/brown-books-carry.md | 5 +++++ .changeset/clever-wombats-sneeze.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/brown-books-carry.md create mode 100644 .changeset/clever-wombats-sneeze.md diff --git a/.changeset/brown-books-carry.md b/.changeset/brown-books-carry.md new file mode 100644 index 0000000000..1299354445 --- /dev/null +++ b/.changeset/brown-books-carry.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Create a core components extension that allows adopters to override core app components such as `Progress`, `BootErrorPage`, `NotFoundErrorPage` and `ErrorBoundaryFallback`. diff --git a/.changeset/clever-wombats-sneeze.md b/.changeset/clever-wombats-sneeze.md new file mode 100644 index 0000000000..ffd94aa9f0 --- /dev/null +++ b/.changeset/clever-wombats-sneeze.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Create factories for overriding default core components extensions. From 221293e22d0089c4486f18dd48e5fa6828021884 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 17 Nov 2023 16:30:36 +0100 Subject: [PATCH 11/16] frontend-*-api: initial ComponentsApi implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Camila Belo Signed-off-by: Patrik Oldsberg --- .../ComponentsApi/ComponentsApi.ts | 34 +++++++++++++++++ .../implementations/ComponentsApi/index.ts | 17 +++++++++ .../src/apis/definitions/ComponentsApi.ts | 37 +++++++++++++++++++ .../src/apis/definitions/index.ts | 1 + 4 files changed, 89 insertions(+) create mode 100644 packages/frontend-app-api/src/apis/implementations/ComponentsApi/ComponentsApi.ts create mode 100644 packages/frontend-app-api/src/apis/implementations/ComponentsApi/index.ts create mode 100644 packages/frontend-plugin-api/src/apis/definitions/ComponentsApi.ts diff --git a/packages/frontend-app-api/src/apis/implementations/ComponentsApi/ComponentsApi.ts b/packages/frontend-app-api/src/apis/implementations/ComponentsApi/ComponentsApi.ts new file mode 100644 index 0000000000..87a12d136e --- /dev/null +++ b/packages/frontend-app-api/src/apis/implementations/ComponentsApi/ComponentsApi.ts @@ -0,0 +1,34 @@ +/* + * 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 { ComponentRef, ComponentsApi } from '@backstage/frontend-plugin-api'; + +/** + * Implementation for the {@linkComponentApi} + * + * @internal + */ +export class DefaultComponentsApi implements ComponentsApi { + #components: Map, any>; + + constructor(components: Map, any>) { + this.#components = components; + } + + getComponent(ref: ComponentRef): T | undefined { + return this.#components.get(ref); + } +} diff --git a/packages/frontend-app-api/src/apis/implementations/ComponentsApi/index.ts b/packages/frontend-app-api/src/apis/implementations/ComponentsApi/index.ts new file mode 100644 index 0000000000..f04059c54f --- /dev/null +++ b/packages/frontend-app-api/src/apis/implementations/ComponentsApi/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { DefaultComponentsApi } from './ComponentsApi'; diff --git a/packages/frontend-plugin-api/src/apis/definitions/ComponentsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/ComponentsApi.ts new file mode 100644 index 0000000000..6f057e70f2 --- /dev/null +++ b/packages/frontend-plugin-api/src/apis/definitions/ComponentsApi.ts @@ -0,0 +1,37 @@ +/* + * 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 { ComponentRef } from '../../components'; +import { createApiRef } from '../system'; + +/** + * API for looking up components based on component refs. + * + * @public + */ +export interface ComponentsApi { + // TODO: Should component refs also provide the default implementation so that we're guaranteed to get a component? + getComponent(ref: ComponentRef): T | undefined; +} + +/** + * The `ApiRef` of {@link ComponentsApi}. + * + * @public + */ +export const componentsApiRef = createApiRef({ + id: 'core.components', +}); diff --git a/packages/frontend-plugin-api/src/apis/definitions/index.ts b/packages/frontend-plugin-api/src/apis/definitions/index.ts index 8791912e4a..f2496ee61a 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/index.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/index.ts @@ -34,6 +34,7 @@ export * from './auth'; export * from './AlertApi'; export * from './AppThemeApi'; +export * from './ComponentsApi'; export * from './ConfigApi'; export * from './DiscoveryApi'; export * from './ErrorApi'; From 413f05a6c9dfe99b17562a448a093619b1b5db35 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 17 Nov 2023 17:10:40 +0100 Subject: [PATCH 12/16] refactor: replace components context in app Signed-off-by: Camila Belo --- .../ComponentsApi/ComponentsApi.ts | 8 ++- .../frontend-app-api/src/extensions/Core.tsx | 16 +---- .../src/extensions/CoreRoutes.tsx | 8 ++- .../frontend-app-api/src/wiring/createApp.tsx | 71 ++++++------------- packages/frontend-plugin-api/api-report.md | 23 +++--- .../src/apis/definitions/ComponentsApi.ts | 2 +- .../src/components/ComponentsContext.tsx | 58 --------------- .../src/components/index.ts | 6 -- 8 files changed, 44 insertions(+), 148 deletions(-) delete mode 100644 packages/frontend-plugin-api/src/components/ComponentsContext.tsx diff --git a/packages/frontend-app-api/src/apis/implementations/ComponentsApi/ComponentsApi.ts b/packages/frontend-app-api/src/apis/implementations/ComponentsApi/ComponentsApi.ts index 87a12d136e..50d125e4ab 100644 --- a/packages/frontend-app-api/src/apis/implementations/ComponentsApi/ComponentsApi.ts +++ b/packages/frontend-app-api/src/apis/implementations/ComponentsApi/ComponentsApi.ts @@ -28,7 +28,11 @@ export class DefaultComponentsApi implements ComponentsApi { this.#components = components; } - getComponent(ref: ComponentRef): T | undefined { - return this.#components.get(ref); + getComponent(ref: ComponentRef): T { + const impl = this.#components.get(ref); + if (!impl) { + throw new Error(`No implementation found for component ref ${ref}`); + } + return impl; } } diff --git a/packages/frontend-app-api/src/extensions/Core.tsx b/packages/frontend-app-api/src/extensions/Core.tsx index 4347eb1b45..441cb5bcf0 100644 --- a/packages/frontend-app-api/src/extensions/Core.tsx +++ b/packages/frontend-app-api/src/extensions/Core.tsx @@ -18,9 +18,7 @@ import { coreExtensionData, createExtension, createExtensionInput, - ComponentsProvider, } from '@backstage/frontend-plugin-api'; -import React from 'react'; export const Core = createExtension({ id: 'core', @@ -46,20 +44,8 @@ export const Core = createExtension({ root: coreExtensionData.reactElement, }, factory({ inputs }) { - const components = inputs.components.reduce( - (rest, { component }) => ({ - ...rest, - [component.ref.id]: component.impl, - }), - {}, - ); - return { - root: ( - - {inputs.root.element} - - ), + root: inputs.root.element, }; }, }); diff --git a/packages/frontend-app-api/src/extensions/CoreRoutes.tsx b/packages/frontend-app-api/src/extensions/CoreRoutes.tsx index b2918cfcc9..37b0e5d62f 100644 --- a/packages/frontend-app-api/src/extensions/CoreRoutes.tsx +++ b/packages/frontend-app-api/src/extensions/CoreRoutes.tsx @@ -20,7 +20,8 @@ import { coreExtensionData, createExtensionInput, coreNotFoundErrorPageComponentRef, - useComponent, + useApi, + componentsApiRef, } from '@backstage/frontend-plugin-api'; import { useRoutes } from 'react-router-dom'; @@ -39,7 +40,10 @@ export const CoreRoutes = createExtension({ }, factory({ inputs }) { const Routes = () => { - const NotFoundErrorPage = useComponent(coreNotFoundErrorPageComponentRef); + const componentsApi = useApi(componentsApiRef); + const NotFoundErrorPage = componentsApi.getComponent( + coreNotFoundErrorPageComponentRef, + ); const element = useRoutes([ ...inputs.routes.map(route => ({ diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index b22d258eae..4e463fb526 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -21,11 +21,8 @@ import { appTreeApiRef, BackstagePlugin, ComponentRef, - coreBootErrorPageComponentRef, - coreErrorBoundaryFallbackComponentRef, + componentsApiRef, coreExtensionData, - coreNotFoundErrorPageComponentRef, - coreProgressComponentRef, ExtensionDataRef, ExtensionOverrides, RouteRef, @@ -108,6 +105,7 @@ import { CoreRouter } from '../extensions/CoreRouter'; import { toInternalBackstagePlugin } from '../../../frontend-plugin-api/src/wiring/createPlugin'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides'; +import { DefaultComponentsApi } from '../apis/implementations/ComponentsApi'; export const builtinExtensions = [ Core, @@ -319,7 +317,6 @@ export function createSpecializedApp(options?: { features.filter( (f): f is BackstagePlugin => f.$$type === '@backstage/BackstagePlugin', ), - tree.root, ); const appIdentityProxy = new AppIdentityProxy(); @@ -375,49 +372,7 @@ export function createSpecializedApp(options?: { }; } -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, - }; - +function createLegacyAppContext(plugins: BackstagePlugin[]): AppContext { return { getPlugins(): LegacyBackstagePlugin[] { return plugins.map(toLegacyPlugin); @@ -434,7 +389,7 @@ function createLegacyAppContext( }, getComponents(): AppComponents { - return overriddenComponents; + return defaultComponents; }, }; } @@ -483,6 +438,24 @@ function createApiHolder( }), }); + const componentsExtensions = + tree.root.edges.attachments + .get('components') + ?.map(e => e.instance?.getData(coreExtensionData.component)) + .filter(x => !!x) ?? []; + + const componentsMap = componentsExtensions.reduce( + (components, component) => + component ? components.set(component.ref, component?.impl) : components, + new Map, any>(), + ); + + factoryRegistry.register('static', { + api: componentsApiRef, + deps: {}, + factory: () => new DefaultComponentsApi(componentsMap), + }); + factoryRegistry.register('static', { api: appThemeApiRef, deps: {}, diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 2255dafdd0..3d89c415fa 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -13,6 +13,7 @@ import { AnyApiRef } from '@backstage/core-plugin-api'; import { ApiFactory } from '@backstage/core-plugin-api'; import { ApiHolder } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; +import { ApiRef as ApiRef_2 } from '@backstage/core-plugin-api/src/apis/system/types'; import { ApiRefConfig } from '@backstage/core-plugin-api'; import { AppTheme } from '@backstage/core-plugin-api'; import { AppThemeApi } from '@backstage/core-plugin-api'; @@ -26,7 +27,6 @@ import { BackstagePlugin as BackstagePlugin_2 } from '@backstage/core-plugin-api import { BackstageUserIdentity } from '@backstage/core-plugin-api'; import { bitbucketAuthApiRef } from '@backstage/core-plugin-api'; import { bitbucketServerAuthApiRef } from '@backstage/core-plugin-api'; -import { ComponentRef as ComponentRef_2 } from '@backstage/frontend-plugin-api'; import { ComponentType } from 'react'; import { ConfigApi } from '@backstage/core-plugin-api'; import { configApiRef } from '@backstage/core-plugin-api'; @@ -293,15 +293,14 @@ export type ComponentRef = { T: T; }; -// @public (undocumented) -export type ComponentsContextValue = Record>; +// @public +export interface ComponentsApi { + // (undocumented) + getComponent(ref: ComponentRef): T; +} -// @public (undocumented) -export function ComponentsProvider( - props: PropsWithChildren<{ - value: ComponentsContextValue; - }>, -): React_2.JSX.Element; +// @public +export const componentsApiRef: ApiRef_2; export { ConfigApi }; @@ -910,12 +909,6 @@ export { useApi }; export { useApiHolder }; -// @public (undocumented) -export function useComponent< - P extends {}, - T extends ComponentRef_2>, ->(ref: T): T['T']; - // @public export function useRouteRef< TOptional extends boolean, diff --git a/packages/frontend-plugin-api/src/apis/definitions/ComponentsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/ComponentsApi.ts index 6f057e70f2..452e692389 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/ComponentsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/ComponentsApi.ts @@ -24,7 +24,7 @@ import { createApiRef } from '../system'; */ export interface ComponentsApi { // TODO: Should component refs also provide the default implementation so that we're guaranteed to get a component? - getComponent(ref: ComponentRef): T | undefined; + getComponent(ref: ComponentRef): T; } /** diff --git a/packages/frontend-plugin-api/src/components/ComponentsContext.tsx b/packages/frontend-plugin-api/src/components/ComponentsContext.tsx deleted file mode 100644 index 7b1058f83c..0000000000 --- a/packages/frontend-plugin-api/src/components/ComponentsContext.tsx +++ /dev/null @@ -1,58 +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 { ComponentRef } from '@backstage/frontend-plugin-api'; -import { - createVersionedContext, - createVersionedValueMap, - useVersionedContext, -} from '@backstage/version-bridge'; -import React from 'react'; -import { ComponentType, PropsWithChildren } from 'react'; - -/** @public */ -export type ComponentsContextValue = Record>; - -const ComponentsContext = createVersionedContext<{ 1: ComponentsContextValue }>( - 'components-context', -); - -/** @public */ -export function ComponentsProvider( - props: PropsWithChildren<{ value: ComponentsContextValue }>, -) { - const { value, children } = props; - return ( - - {children} - - ); -} - -/** @public */ -export function useComponent< - P extends {}, - T extends ComponentRef>, ->(ref: T): T['T'] { - const components = useVersionedContext<{ 1: ComponentsContextValue }>( - 'components-context', - ); - const component = components?.atVersion(1)?.[ref.id]; - if (!component) { - throw new Error(`No implementation available for ${ref.id}`); - } - return component; -} diff --git a/packages/frontend-plugin-api/src/components/index.ts b/packages/frontend-plugin-api/src/components/index.ts index 575e5efbe0..4bee5c5f7b 100644 --- a/packages/frontend-plugin-api/src/components/index.ts +++ b/packages/frontend-plugin-api/src/components/index.ts @@ -14,12 +14,6 @@ * limitations under the License. */ -export { - ComponentsProvider, - useComponent, - type ComponentsContextValue, -} from './ComponentsContext'; - export { coreProgressComponentRef, coreBootErrorPageComponentRef, From a9da252cb4c2816ca4e8b3ed01deda296427d900 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 20 Nov 2023 09:37:24 +0100 Subject: [PATCH 13/16] refator: group core component ref in a object Signed-off-by: Camila Belo --- .../examples/notFoundErrorPageExtension.tsx | 4 ++-- .../src/collectLegacyComponents.tsx | 13 ++++------- .../src/extensions/CoreRoutes.tsx | 4 ++-- .../src/extensions/components.tsx | 13 ++++------- packages/frontend-plugin-api/api-report.md | 16 +++++-------- .../src/components/ComponentRef.tsx | 23 +++++++++++-------- .../src/components/index.ts | 8 +------ 7 files changed, 35 insertions(+), 46 deletions(-) diff --git a/packages/app-next/src/examples/notFoundErrorPageExtension.tsx b/packages/app-next/src/examples/notFoundErrorPageExtension.tsx index c076b059f4..b1e88b6457 100644 --- a/packages/app-next/src/examples/notFoundErrorPageExtension.tsx +++ b/packages/app-next/src/examples/notFoundErrorPageExtension.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { createComponentExtension, - coreNotFoundErrorPageComponentRef, + coreComponentsRefs, } from '@backstage/frontend-plugin-api'; import { Box, Typography } from '@material-ui/core'; import { Button } from '@backstage/core-components'; @@ -57,6 +57,6 @@ export function CustomNotFoundErrorPage() { } export default createComponentExtension({ - ref: coreNotFoundErrorPageComponentRef, + ref: coreComponentsRefs.notFoundErrorPage, component: async () => CustomNotFoundErrorPage, }); diff --git a/packages/core-compat-api/src/collectLegacyComponents.tsx b/packages/core-compat-api/src/collectLegacyComponents.tsx index 97c3ee22ad..17a354f52d 100644 --- a/packages/core-compat-api/src/collectLegacyComponents.tsx +++ b/packages/core-compat-api/src/collectLegacyComponents.tsx @@ -18,20 +18,17 @@ import { Extension, ComponentRef, createComponentExtension, - coreProgressComponentRef, - coreBootErrorPageComponentRef, - coreNotFoundErrorPageComponentRef, - coreErrorBoundaryFallbackComponentRef, + coreComponentsRefs, } from '@backstage/frontend-plugin-api'; import { AppComponents } from '@backstage/core-plugin-api'; type ComponentTypes = T[keyof T]; const refs: Record> = { - Progress: coreProgressComponentRef, - BootErrorPage: coreBootErrorPageComponentRef, - NotFoundErrorPage: coreNotFoundErrorPageComponentRef, - ErrorBoundaryFallback: coreErrorBoundaryFallbackComponentRef, + Progress: coreComponentsRefs.progress, + BootErrorPage: coreComponentsRefs.bootErrorPage, + NotFoundErrorPage: coreComponentsRefs.notFoundErrorPage, + ErrorBoundaryFallback: coreComponentsRefs.errorBoundaryFallback, }; /** @public */ diff --git a/packages/frontend-app-api/src/extensions/CoreRoutes.tsx b/packages/frontend-app-api/src/extensions/CoreRoutes.tsx index 37b0e5d62f..9acd27b6bf 100644 --- a/packages/frontend-app-api/src/extensions/CoreRoutes.tsx +++ b/packages/frontend-app-api/src/extensions/CoreRoutes.tsx @@ -19,7 +19,7 @@ import { createExtension, coreExtensionData, createExtensionInput, - coreNotFoundErrorPageComponentRef, + coreComponentsRefs, useApi, componentsApiRef, } from '@backstage/frontend-plugin-api'; @@ -42,7 +42,7 @@ export const CoreRoutes = createExtension({ const Routes = () => { const componentsApi = useApi(componentsApiRef); const NotFoundErrorPage = componentsApi.getComponent( - coreNotFoundErrorPageComponentRef, + coreComponentsRefs.notFoundErrorPage, ); const element = useRoutes([ diff --git a/packages/frontend-app-api/src/extensions/components.tsx b/packages/frontend-app-api/src/extensions/components.tsx index 5bd4431a28..cf34859aa0 100644 --- a/packages/frontend-app-api/src/extensions/components.tsx +++ b/packages/frontend-app-api/src/extensions/components.tsx @@ -16,31 +16,28 @@ import { createComponentExtension, - coreProgressComponentRef, - coreBootErrorPageComponentRef, - coreNotFoundErrorPageComponentRef, - coreErrorBoundaryFallbackComponentRef, + coreComponentsRefs, } 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, + ref: coreComponentsRefs.progress, component: async () => defaultComponents.Progress, }); export const DefaultBootErrorPageComponent = createComponentExtension({ - ref: coreBootErrorPageComponentRef, + ref: coreComponentsRefs.bootErrorPage, component: async () => defaultComponents.BootErrorPage, }); export const DefaultNotFoundErrorPageComponent = createComponentExtension({ - ref: coreNotFoundErrorPageComponentRef, + ref: coreComponentsRefs.notFoundErrorPage, component: async () => defaultComponents.NotFoundErrorPage, }); export const DefaultErrorBoundaryComponent = createComponentExtension({ - ref: coreErrorBoundaryFallbackComponentRef, + ref: coreComponentsRefs.errorBoundaryFallback, component: async () => defaultComponents.ErrorBoundaryFallback, }); diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 3d89c415fa..fe92a9cb44 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -331,7 +331,12 @@ export type CoreBootErrorPageComponent = ComponentType< >; // @public (undocumented) -export const coreBootErrorPageComponentRef: ComponentRef; +export const coreComponentsRefs: { + progress: ComponentRef; + bootErrorPage: ComponentRef; + notFoundErrorPage: ComponentRef; + errorBoundaryFallback: ComponentRef; +}; // @public (undocumented) export type CoreErrorBoundaryFallbackComponent = ComponentType< @@ -342,9 +347,6 @@ export type CoreErrorBoundaryFallbackComponent = ComponentType< }> >; -// @public (undocumented) -export const coreErrorBoundaryFallbackComponentRef: ComponentRef; - // @public (undocumented) export const coreExtensionData: { reactElement: ConfigurableExtensionDataRef; @@ -368,15 +370,9 @@ export type CoreNotFoundErrorPageComponent = ComponentType< PropsWithChildren<{}> >; -// @public (undocumented) -export const coreNotFoundErrorPageComponentRef: ComponentRef; - // @public (undocumented) export type CoreProgressComponent = ComponentType>; -// @public (undocumented) -export const coreProgressComponentRef: ComponentRef; - // @public (undocumented) export function createApiExtension< TConfig extends {}, diff --git a/packages/frontend-plugin-api/src/components/ComponentRef.tsx b/packages/frontend-plugin-api/src/components/ComponentRef.tsx index 13588ba779..2db85b9a42 100644 --- a/packages/frontend-plugin-api/src/components/ComponentRef.tsx +++ b/packages/frontend-plugin-api/src/components/ComponentRef.tsx @@ -40,24 +40,29 @@ export function createComponentRef(options: { }; } -/** @public */ -export const coreProgressComponentRef = - createComponentRef({ id: 'core.components.progress' }); +const coreProgressComponentRef = createComponentRef({ + id: 'core.components.progress', +}); -/** @public */ -export const coreBootErrorPageComponentRef = +const coreBootErrorPageComponentRef = createComponentRef({ id: 'core.components.bootErrorPage', }); -/** @public */ -export const coreNotFoundErrorPageComponentRef = +const coreNotFoundErrorPageComponentRef = createComponentRef({ id: 'core.components.notFoundErrorPage', }); -/** @public */ -export const coreErrorBoundaryFallbackComponentRef = +const coreErrorBoundaryFallbackComponentRef = createComponentRef({ id: 'core.components.errorBoundaryFallback', }); + +/** @public */ +export const coreComponentsRefs = { + progress: coreProgressComponentRef, + bootErrorPage: coreBootErrorPageComponentRef, + notFoundErrorPage: coreNotFoundErrorPageComponentRef, + errorBoundaryFallback: coreErrorBoundaryFallbackComponentRef, +}; diff --git a/packages/frontend-plugin-api/src/components/index.ts b/packages/frontend-plugin-api/src/components/index.ts index 4bee5c5f7b..e0b3d09543 100644 --- a/packages/frontend-plugin-api/src/components/index.ts +++ b/packages/frontend-plugin-api/src/components/index.ts @@ -14,13 +14,7 @@ * limitations under the License. */ -export { - coreProgressComponentRef, - coreBootErrorPageComponentRef, - coreErrorBoundaryFallbackComponentRef, - coreNotFoundErrorPageComponentRef, - type ComponentRef, -} from './ComponentRef'; +export { coreComponentsRefs, type ComponentRef } from './ComponentRef'; export { ExtensionError } from './ExtensionError'; From 73e65a75ffb771d5ad2da6e8078768cb0020e57b Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 20 Nov 2023 09:43:07 +0100 Subject: [PATCH 14/16] refator: remove extension error pattern Signed-off-by: Camila Belo --- packages/frontend-plugin-api/api-report.md | 3 --- .../src/components/ExtensionError.tsx | 27 ------------------- .../src/components/index.ts | 2 -- .../extensions/createComponentExtension.tsx | 7 ++--- .../src/extensions/createPageExtension.tsx | 7 ++--- 5 files changed, 4 insertions(+), 42 deletions(-) delete mode 100644 packages/frontend-plugin-api/src/components/ExtensionError.tsx diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index fe92a9cb44..b5c63f9d22 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -695,9 +695,6 @@ export type ExtensionDataValues = { : never]?: TExtensionData[DataName]['T']; }; -// @public (undocumented) -export function ExtensionError(props: { error: Error }): React_2.JSX.Element; - // @public (undocumented) export interface ExtensionInput< TExtensionData extends AnyExtensionDataMap, diff --git a/packages/frontend-plugin-api/src/components/ExtensionError.tsx b/packages/frontend-plugin-api/src/components/ExtensionError.tsx deleted file mode 100644 index 5aa2ae03bd..0000000000 --- a/packages/frontend-plugin-api/src/components/ExtensionError.tsx +++ /dev/null @@ -1,27 +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 from 'react'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { useApp } from '../../../core-plugin-api/src/app/useApp'; - -/** @public */ -export function ExtensionError(props: { error: Error }) { - const { error } = props; - const app = useApp(); - const { BootErrorPage } = app.getComponents(); - return ; -} diff --git a/packages/frontend-plugin-api/src/components/index.ts b/packages/frontend-plugin-api/src/components/index.ts index e0b3d09543..a7d947599f 100644 --- a/packages/frontend-plugin-api/src/components/index.ts +++ b/packages/frontend-plugin-api/src/components/index.ts @@ -16,8 +16,6 @@ export { coreComponentsRefs, type ComponentRef } from './ComponentRef'; -export { ExtensionError } from './ExtensionError'; - export { ExtensionBoundary, type ExtensionBoundaryProps, diff --git a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx index 376510312f..e337f3f822 100644 --- a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx @@ -23,7 +23,7 @@ import { } from '../wiring'; import { Expand } from '../types'; import { PortableSchema } from '../schema'; -import { ExtensionError, ExtensionBoundary, ComponentRef } from '../components'; +import { ExtensionBoundary, ComponentRef } from '../components'; /** @public */ export function createComponentExtension< @@ -54,10 +54,7 @@ export function createComponentExtension< const ExtensionComponent = lazy(() => options .component({ config, inputs }) - .then(component => ({ default: component })) - .catch(error => ({ - default: () => , - })), + .then(component => ({ default: component })), ); return { diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index d05f051a67..99733ba367 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -15,7 +15,7 @@ */ import React, { lazy } from 'react'; -import { ExtensionError, ExtensionBoundary } from '../components'; +import { ExtensionBoundary } from '../components'; import { createSchemaFromZod, PortableSchema } from '../schema'; import { coreExtensionData, @@ -79,10 +79,7 @@ export function createPageExtension< const ExtensionComponent = lazy(() => options .loader({ config, inputs }) - .then(element => ({ default: () => element })) - .catch(error => ({ - default: () => , - })), + .then(element => ({ default: () => element })), ); return { From 0e8f4a65aa6553ec0591ebe0459f280288f5e2c2 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 28 Nov 2023 10:01:37 +0100 Subject: [PATCH 15/16] refactor: make components optionally lazy Signed-off-by: Camila Belo --- .../examples/notFoundErrorPageExtension.tsx | 2 +- .../src/collectLegacyComponents.tsx | 6 ++-- .../src/extensions/components.tsx | 8 ++--- .../extensions/createComponentExtension.tsx | 32 +++++++++++++------ 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/packages/app-next/src/examples/notFoundErrorPageExtension.tsx b/packages/app-next/src/examples/notFoundErrorPageExtension.tsx index b1e88b6457..82acc25fb5 100644 --- a/packages/app-next/src/examples/notFoundErrorPageExtension.tsx +++ b/packages/app-next/src/examples/notFoundErrorPageExtension.tsx @@ -58,5 +58,5 @@ export function CustomNotFoundErrorPage() { export default createComponentExtension({ ref: coreComponentsRefs.notFoundErrorPage, - component: async () => CustomNotFoundErrorPage, + component: { sync: () => CustomNotFoundErrorPage }, }); diff --git a/packages/core-compat-api/src/collectLegacyComponents.tsx b/packages/core-compat-api/src/collectLegacyComponents.tsx index 17a354f52d..6092ca1be7 100644 --- a/packages/core-compat-api/src/collectLegacyComponents.tsx +++ b/packages/core-compat-api/src/collectLegacyComponents.tsx @@ -34,13 +34,13 @@ const refs: Record> = { /** @public */ export function collectLegacyComponents(components: Partial) { return Object.entries(components).reduce[]>( - (extensions, [componentName, componentFunction]) => { - const ref = refs[componentName]; + (extensions, [name, component]) => { + const ref = refs[name]; return ref ? extensions.concat( createComponentExtension({ ref, - component: async () => componentFunction, + component: { sync: () => component }, }), ) : extensions; diff --git a/packages/frontend-app-api/src/extensions/components.tsx b/packages/frontend-app-api/src/extensions/components.tsx index cf34859aa0..fa9187a831 100644 --- a/packages/frontend-app-api/src/extensions/components.tsx +++ b/packages/frontend-app-api/src/extensions/components.tsx @@ -24,20 +24,20 @@ import { components as defaultComponents } from '../../../app-defaults/src/defau export const DefaultProgressComponent = createComponentExtension({ ref: coreComponentsRefs.progress, - component: async () => defaultComponents.Progress, + component: { sync: () => defaultComponents.Progress }, }); export const DefaultBootErrorPageComponent = createComponentExtension({ ref: coreComponentsRefs.bootErrorPage, - component: async () => defaultComponents.BootErrorPage, + component: { sync: () => defaultComponents.BootErrorPage }, }); export const DefaultNotFoundErrorPageComponent = createComponentExtension({ ref: coreComponentsRefs.notFoundErrorPage, - component: async () => defaultComponents.NotFoundErrorPage, + component: { sync: () => defaultComponents.NotFoundErrorPage }, }); export const DefaultErrorBoundaryComponent = createComponentExtension({ ref: coreComponentsRefs.errorBoundaryFallback, - component: async () => defaultComponents.ErrorBoundaryFallback, + component: { sync: () => defaultComponents.ErrorBoundaryFallback }, }); diff --git a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx index e337f3f822..a20faa0b3a 100644 --- a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx @@ -35,10 +35,19 @@ export function createComponentExtension< disabled?: boolean; inputs?: TInputs; configSchema?: PortableSchema; - component: (values: { - config: TConfig; - inputs: Expand>; - }) => Promise; + component: + | { + lazy: (values: { + config: TConfig; + inputs: Expand>; + }) => Promise; + } + | { + sync: (values: { + config: TConfig; + inputs: Expand>; + }) => TRef['T']; + }; }) { const id = options.ref.id; return createExtension({ @@ -51,11 +60,16 @@ export function createComponentExtension< component: coreExtensionData.component, }, factory({ config, inputs, node }) { - const ExtensionComponent = lazy(() => - options - .component({ config, inputs }) - .then(component => ({ default: component })), - ); + let ExtensionComponent: TRef['T']; + + if ('sync' in options.component) { + ExtensionComponent = options.component.sync({ config, inputs }); + } else { + const loader = options.component.lazy({ config, inputs }); + ExtensionComponent = lazy(() => + loader.then(component => ({ default: component })), + ); + } return { component: { From 0025a3c5953181032cb62833ab922172fc3ecc9c Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 28 Nov 2023 10:50:42 +0100 Subject: [PATCH 16/16] docs: update api reports Signed-off-by: Camila Belo --- packages/frontend-plugin-api/api-report.md | 20 +++++++++++++------ .../src/apis/definitions/ComponentsApi.ts | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index b5c63f9d22..e1ed76d82c 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -13,7 +13,6 @@ import { AnyApiRef } from '@backstage/core-plugin-api'; import { ApiFactory } from '@backstage/core-plugin-api'; import { ApiHolder } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; -import { ApiRef as ApiRef_2 } from '@backstage/core-plugin-api/src/apis/system/types'; import { ApiRefConfig } from '@backstage/core-plugin-api'; import { AppTheme } from '@backstage/core-plugin-api'; import { AppThemeApi } from '@backstage/core-plugin-api'; @@ -300,7 +299,7 @@ export interface ComponentsApi { } // @public -export const componentsApiRef: ApiRef_2; +export const componentsApiRef: ApiRef; export { ConfigApi }; @@ -409,10 +408,19 @@ export function createComponentExtension< disabled?: boolean; inputs?: TInputs; configSchema?: PortableSchema; - component: (values: { - config: TConfig; - inputs: Expand>; - }) => Promise; + component: + | { + lazy: (values: { + config: TConfig; + inputs: Expand>; + }) => Promise; + } + | { + sync: (values: { + config: TConfig; + inputs: Expand>; + }) => TRef['T']; + }; }): Extension; // @public (undocumented) diff --git a/packages/frontend-plugin-api/src/apis/definitions/ComponentsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/ComponentsApi.ts index 452e692389..b55ed96549 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/ComponentsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/ComponentsApi.ts @@ -14,8 +14,8 @@ * limitations under the License. */ +import { createApiRef } from '@backstage/core-plugin-api'; import { ComponentRef } from '../../components'; -import { createApiRef } from '../system'; /** * API for looking up components based on component refs.