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