From 7c86ff2b6b113e6f7e1ae61009080061842feebc Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 27 Oct 2023 08:45:39 +0200 Subject: [PATCH] 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'), + }, };