diff --git a/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx b/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx index 2f0cd451e2..2652aa69cf 100644 --- a/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx +++ b/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx @@ -27,6 +27,9 @@ import { iconsApiRef, useApi, routeResolutionApiRef, + ErrorBoundary, + NotFoundErrorPage, + Progress, } from '@backstage/frontend-plugin-api'; import { AppComponents, @@ -42,9 +45,6 @@ import { } from '@backstage/version-bridge'; import { convertLegacyRouteRef } from '../convertLegacyRouteRef'; -import { ErrorBoundary, NotFoundErrorPage } from '@backstage/plugin-app'; -import { Progress } from '@backstage/core-components'; - // Make sure that we only convert each new plugin instance to its legacy equivalent once const legacyPluginStore = getOrCreateGlobalSingleton( 'legacy-plugin-compatibility-store', diff --git a/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx b/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx index aa0b7bf556..ce2ce29bdb 100644 --- a/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx +++ b/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx @@ -37,6 +37,9 @@ import { componentsApiRef, iconsApiRef, routeResolutionApiRef, + Progress, + NotFoundErrorPage, + ErrorBoundary, } from '@backstage/frontend-plugin-api'; import { ComponentType, useMemo } from 'react'; import { ReactNode } from 'react'; @@ -66,14 +69,28 @@ class CompatComponentsApi implements ComponentsApi { this.#ErrorBoundaryFallback = ErrorBoundaryFallback; } - getComponent(ref: ComponentRef): ComponentType { + getComponent< + TInnerComponentProps extends {}, + TExternalComponentProps extends {} = TInnerComponentProps, + >( + ref: ComponentRef, + ): + | (() => (props: TInnerComponentProps) => JSX.Element | null) + | (() => Promise<(props: TInnerComponentProps) => JSX.Element | null>) + | undefined { switch (ref.id) { - case coreComponentRefs.progress.id: - return this.#Progress as ComponentType; - case coreComponentRefs.notFoundErrorPage.id: - return this.#NotFoundErrorPage as ComponentType; - case coreComponentRefs.errorBoundaryFallback.id: - return this.#ErrorBoundaryFallback as ComponentType; + case Progress.ref.id: + return (() => this.#Progress) as () => ( + props: object, + ) => JSX.Element | null; + case NotFoundErrorPage.ref.id: + return (() => this.#NotFoundErrorPage) as () => ( + props: object, + ) => JSX.Element | null; + case ErrorBoundary.ref.id: + return (() => this.#ErrorBoundaryFallback) as () => ( + props: object, + ) => JSX.Element | null; default: throw new Error( `No backwards compatible component is available for ref '${ref.id}'`, diff --git a/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx b/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx index f1418e545f..785a1769f8 100644 --- a/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx +++ b/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx @@ -16,13 +16,15 @@ import { componentsApiRef, - coreComponentRefs, coreExtensionData, createExtension, iconsApiRef, useRouteRef as useNewRouteRef, createRouteRef as createNewRouteRef, useApi, + NotFoundErrorPage, + ErrorBoundary, + Progress, } from '@backstage/frontend-plugin-api'; import { createExtensionTester, @@ -97,13 +99,19 @@ describe('BackwardsCompatProvider', () => { describe('ForwardsCompatProvider', () => { it('should convert the app context', async () => { + const defaultComponentRefs = { + progress: Progress.ref, + notFoundErrorPage: NotFoundErrorPage.ref, + errorBoundary: ErrorBoundary.ref, + }; + function Component() { const components = useApi(componentsApiRef); const icons = useApi(iconsApiRef); return (
components:{' '} - {Object.entries(coreComponentRefs) + {Object.entries(defaultComponentRefs) .map( ([name, ref]) => `${name}=${Boolean(components.getComponent(ref))}`, diff --git a/packages/frontend-plugin-api/src/blueprints/AdaptableComponentBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/AdaptableComponentBlueprint.ts index 457a04327c..a53f4d4a42 100644 --- a/packages/frontend-plugin-api/src/blueprints/AdaptableComponentBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/AdaptableComponentBlueprint.ts @@ -27,6 +27,11 @@ export const componentDataRef = createExtensionDataRef<{ | (() => Promise<(props: {}) => JSX.Element | null>); }>().with({ id: 'core.component.component' }); +/** + * Blueprint for creating adaptable components from a componentRef and a loader + * + * @public + */ export const AdaptableComponentBlueprint = createExtensionBlueprint({ kind: 'component', attachTo: { id: 'api:app/components', input: 'components' }, diff --git a/packages/frontend-plugin-api/src/components/DefaultAdaptableComponents.ts b/packages/frontend-plugin-api/src/components/DefaultAdaptableComponents.ts index db8b29446c..2dde24eb1b 100644 --- a/packages/frontend-plugin-api/src/components/DefaultAdaptableComponents.ts +++ b/packages/frontend-plugin-api/src/components/DefaultAdaptableComponents.ts @@ -13,16 +13,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createAdaptableComponent } from '@backstage/frontend-plugin-api'; +import { + CoreErrorBoundaryFallbackProps, + CoreNotFoundErrorPageProps, + CoreProgressProps, + createAdaptableComponent, +} from '@backstage/frontend-plugin-api'; -export const Progress = createAdaptableComponent({ +/** + * @public + */ +export const Progress = createAdaptableComponent({ id: 'core.components.progress', }); -export const NotFoundErrorPage = createAdaptableComponent({ - id: 'core.components.notFoundErrorPage', -}); +/** + * @public + */ +export const NotFoundErrorPage = + createAdaptableComponent({ + id: 'core.components.notFoundErrorPage', + }); -export const ErrorBoundary = createAdaptableComponent({ - id: 'core.components.errorBoundary', -}); +/** + * @public + */ +export const ErrorBoundary = + createAdaptableComponent({ + id: 'core.components.errorBoundary', + }); diff --git a/packages/frontend-plugin-api/src/components/createAdaptableComponent.tsx b/packages/frontend-plugin-api/src/components/createAdaptableComponent.tsx index 3a454d5c7e..3d9b4149c2 100644 --- a/packages/frontend-plugin-api/src/components/createAdaptableComponent.tsx +++ b/packages/frontend-plugin-api/src/components/createAdaptableComponent.tsx @@ -29,7 +29,12 @@ export type ComponentRef< $$type: '@backstage/ComponentRef'; }; -export type ComponentRefOptions< +/** + * Options for creating an AdaptableComponent. + * + * @public + */ +export type CreateAdaptableComponentOptions< TInnerComponentProps extends {}, TExternalComponentProps extends {} = TInnerComponentProps, > = { @@ -90,11 +95,19 @@ function makeComponentFromRef< return ComponentRefImpl; } +/** + * Creates a AdaptableComponent that can be used to render the component, optionally overriden by the app. + * + * @public + */ export function createAdaptableComponent< TInnerComponentProps extends {}, TExternalComponentProps extends {} = TInnerComponentProps, >( - options: ComponentRefOptions, + options: CreateAdaptableComponentOptions< + TInnerComponentProps, + TExternalComponentProps + >, ): ((props: TExternalComponentProps) => JSX.Element) & { ref: ComponentRef; } { diff --git a/packages/frontend-plugin-api/src/components/index.ts b/packages/frontend-plugin-api/src/components/index.ts index 0c8c69c355..f7897dfc1a 100644 --- a/packages/frontend-plugin-api/src/components/index.ts +++ b/packages/frontend-plugin-api/src/components/index.ts @@ -20,6 +20,7 @@ export { } from './ExtensionBoundary'; export { createAdaptableComponent, + type CreateAdaptableComponentOptions, type ComponentRef, } from './createAdaptableComponent'; export { useAppNode } from './AppNodeProvider'; diff --git a/plugins/app/report.api.md b/plugins/app/report.api.md index f6632550c2..74b8927b44 100644 --- a/plugins/app/report.api.md +++ b/plugins/app/report.api.md @@ -324,7 +324,9 @@ const appPlugin: FrontendPlugin< ConfigurableExtensionDataRef< { ref: ComponentRef; - impl: ComponentType; + loader: + | (() => (props: {}) => JSX.Element | null) + | (() => Promise<(props: {}) => JSX.Element | null>); }, 'core.component.component', {} @@ -726,6 +728,138 @@ const appPlugin: FrontendPlugin< element: JSX.Element; }; }>; + 'component:app/core.components.errorBoundary': ExtensionDefinition<{ + kind: 'component'; + name: 'core.components.errorBoundary'; + config: {}; + configInput: {}; + output: ConfigurableExtensionDataRef< + { + ref: ComponentRef; + loader: + | (() => (props: {}) => JSX.Element | null) + | (() => Promise<(props: {}) => JSX.Element | null>); + }, + 'core.component.component', + {} + >; + inputs: {}; + params: >(params: { + component: Ref extends ComponentRef + ? { + ref: Ref; + } & ((props: IExternalComponentProps) => JSX.Element) + : never; + loader: Ref extends ComponentRef + ? + | (() => (props: IInnerComponentProps) => JSX.Element | null) + | (() => Promise< + (props: IInnerComponentProps) => JSX.Element | null + >) + : never; + }) => ExtensionBlueprintParams<{ + component: Ref extends ComponentRef + ? { + ref: Ref; + } & ((props: IExternalComponentProps) => JSX.Element) + : never; + loader: Ref extends ComponentRef + ? + | (() => (props: IInnerComponentProps) => JSX.Element | null) + | (() => Promise< + (props: IInnerComponentProps) => JSX.Element | null + >) + : never; + }>; + }>; + 'component:app/core.components.notFoundErrorPage': ExtensionDefinition<{ + kind: 'component'; + name: 'core.components.notFoundErrorPage'; + config: {}; + configInput: {}; + output: ConfigurableExtensionDataRef< + { + ref: ComponentRef; + loader: + | (() => (props: {}) => JSX.Element | null) + | (() => Promise<(props: {}) => JSX.Element | null>); + }, + 'core.component.component', + {} + >; + inputs: {}; + params: >(params: { + component: Ref extends ComponentRef + ? { + ref: Ref; + } & ((props: IExternalComponentProps) => JSX.Element) + : never; + loader: Ref extends ComponentRef + ? + | (() => (props: IInnerComponentProps) => JSX.Element | null) + | (() => Promise< + (props: IInnerComponentProps) => JSX.Element | null + >) + : never; + }) => ExtensionBlueprintParams<{ + component: Ref extends ComponentRef + ? { + ref: Ref; + } & ((props: IExternalComponentProps) => JSX.Element) + : never; + loader: Ref extends ComponentRef + ? + | (() => (props: IInnerComponentProps) => JSX.Element | null) + | (() => Promise< + (props: IInnerComponentProps) => JSX.Element | null + >) + : never; + }>; + }>; + 'component:app/core.components.progress': ExtensionDefinition<{ + kind: 'component'; + name: 'core.components.progress'; + config: {}; + configInput: {}; + output: ConfigurableExtensionDataRef< + { + ref: ComponentRef; + loader: + | (() => (props: {}) => JSX.Element | null) + | (() => Promise<(props: {}) => JSX.Element | null>); + }, + 'core.component.component', + {} + >; + inputs: {}; + params: >(params: { + component: Ref extends ComponentRef + ? { + ref: Ref; + } & ((props: IExternalComponentProps) => JSX.Element) + : never; + loader: Ref extends ComponentRef + ? + | (() => (props: IInnerComponentProps) => JSX.Element | null) + | (() => Promise< + (props: IInnerComponentProps) => JSX.Element | null + >) + : never; + }) => ExtensionBlueprintParams<{ + component: Ref extends ComponentRef + ? { + ref: Ref; + } & ((props: IExternalComponentProps) => JSX.Element) + : never; + loader: Ref extends ComponentRef + ? + | (() => (props: IInnerComponentProps) => JSX.Element | null) + | (() => Promise< + (props: IInnerComponentProps) => JSX.Element | null + >) + : never; + }>; + }>; 'sign-in-page:app': ExtensionDefinition<{ kind: 'sign-in-page'; name: undefined; diff --git a/plugins/app/src/extensions/components.tsx b/plugins/app/src/extensions/components.tsx index 91cf8c7b63..25a7a046cc 100644 --- a/plugins/app/src/extensions/components.tsx +++ b/plugins/app/src/extensions/components.tsx @@ -20,7 +20,12 @@ import { AdaptableComponentBlueprint, } from '@backstage/frontend-plugin-api'; -import { Progress as ProgressComponent } from '@backstage/core-components'; +import { + ErrorPage, + ErrorPanel, + Progress as ProgressComponent, +} from '@backstage/core-components'; +import Button from '@material-ui/core/Button'; export const Progress = AdaptableComponentBlueprint.make({ name: 'core.components.progress', @@ -36,7 +41,8 @@ export const NotFoundErrorPage = AdaptableComponentBlueprint.make({ params: define => define({ component: AdaptableNotFoundErrorPage, - loader: () => NotFoundErrorPageComponent, + loader: () => () => + , }), }); @@ -45,6 +51,16 @@ export const ErrorBoundary = AdaptableComponentBlueprint.make({ params: define => define({ component: AdaptableErrorBoundary, - loader: () => ErrorBoundaryComponent, + loader: () => props => { + const { plugin, error, resetError } = props; + const title = `Error in ${plugin?.id}`; + return ( + + + + ); + }, }), }); diff --git a/yarn.lock b/yarn.lock index 3dbfb91e4b..934fbc5bcb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -32295,12 +32295,12 @@ __metadata: languageName: node linkType: hard -"for-each@npm:^0.3.3": - version: 0.3.3 - resolution: "for-each@npm:0.3.3" +"for-each@npm:^0.3.3, for-each@npm:^0.3.5": + version: 0.3.5 + resolution: "for-each@npm:0.3.5" dependencies: - is-callable: "npm:^1.1.3" - checksum: 10/fdac0cde1be35610bd635ae958422e8ce0cc1313e8d32ea6d34cfda7b60850940c1fd07c36456ad76bd9c24aef6ff5e03b02beb58c83af5ef6c968a64eada676 + is-callable: "npm:^1.2.7" + checksum: 10/330cc2439f85c94f4609de3ee1d32c5693ae15cdd7fe3d112c4fd9efd4ce7143f2c64ef6c2c9e0cfdb0058437f33ef05b5bdae5b98fcc903fb2143fbaf0fea0f languageName: node linkType: hard @@ -34907,7 +34907,7 @@ __metadata: languageName: node linkType: hard -"is-callable@npm:^1.1.3, is-callable@npm:^1.1.5, is-callable@npm:^1.2.7": +"is-callable@npm:^1.1.5, is-callable@npm:^1.2.7": version: 1.2.7 resolution: "is-callable@npm:1.2.7" checksum: 10/48a9297fb92c99e9df48706241a189da362bff3003354aea4048bd5f7b2eb0d823cd16d0a383cece3d76166ba16d85d9659165ac6fcce1ac12e6c649d66dbdb9 @@ -45108,14 +45108,16 @@ __metadata: linkType: hard "regexp.prototype.flags@npm:^1.5.3": - version: 1.5.3 - resolution: "regexp.prototype.flags@npm:1.5.3" + version: 1.5.4 + resolution: "regexp.prototype.flags@npm:1.5.4" dependencies: - call-bind: "npm:^1.0.7" + call-bind: "npm:^1.0.8" define-properties: "npm:^1.2.1" es-errors: "npm:^1.3.0" + get-proto: "npm:^1.0.1" + gopd: "npm:^1.2.0" set-function-name: "npm:^2.0.2" - checksum: 10/fe17bc4eebbc72945aaf9dd059eb7784a5ca453a67cc4b5b3e399ab08452c9a05befd92063e2c52e7b24d9238c60031656af32dd57c555d1ba6330dbf8c23b43 + checksum: 10/8ab897ca445968e0b96f6237641510f3243e59c180ee2ee8d83889c52ff735dd1bf3657fcd36db053e35e1d823dd53f2565d0b8021ea282c9fe62401c6c3bd6d languageName: node linkType: hard @@ -50789,16 +50791,17 @@ __metadata: linkType: hard "which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18, which-typed-array@npm:^1.1.2": - version: 1.1.18 - resolution: "which-typed-array@npm:1.1.18" + version: 1.1.19 + resolution: "which-typed-array@npm:1.1.19" dependencies: available-typed-arrays: "npm:^1.0.7" call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" - for-each: "npm:^0.3.3" + call-bound: "npm:^1.0.4" + for-each: "npm:^0.3.5" + get-proto: "npm:^1.0.1" gopd: "npm:^1.2.0" has-tostringtag: "npm:^1.0.2" - checksum: 10/11eed801b2bd08cdbaecb17aff381e0fb03526532f61acc06e6c7b9370e08062c33763a51f27825f13fdf34aabd0df6104007f4e8f96e6eaef7db0ce17a26d6e + checksum: 10/12be30fb88567f9863186bee1777f11bea09dd59ed8b3ce4afa7dd5cade75e2f4cc56191a2da165113cc7cf79987ba021dac1e22b5b62aa7e5c56949f2469a68 languageName: node linkType: hard