Merge pull request #21652 from backstage/camilaibs/app-components-followups
[DI] App components migration follow-up refactors
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': patch
|
||||
---
|
||||
|
||||
Update alpha component ref type to be more specific than any, delete boot page component and use new plugin type for error boundary component extensions.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
Use the new plugin type for error boundary components.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-compat-api': patch
|
||||
---
|
||||
|
||||
Delete alpha DI compatibility helper for components, migrating components should be simple without a helper.
|
||||
@@ -17,7 +17,7 @@
|
||||
import React from 'react';
|
||||
import { createApp } from '@backstage/frontend-app-api';
|
||||
import { pagesPlugin } from './examples/pagesPlugin';
|
||||
import { CustomNotFoundErrorPage } from './examples/notFoundErrorPageExtension';
|
||||
import notFoundErrorPage 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';
|
||||
@@ -33,10 +33,7 @@ import {
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import techdocsPlugin from '@backstage/plugin-techdocs/alpha';
|
||||
import { homePage } from './HomePage';
|
||||
import {
|
||||
collectLegacyComponents,
|
||||
collectLegacyRoutes,
|
||||
} from '@backstage/core-compat-api';
|
||||
import { collectLegacyRoutes } from '@backstage/core-compat-api';
|
||||
import { FlatRoutes } from '@backstage/core-app-api';
|
||||
import { Route } from 'react-router';
|
||||
import { CatalogImportPage } from '@backstage/plugin-catalog-import';
|
||||
@@ -118,10 +115,6 @@ const collectedLegacyPlugins = collectLegacyRoutes(
|
||||
</FlatRoutes>,
|
||||
);
|
||||
|
||||
const legacyAppComponents = collectLegacyComponents({
|
||||
NotFoundErrorPage: CustomNotFoundErrorPage,
|
||||
});
|
||||
|
||||
const app = createApp({
|
||||
features: [
|
||||
graphiqlPlugin,
|
||||
@@ -137,7 +130,7 @@ const app = createApp({
|
||||
scmAuthExtension,
|
||||
scmIntegrationApi,
|
||||
signInPage,
|
||||
...legacyAppComponents,
|
||||
notFoundErrorPage,
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
createComponentExtension,
|
||||
coreComponentsRefs,
|
||||
coreComponentRefs,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { Box, Typography } from '@material-ui/core';
|
||||
import { Button } from '@backstage/core-components';
|
||||
@@ -36,15 +36,9 @@ export function CustomNotFoundErrorPage() {
|
||||
>
|
||||
<Typography variant="h1">404</Typography>
|
||||
<Typography color="textSecondary" paragraph style={{ width: 300 }}>
|
||||
Bowie was unable to locate this page. Please contact your support team
|
||||
if this page used to exist.
|
||||
Unable to locate this page. Please contact your support team if this
|
||||
page used to exist.
|
||||
</Typography>
|
||||
<img
|
||||
alt="Backstage bowie"
|
||||
src="https://info.backstage.spotify.com/hs-fs/hubfs/Call%20Bowie%202.png"
|
||||
width="200"
|
||||
style={{ filter: 'grayscale(50%)' }}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
to="/"
|
||||
@@ -57,6 +51,6 @@ export function CustomNotFoundErrorPage() {
|
||||
}
|
||||
|
||||
export default createComponentExtension({
|
||||
ref: coreComponentsRefs.notFoundErrorPage,
|
||||
ref: coreComponentRefs.notFoundErrorPage,
|
||||
component: { sync: () => CustomNotFoundErrorPage },
|
||||
});
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
/// <reference types="react" />
|
||||
|
||||
import { AnyRouteRefParams } from '@backstage/core-plugin-api';
|
||||
import { AppComponents } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionOverrides } from '@backstage/frontend-plugin-api';
|
||||
import { ExternalRouteRef } from '@backstage/core-plugin-api';
|
||||
import { ExternalRouteRef as ExternalRouteRef_2 } from '@backstage/frontend-plugin-api';
|
||||
@@ -19,11 +17,6 @@ import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api';
|
||||
import { SubRouteRef } from '@backstage/core-plugin-api';
|
||||
import { SubRouteRef as SubRouteRef_2 } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @public (undocumented)
|
||||
export function collectLegacyComponents(
|
||||
components: Partial<AppComponents>,
|
||||
): ExtensionDefinition<unknown>[];
|
||||
|
||||
// @public (undocumented)
|
||||
export function collectLegacyRoutes(
|
||||
flatRoutesElement: JSX.Element,
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* 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 { collectLegacyComponents } from './collectLegacyComponents';
|
||||
|
||||
describe('collectLegacyComponents', () => {
|
||||
const components = {
|
||||
Progress: () => <div>Progress</div>,
|
||||
BootErrorPage: () => <div>BootErrorPage</div>,
|
||||
NotFoundErrorPage: () => <div>NotFoundErrorPage</div>,
|
||||
ErrorBoundaryFallback: () => <div>ErrorBoundaryFallback</div>,
|
||||
};
|
||||
|
||||
it('should collect legacy routes', () => {
|
||||
const collected = collectLegacyComponents(components);
|
||||
|
||||
expect(
|
||||
collected.map(p => ({
|
||||
kind: p.kind,
|
||||
namespace: p.namespace,
|
||||
attachTo: p.attachTo,
|
||||
disabled: p.disabled,
|
||||
})),
|
||||
).toEqual([
|
||||
{
|
||||
kind: 'component',
|
||||
namespace: 'core.components.progress',
|
||||
attachTo: { id: 'core', input: 'components' },
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
kind: 'component',
|
||||
namespace: 'core.components.bootErrorPage',
|
||||
attachTo: { id: 'core', input: 'components' },
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
kind: 'component',
|
||||
namespace: 'core.components.notFoundErrorPage',
|
||||
attachTo: { id: 'core', input: 'components' },
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
kind: 'component',
|
||||
namespace: 'core.components.errorBoundaryFallback',
|
||||
attachTo: { id: 'core', input: 'components' },
|
||||
disabled: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
ComponentRef,
|
||||
createComponentExtension,
|
||||
coreComponentsRefs,
|
||||
ExtensionDefinition,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { AppComponents } from '@backstage/core-plugin-api';
|
||||
|
||||
type ComponentTypes<T = AppComponents> = T[keyof T];
|
||||
|
||||
const refs: Record<string, ComponentRef<ComponentTypes>> = {
|
||||
Progress: coreComponentsRefs.progress,
|
||||
BootErrorPage: coreComponentsRefs.bootErrorPage,
|
||||
NotFoundErrorPage: coreComponentsRefs.notFoundErrorPage,
|
||||
ErrorBoundaryFallback: coreComponentsRefs.errorBoundaryFallback,
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export function collectLegacyComponents(components: Partial<AppComponents>) {
|
||||
return Object.entries(components).reduce<ExtensionDefinition<unknown>[]>(
|
||||
(extensions, [name, component]) => {
|
||||
const ref = refs[name];
|
||||
return ref
|
||||
? extensions.concat(
|
||||
createComponentExtension({
|
||||
ref,
|
||||
component: { sync: () => component },
|
||||
}),
|
||||
)
|
||||
: extensions;
|
||||
},
|
||||
[],
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,5 @@
|
||||
export * from './compatWrapper';
|
||||
|
||||
export { collectLegacyRoutes } from './collectLegacyRoutes';
|
||||
export { collectLegacyComponents } from './collectLegacyComponents';
|
||||
export { convertLegacyApp } from './convertLegacyApp';
|
||||
export { convertLegacyRouteRef } from './convertLegacyRouteRef';
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ComponentType } from 'react';
|
||||
import { ComponentRef, ComponentsApi } from '@backstage/frontend-plugin-api';
|
||||
|
||||
/**
|
||||
@@ -22,13 +23,13 @@ import { ComponentRef, ComponentsApi } from '@backstage/frontend-plugin-api';
|
||||
* @internal
|
||||
*/
|
||||
export class DefaultComponentsApi implements ComponentsApi {
|
||||
#components: Map<ComponentRef<any>, any>;
|
||||
#components: Map<ComponentRef<any>, ComponentType<any>>;
|
||||
|
||||
constructor(components: Map<ComponentRef<any>, any>) {
|
||||
this.#components = components;
|
||||
}
|
||||
|
||||
getComponent<T>(ref: ComponentRef<T>): T {
|
||||
getComponent<T extends {}>(ref: ComponentRef<T>): ComponentType<T> {
|
||||
const impl = this.#components.get(ref);
|
||||
if (!impl) {
|
||||
throw new Error(`No implementation found for component ref ${ref}`);
|
||||
|
||||
@@ -19,9 +19,8 @@ import {
|
||||
createExtension,
|
||||
coreExtensionData,
|
||||
createExtensionInput,
|
||||
coreComponentsRefs,
|
||||
useApi,
|
||||
componentsApiRef,
|
||||
coreComponentRefs,
|
||||
useComponentRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { useRoutes } from 'react-router-dom';
|
||||
|
||||
@@ -41,9 +40,8 @@ export const CoreRoutes = createExtension({
|
||||
},
|
||||
factory({ inputs }) {
|
||||
const Routes = () => {
|
||||
const componentsApi = useApi(componentsApiRef);
|
||||
const NotFoundErrorPage = componentsApi.getComponent(
|
||||
coreComponentsRefs.notFoundErrorPage,
|
||||
const NotFoundErrorPage = useComponentRef(
|
||||
coreComponentRefs.notFoundErrorPage,
|
||||
);
|
||||
|
||||
const element = useRoutes([
|
||||
|
||||
@@ -14,30 +14,43 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
// TODO: Dependency on MUI should be removed from core packages
|
||||
import { Button } from '@material-ui/core';
|
||||
|
||||
import {
|
||||
createComponentExtension,
|
||||
coreComponentsRefs,
|
||||
coreComponentRefs,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { ErrorPanel } from '@backstage/core-components';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { components as defaultComponents } from '../../../app-defaults/src/defaults';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
|
||||
export const DefaultProgressComponent = createComponentExtension({
|
||||
ref: coreComponentsRefs.progress,
|
||||
ref: coreComponentRefs.progress,
|
||||
component: { sync: () => defaultComponents.Progress },
|
||||
});
|
||||
|
||||
export const DefaultBootErrorPageComponent = createComponentExtension({
|
||||
ref: coreComponentsRefs.bootErrorPage,
|
||||
component: { sync: () => defaultComponents.BootErrorPage },
|
||||
});
|
||||
|
||||
export const DefaultNotFoundErrorPageComponent = createComponentExtension({
|
||||
ref: coreComponentsRefs.notFoundErrorPage,
|
||||
ref: coreComponentRefs.notFoundErrorPage,
|
||||
component: { sync: () => defaultComponents.NotFoundErrorPage },
|
||||
});
|
||||
|
||||
export const DefaultErrorBoundaryComponent = createComponentExtension({
|
||||
ref: coreComponentsRefs.errorBoundaryFallback,
|
||||
component: { sync: () => defaultComponents.ErrorBoundaryFallback },
|
||||
ref: coreComponentRefs.errorBoundaryFallback,
|
||||
component: {
|
||||
sync: () => props => {
|
||||
const { plugin, error, resetError } = props;
|
||||
const title = `Error in ${plugin?.id}`;
|
||||
|
||||
return (
|
||||
<ErrorPanel title={title} error={error} defaultExpanded>
|
||||
<Button variant="outlined" onClick={resetError}>
|
||||
Retry
|
||||
</Button>
|
||||
</ErrorPanel>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -201,10 +201,9 @@ describe('createApp', () => {
|
||||
</core/router>
|
||||
]
|
||||
components [
|
||||
<component:core.components.progress out=[component.ref] />
|
||||
<component:core.components.errorBoundaryFallback out=[component.ref] />
|
||||
<component:core.components.bootErrorPage out=[component.ref] />
|
||||
<component:core.components.notFoundErrorPage out=[component.ref] />
|
||||
<component:core.components.progress out=[core.component] />
|
||||
<component:core.components.errorBoundaryFallback out=[core.component] />
|
||||
<component:core.components.notFoundErrorPage out=[core.component] />
|
||||
]
|
||||
themes [
|
||||
<theme:app/light out=[core.theme] />
|
||||
|
||||
@@ -88,7 +88,6 @@ import { createAppTree } from '../tree';
|
||||
import {
|
||||
DefaultProgressComponent,
|
||||
DefaultErrorBoundaryComponent,
|
||||
DefaultBootErrorPageComponent,
|
||||
DefaultNotFoundErrorPageComponent,
|
||||
} from '../extensions/components';
|
||||
import { AppNode } from '@backstage/frontend-plugin-api';
|
||||
@@ -108,7 +107,6 @@ export const builtinExtensions = [
|
||||
CoreLayout,
|
||||
DefaultProgressComponent,
|
||||
DefaultErrorBoundaryComponent,
|
||||
DefaultBootErrorPageComponent,
|
||||
DefaultNotFoundErrorPageComponent,
|
||||
LightTheme,
|
||||
DarkTheme,
|
||||
|
||||
@@ -22,7 +22,6 @@ import { AuthProviderInfo } from '@backstage/core-plugin-api';
|
||||
import { AuthRequestOptions } from '@backstage/core-plugin-api';
|
||||
import { BackstageIdentityApi } from '@backstage/core-plugin-api';
|
||||
import { BackstageIdentityResponse } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin as BackstagePlugin_2 } from '@backstage/core-plugin-api';
|
||||
import { BackstageUserIdentity } from '@backstage/core-plugin-api';
|
||||
import { bitbucketAuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { bitbucketServerAuthApiRef } from '@backstage/core-plugin-api';
|
||||
@@ -68,7 +67,6 @@ import { OpenIdConnectApi } from '@backstage/core-plugin-api';
|
||||
import { PendingOAuthRequest } from '@backstage/core-plugin-api';
|
||||
import { ProfileInfo } from '@backstage/core-plugin-api';
|
||||
import { ProfileInfoApi } from '@backstage/core-plugin-api';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { default as React_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { SessionApi } from '@backstage/core-plugin-api';
|
||||
@@ -297,7 +295,7 @@ export type CommonAnalyticsContext = {
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type ComponentRef<T> = {
|
||||
export type ComponentRef<T extends {} = {}> = {
|
||||
id: string;
|
||||
T: T;
|
||||
};
|
||||
@@ -305,7 +303,7 @@ export type ComponentRef<T> = {
|
||||
// @public
|
||||
export interface ComponentsApi {
|
||||
// (undocumented)
|
||||
getComponent<T>(ref: ComponentRef<T>): T;
|
||||
getComponent<T extends {}>(ref: ComponentRef<T>): ComponentType<T>;
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -332,29 +330,18 @@ export interface ConfigurableExtensionDataRef<
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type CoreBootErrorPageComponent = ComponentType<
|
||||
PropsWithChildren<{
|
||||
step: 'load-config' | 'load-chunk';
|
||||
error: Error;
|
||||
}>
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const coreComponentsRefs: {
|
||||
progress: ComponentRef<CoreProgressComponent>;
|
||||
bootErrorPage: ComponentRef<CoreBootErrorPageComponent>;
|
||||
notFoundErrorPage: ComponentRef<CoreNotFoundErrorPageComponent>;
|
||||
errorBoundaryFallback: ComponentRef<CoreErrorBoundaryFallbackComponent>;
|
||||
export const coreComponentRefs: {
|
||||
progress: ComponentRef<CoreProgressProps>;
|
||||
notFoundErrorPage: ComponentRef<CoreNotFoundErrorPageProps>;
|
||||
errorBoundaryFallback: ComponentRef<CoreErrorBoundaryFallbackProps>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type CoreErrorBoundaryFallbackComponent = ComponentType<
|
||||
PropsWithChildren<{
|
||||
plugin?: BackstagePlugin_2;
|
||||
error: Error;
|
||||
resetError: () => void;
|
||||
}>
|
||||
>;
|
||||
export type CoreErrorBoundaryFallbackProps = {
|
||||
plugin?: BackstagePlugin;
|
||||
error: Error;
|
||||
resetError: () => void;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export const coreExtensionData: {
|
||||
@@ -367,20 +354,20 @@ export const coreExtensionData: {
|
||||
logoElements: ConfigurableExtensionDataRef<LogoElements, {}>;
|
||||
component: ConfigurableExtensionDataRef<
|
||||
{
|
||||
ref: ComponentRef<ComponentType<any>>;
|
||||
impl: ComponentType<any>;
|
||||
ref: ComponentRef;
|
||||
impl: ComponentType;
|
||||
},
|
||||
{}
|
||||
>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type CoreNotFoundErrorPageComponent = ComponentType<
|
||||
PropsWithChildren<{}>
|
||||
>;
|
||||
export type CoreNotFoundErrorPageProps = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type CoreProgressComponent = ComponentType<PropsWithChildren<{}>>;
|
||||
export type CoreProgressProps = {};
|
||||
|
||||
// @public (undocumented)
|
||||
export function createApiExtension<
|
||||
@@ -410,11 +397,11 @@ export { createApiRef };
|
||||
|
||||
// @public (undocumented)
|
||||
export function createComponentExtension<
|
||||
TRef extends ComponentRef<any>,
|
||||
TProps extends {},
|
||||
TConfig extends {},
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
ref: TRef;
|
||||
ref: ComponentRef<TProps>;
|
||||
name?: string;
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
@@ -424,13 +411,13 @@ export function createComponentExtension<
|
||||
lazy: (values: {
|
||||
config: TConfig;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}) => Promise<TRef['T']>;
|
||||
}) => Promise<ComponentType<TProps>>;
|
||||
}
|
||||
| {
|
||||
sync: (values: {
|
||||
config: TConfig;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}) => TRef['T'];
|
||||
}) => ComponentType<TProps>;
|
||||
};
|
||||
}): ExtensionDefinition<TConfig>;
|
||||
|
||||
@@ -1005,6 +992,11 @@ export { useApi };
|
||||
|
||||
export { useApiHolder };
|
||||
|
||||
// @public
|
||||
export function useComponentRef<T extends {}>(
|
||||
ref: ComponentRef<T>,
|
||||
): ComponentType<T>;
|
||||
|
||||
// @public
|
||||
export function useRouteRef<
|
||||
TOptional extends boolean,
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createApiRef } from '@backstage/core-plugin-api';
|
||||
import { ComponentType } from 'react';
|
||||
import { createApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import { ComponentRef } from '../../components';
|
||||
|
||||
/**
|
||||
@@ -24,7 +25,7 @@ import { ComponentRef } from '../../components';
|
||||
*/
|
||||
export interface ComponentsApi {
|
||||
// TODO: Should component refs also provide the default implementation so that we're guaranteed to get a component?
|
||||
getComponent<T>(ref: ComponentRef<T>): T;
|
||||
getComponent<T extends {}>(ref: ComponentRef<T>): ComponentType<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,3 +36,14 @@ export interface ComponentsApi {
|
||||
export const componentsApiRef = createApiRef<ComponentsApi>({
|
||||
id: 'core.components',
|
||||
});
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Returns the component associated with the given ref.
|
||||
*/
|
||||
export function useComponentRef<T extends {}>(
|
||||
ref: ComponentRef<T>,
|
||||
): ComponentType<T> {
|
||||
const componentsApi = useApi(componentsApiRef);
|
||||
return componentsApi.getComponent<T>(ref);
|
||||
}
|
||||
|
||||
@@ -15,54 +15,44 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
CoreBootErrorPageComponent,
|
||||
CoreErrorBoundaryFallbackComponent,
|
||||
CoreNotFoundErrorPageComponent,
|
||||
CoreProgressComponent,
|
||||
CoreErrorBoundaryFallbackProps,
|
||||
CoreNotFoundErrorPageProps,
|
||||
CoreProgressProps,
|
||||
} from '../types';
|
||||
|
||||
/** @public */
|
||||
export type ComponentRef<T> = {
|
||||
export type ComponentRef<T extends {} = {}> = {
|
||||
id: string;
|
||||
T: T;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export function createComponentRef<T>(options: {
|
||||
export function createComponentRef<T extends {} = {}>(options: {
|
||||
id: string;
|
||||
}): ComponentRef<T> {
|
||||
const { id } = options;
|
||||
return {
|
||||
id,
|
||||
get T(): T {
|
||||
throw new Error(`tried to read ComponentRef.T of ${id}`);
|
||||
},
|
||||
};
|
||||
} as ComponentRef<T>;
|
||||
}
|
||||
|
||||
const coreProgressComponentRef = createComponentRef<CoreProgressComponent>({
|
||||
const coreProgressComponentRef = createComponentRef<CoreProgressProps>({
|
||||
id: 'core.components.progress',
|
||||
});
|
||||
|
||||
const coreBootErrorPageComponentRef =
|
||||
createComponentRef<CoreBootErrorPageComponent>({
|
||||
id: 'core.components.bootErrorPage',
|
||||
});
|
||||
|
||||
const coreNotFoundErrorPageComponentRef =
|
||||
createComponentRef<CoreNotFoundErrorPageComponent>({
|
||||
createComponentRef<CoreNotFoundErrorPageProps>({
|
||||
id: 'core.components.notFoundErrorPage',
|
||||
});
|
||||
|
||||
const coreErrorBoundaryFallbackComponentRef =
|
||||
createComponentRef<CoreErrorBoundaryFallbackComponent>({
|
||||
createComponentRef<CoreErrorBoundaryFallbackProps>({
|
||||
id: 'core.components.errorBoundaryFallback',
|
||||
});
|
||||
|
||||
/** @public */
|
||||
export const coreComponentsRefs = {
|
||||
export const coreComponentRefs = {
|
||||
progress: coreProgressComponentRef,
|
||||
bootErrorPage: coreBootErrorPageComponentRef,
|
||||
notFoundErrorPage: coreNotFoundErrorPageComponentRef,
|
||||
errorBoundaryFallback: coreErrorBoundaryFallbackComponentRef,
|
||||
};
|
||||
|
||||
@@ -14,35 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { Component, PropsWithChildren } from 'react';
|
||||
// TODO: Dependency on MUI should be removed from core packages
|
||||
import { Button } from '@material-ui/core';
|
||||
import { ErrorPanel } from '@backstage/core-components';
|
||||
import React, { Component, ComponentType, PropsWithChildren } from 'react';
|
||||
import { BackstagePlugin } from '../wiring';
|
||||
import { CoreErrorBoundaryFallbackProps } from '../types';
|
||||
|
||||
type DefaultErrorBoundaryFallbackProps = PropsWithChildren<{
|
||||
type ErrorBoundaryProps = PropsWithChildren<{
|
||||
plugin?: BackstagePlugin;
|
||||
error: Error;
|
||||
resetError: () => void;
|
||||
fallback: ComponentType<CoreErrorBoundaryFallbackProps>;
|
||||
}>;
|
||||
|
||||
const DefaultErrorBoundaryFallback = ({
|
||||
plugin,
|
||||
error,
|
||||
resetError,
|
||||
}: DefaultErrorBoundaryFallbackProps) => {
|
||||
const title = `Error in ${plugin?.id}`;
|
||||
|
||||
return (
|
||||
<ErrorPanel title={title} error={error} defaultExpanded>
|
||||
<Button variant="outlined" onClick={resetError}>
|
||||
Retry
|
||||
</Button>
|
||||
</ErrorPanel>
|
||||
);
|
||||
};
|
||||
|
||||
type ErrorBoundaryProps = PropsWithChildren<{ plugin?: BackstagePlugin }>;
|
||||
type ErrorBoundaryState = { error?: Error };
|
||||
|
||||
/** @internal */
|
||||
@@ -62,12 +41,11 @@ export class ErrorBoundary extends Component<
|
||||
|
||||
render() {
|
||||
const { error } = this.state;
|
||||
const { plugin, children } = this.props;
|
||||
const { plugin, children, fallback: Fallback } = this.props;
|
||||
|
||||
if (error) {
|
||||
// TODO: use a configurable error boundary fallback
|
||||
return (
|
||||
<DefaultErrorBoundaryFallback
|
||||
<Fallback
|
||||
plugin={plugin}
|
||||
error={error}
|
||||
resetError={this.handleErrorReset}
|
||||
|
||||
@@ -24,7 +24,8 @@ import { AnalyticsContext, useAnalytics } from '@backstage/core-plugin-api';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { routableExtensionRenderedEvent } from '../../../core-plugin-api/src/analytics/Tracker';
|
||||
import { AppNode } from '../apis';
|
||||
import { AppNode, useComponentRef } from '../apis';
|
||||
import { coreComponentRefs } from './ComponentRef';
|
||||
|
||||
type RouteTrackerProps = PropsWithChildren<{
|
||||
disableTracking?: boolean;
|
||||
@@ -57,6 +58,10 @@ export interface ExtensionBoundaryProps {
|
||||
export function ExtensionBoundary(props: ExtensionBoundaryProps) {
|
||||
const { node, routable, children } = props;
|
||||
|
||||
const plugin = node.spec.source;
|
||||
const Progress = useComponentRef(coreComponentRefs.progress);
|
||||
const fallback = useComponentRef(coreComponentRefs.errorBoundaryFallback);
|
||||
|
||||
// Skipping "routeRef" attribute in the new system, the extension "id" should provide more insight
|
||||
const attributes = {
|
||||
extensionId: node.spec.id,
|
||||
@@ -64,8 +69,8 @@ export function ExtensionBoundary(props: ExtensionBoundaryProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Suspense fallback="Loading...">
|
||||
<ErrorBoundary plugin={node.spec.source}>
|
||||
<Suspense fallback={<Progress />}>
|
||||
<ErrorBoundary plugin={plugin} fallback={fallback}>
|
||||
<AnalyticsContext attributes={attributes}>
|
||||
<RouteTracker disableTracking={!routable}>{children}</RouteTracker>
|
||||
</AnalyticsContext>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { coreComponentsRefs, type ComponentRef } from './ComponentRef';
|
||||
export { coreComponentRefs, type ComponentRef } from './ComponentRef';
|
||||
|
||||
export {
|
||||
ExtensionBoundary,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { lazy } from 'react';
|
||||
import React, { lazy, ComponentType } from 'react';
|
||||
import {
|
||||
AnyExtensionInputMap,
|
||||
ResolvedExtensionInputs,
|
||||
@@ -27,11 +27,11 @@ import { ExtensionBoundary, ComponentRef } from '../components';
|
||||
|
||||
/** @public */
|
||||
export function createComponentExtension<
|
||||
TRef extends ComponentRef<any>,
|
||||
TProps extends {},
|
||||
TConfig extends {},
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
ref: TRef;
|
||||
ref: ComponentRef<TProps>;
|
||||
name?: string;
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
@@ -41,13 +41,13 @@ export function createComponentExtension<
|
||||
lazy: (values: {
|
||||
config: TConfig;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}) => Promise<TRef['T']>;
|
||||
}) => Promise<ComponentType<TProps>>;
|
||||
}
|
||||
| {
|
||||
sync: (values: {
|
||||
config: TConfig;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}) => TRef['T'];
|
||||
}) => ComponentType<TProps>;
|
||||
};
|
||||
}) {
|
||||
return createExtension({
|
||||
@@ -62,15 +62,17 @@ export function createComponentExtension<
|
||||
component: coreExtensionData.component,
|
||||
},
|
||||
factory({ config, inputs, node }) {
|
||||
let ExtensionComponent: TRef['T'];
|
||||
let ExtensionComponent: ComponentType<TProps>;
|
||||
|
||||
if ('sync' in options.component) {
|
||||
ExtensionComponent = options.component.sync({ config, inputs });
|
||||
} else {
|
||||
const loader = options.component.lazy({ config, inputs });
|
||||
const lazyLoader = options.component.lazy;
|
||||
ExtensionComponent = lazy(() =>
|
||||
loader.then(component => ({ default: component })),
|
||||
);
|
||||
lazyLoader({ config, inputs }).then(component => ({
|
||||
default: component,
|
||||
})),
|
||||
) as unknown as ComponentType<TProps>;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -78,7 +80,7 @@ export function createComponentExtension<
|
||||
ref: options.ref,
|
||||
impl: props => (
|
||||
<ExtensionBoundary node={node}>
|
||||
<ExtensionComponent {...props} />
|
||||
<ExtensionComponent {...(props as TProps)} />
|
||||
</ExtensionBoundary>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -32,8 +32,7 @@ export * from './translation';
|
||||
export * from './wiring';
|
||||
|
||||
export type {
|
||||
CoreProgressComponent,
|
||||
CoreBootErrorPageComponent,
|
||||
CoreNotFoundErrorPageComponent,
|
||||
CoreErrorBoundaryFallbackComponent,
|
||||
CoreProgressProps,
|
||||
CoreNotFoundErrorPageProps,
|
||||
CoreErrorBoundaryFallbackProps,
|
||||
} from './types';
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { ComponentType, PropsWithChildren } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { BackstagePlugin } from './wiring';
|
||||
|
||||
// TODO(Rugvip): This might be a quite useful utility type, maybe add to @backstage/types?
|
||||
/**
|
||||
@@ -25,26 +25,16 @@ import { ComponentType, PropsWithChildren } from 'react';
|
||||
export type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
|
||||
|
||||
/** @public */
|
||||
export type CoreProgressComponent = ComponentType<PropsWithChildren<{}>>;
|
||||
export type CoreProgressProps = {};
|
||||
|
||||
/** @public */
|
||||
export type CoreBootErrorPageComponent = ComponentType<
|
||||
PropsWithChildren<{
|
||||
step: 'load-config' | 'load-chunk';
|
||||
error: Error;
|
||||
}>
|
||||
>;
|
||||
export type CoreNotFoundErrorPageProps = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type CoreNotFoundErrorPageComponent = ComponentType<
|
||||
PropsWithChildren<{}>
|
||||
>;
|
||||
|
||||
/** @public */
|
||||
export type CoreErrorBoundaryFallbackComponent = ComponentType<
|
||||
PropsWithChildren<{
|
||||
plugin?: BackstagePlugin;
|
||||
error: Error;
|
||||
resetError: () => void;
|
||||
}>
|
||||
>;
|
||||
export type CoreErrorBoundaryFallbackProps = {
|
||||
plugin?: BackstagePlugin;
|
||||
error: Error;
|
||||
resetError: () => void;
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@ export const coreExtensionData = {
|
||||
theme: createExtensionDataRef<AppTheme>('core.theme'),
|
||||
logoElements: createExtensionDataRef<LogoElements>('core.logos'),
|
||||
component: createExtensionDataRef<{
|
||||
ref: ComponentRef<ComponentType<any>>;
|
||||
impl: ComponentType<any>;
|
||||
}>('component.ref'),
|
||||
ref: ComponentRef;
|
||||
impl: ComponentType;
|
||||
}>('core.component'),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user