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