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'),
+ },
};