Merge pull request #20849 from backstage/camilaibs/di-migrate-app-components

[DI] Start app components migration
This commit is contained in:
Camila Belo
2023-11-28 12:27:09 +01:00
committed by GitHub
27 changed files with 684 additions and 17 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-app-api': patch
---
Create a core components extension that allows adopters to override core app components such as `Progress`, `BootErrorPage`, `NotFoundErrorPage` and `ErrorBoundaryFallback`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-plugin-api': patch
---
Create factories for overriding default core components extensions.
+10 -1
View File
@@ -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';
@@ -32,7 +33,10 @@ import {
} from '@backstage/frontend-plugin-api';
import techdocsPlugin from '@backstage/plugin-techdocs/alpha';
import { homePage } from './HomePage';
import { collectLegacyRoutes } from '@backstage/core-compat-api';
import {
collectLegacyComponents,
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';
@@ -114,6 +118,10 @@ const collectedLegacyPlugins = collectLegacyRoutes(
</FlatRoutes>,
);
const legacyAppComponents = collectLegacyComponents({
NotFoundErrorPage: CustomNotFoundErrorPage,
});
const app = createApp({
features: [
graphiqlPlugin,
@@ -129,6 +137,7 @@ const app = createApp({
scmAuthExtension,
scmIntegrationApi,
signInPage,
...legacyAppComponents,
],
}),
],
@@ -0,0 +1,62 @@
/*
* 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 {
createComponentExtension,
coreComponentsRefs,
} from '@backstage/frontend-plugin-api';
import { Box, Typography } from '@material-ui/core';
import { Button } from '@backstage/core-components';
export function CustomNotFoundErrorPage() {
return (
<Box
component="article"
width="100%"
height="100vh"
display="grid"
textAlign="center"
alignContent="center"
justifyContent="center"
justifyItems="center"
>
<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.
</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="/"
style={{ marginTop: '1rem', width: 200 }}
>
Go home
</Button>
</Box>
);
}
export default createComponentExtension({
ref: coreComponentsRefs.notFoundErrorPage,
component: { sync: () => CustomNotFoundErrorPage },
});
+7
View File
@@ -6,7 +6,9 @@
/// <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 { Extension } 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';
@@ -16,6 +18,11 @@ 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>,
): Extension<unknown>[];
// @public (undocumented)
export function collectLegacyRoutes(
flatRoutesElement: JSX.Element,
@@ -0,0 +1,61 @@
/*
* 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 => ({
id: p.id,
attachTo: p.attachTo,
disabled: p.disabled,
})),
).toEqual([
{
id: 'core.components.progress',
attachTo: { id: 'core', input: 'components' },
disabled: false,
},
{
id: 'core.components.bootErrorPage',
attachTo: { id: 'core', input: 'components' },
disabled: false,
},
{
id: 'core.components.notFoundErrorPage',
attachTo: { id: 'core', input: 'components' },
disabled: false,
},
{
id: 'core.components.errorBoundaryFallback',
attachTo: { id: 'core', input: 'components' },
disabled: false,
},
]);
});
});
@@ -0,0 +1,50 @@
/*
* 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 {
Extension,
ComponentRef,
createComponentExtension,
coreComponentsRefs,
} 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<Extension<unknown>[]>(
(extensions, [name, component]) => {
const ref = refs[name];
return ref
? extensions.concat(
createComponentExtension({
ref,
component: { sync: () => component },
}),
)
: extensions;
},
[],
);
}
+1
View File
@@ -14,5 +14,6 @@
* limitations under the License.
*/
export { collectLegacyRoutes } from './collectLegacyRoutes';
export { collectLegacyComponents } from './collectLegacyComponents';
export { convertLegacyApp } from './convertLegacyApp';
export { convertLegacyRouteRef } from './convertLegacyRouteRef';
@@ -0,0 +1,38 @@
/*
* 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, ComponentsApi } from '@backstage/frontend-plugin-api';
/**
* Implementation for the {@linkComponentApi}
*
* @internal
*/
export class DefaultComponentsApi implements ComponentsApi {
#components: Map<ComponentRef<any>, any>;
constructor(components: Map<ComponentRef<any>, any>) {
this.#components = components;
}
getComponent<T>(ref: ComponentRef<T>): T {
const impl = this.#components.get(ref);
if (!impl) {
throw new Error(`No implementation found for component ref ${ref}`);
}
return impl;
}
}
@@ -0,0 +1,17 @@
/*
* 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.
*/
export { DefaultComponentsApi } from './ComponentsApi';
@@ -30,6 +30,9 @@ export const Core = createExtension({
themes: createExtensionInput({
theme: coreExtensionData.theme,
}),
components: createExtensionInput({
component: coreExtensionData.component,
}),
root: createExtensionInput(
{
element: coreExtensionData.reactElement,
@@ -19,6 +19,9 @@ import {
createExtension,
coreExtensionData,
createExtensionInput,
coreComponentsRefs,
useApi,
componentsApiRef,
} from '@backstage/frontend-plugin-api';
import { useRoutes } from 'react-router-dom';
@@ -37,12 +40,21 @@ export const CoreRoutes = createExtension({
},
factory({ inputs }) {
const Routes = () => {
const element = useRoutes(
inputs.routes.map(route => ({
const componentsApi = useApi(componentsApiRef);
const NotFoundErrorPage = componentsApi.getComponent(
coreComponentsRefs.notFoundErrorPage,
);
const element = useRoutes([
...inputs.routes.map(route => ({
path: `${route.path}/*`,
element: route.element,
})),
);
{
path: '*',
element: <NotFoundErrorPage />,
},
]);
return element;
};
@@ -0,0 +1,43 @@
/*
* 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 {
createComponentExtension,
coreComponentsRefs,
} from '@backstage/frontend-plugin-api';
// 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,
component: { sync: () => defaultComponents.Progress },
});
export const DefaultBootErrorPageComponent = createComponentExtension({
ref: coreComponentsRefs.bootErrorPage,
component: { sync: () => defaultComponents.BootErrorPage },
});
export const DefaultNotFoundErrorPageComponent = createComponentExtension({
ref: coreComponentsRefs.notFoundErrorPage,
component: { sync: () => defaultComponents.NotFoundErrorPage },
});
export const DefaultErrorBoundaryComponent = createComponentExtension({
ref: coreComponentsRefs.errorBoundaryFallback,
component: { sync: () => defaultComponents.ErrorBoundaryFallback },
});
@@ -30,11 +30,7 @@ import {
} from '@backstage/frontend-plugin-api';
import { MockConfigApi } from '@backstage/test-utils';
import { createAppTree } from '../tree';
import { Core } from '../extensions/Core';
import { CoreRoutes } from '../extensions/CoreRoutes';
import { CoreNav } from '../extensions/CoreNav';
import { CoreLayout } from '../extensions/CoreLayout';
import { CoreRouter } from '../extensions/CoreRouter';
import { builtinExtensions } from '../wiring/createApp';
const ref1 = createRouteRef();
const ref2 = createRouteRef();
@@ -80,8 +76,8 @@ function routeInfoFromExtensions(extensions: Extension<unknown>[]) {
extensions,
});
const tree = createAppTree({
builtinExtensions,
config: new MockConfigApi({}),
builtinExtensions: [Core, CoreRoutes, CoreNav, CoreLayout, CoreRouter],
features: [plugin],
});
@@ -199,6 +199,12 @@ describe('createApp', () => {
]
</core.router>
]
components [
<core.components.progress out=[component.ref] />
<core.components.errorBoundaryFallback out=[component.ref] />
<core.components.bootErrorPage out=[component.ref] />
<core.components.notFoundErrorPage out=[component.ref] />
]
themes [
<themes.light out=[core.theme] />
<themes.dark out=[core.theme] />
@@ -20,6 +20,8 @@ import {
AppTree,
appTreeApiRef,
BackstagePlugin,
ComponentRef,
componentsApiRef,
coreExtensionData,
ExtensionDataRef,
ExtensionOverrides,
@@ -89,6 +91,12 @@ import { RoutingProvider } from '../routing/RoutingProvider';
import { resolveRouteBindings } from '../routing/resolveRouteBindings';
import { collectRouteIds } from '../routing/collectRouteIds';
import { createAppTree } from '../tree';
import {
DefaultProgressComponent,
DefaultErrorBoundaryComponent,
DefaultBootErrorPageComponent,
DefaultNotFoundErrorPageComponent,
} from '../extensions/components';
import { AppNode } from '@backstage/frontend-plugin-api';
import { toLegacyPlugin } from '../routing/toLegacyPlugin';
import { InternalAppContext } from './InternalAppContext';
@@ -97,13 +105,18 @@ import { CoreRouter } from '../extensions/CoreRouter';
import { toInternalBackstagePlugin } from '../../../frontend-plugin-api/src/wiring/createPlugin';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides';
import { DefaultComponentsApi } from '../apis/implementations/ComponentsApi';
const builtinExtensions = [
export const builtinExtensions = [
Core,
CoreRouter,
CoreRoutes,
CoreNav,
CoreLayout,
DefaultProgressComponent,
DefaultErrorBoundaryComponent,
DefaultBootErrorPageComponent,
DefaultNotFoundErrorPageComponent,
LightTheme,
DarkTheme,
];
@@ -425,6 +438,24 @@ function createApiHolder(
}),
});
const componentsExtensions =
tree.root.edges.attachments
.get('components')
?.map(e => e.instance?.getData(coreExtensionData.component))
.filter(x => !!x) ?? [];
const componentsMap = componentsExtensions.reduce(
(components, component) =>
component ? components.set(component.ref, component?.impl) : components,
new Map<ComponentRef<any>, any>(),
);
factoryRegistry.register('static', {
api: componentsApiRef,
deps: {},
factory: () => new DefaultComponentsApi(componentsMap),
});
factoryRegistry.register('static', {
api: appThemeApiRef,
deps: {},
@@ -22,6 +22,7 @@ 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';
@@ -64,6 +65,7 @@ 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';
@@ -284,6 +286,21 @@ export type CommonAnalyticsContext = {
extensionId: string;
};
// @public (undocumented)
export type ComponentRef<T> = {
id: string;
T: T;
};
// @public
export interface ComponentsApi {
// (undocumented)
getComponent<T>(ref: ComponentRef<T>): T;
}
// @public
export const componentsApiRef: ApiRef<ComponentsApi>;
export { ConfigApi };
export { configApiRef };
@@ -304,6 +321,31 @@ 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>;
};
// @public (undocumented)
export type CoreErrorBoundaryFallbackComponent = ComponentType<
PropsWithChildren<{
plugin?: BackstagePlugin_2;
error: Error;
resetError: () => void;
}>
>;
// @public (undocumented)
export const coreExtensionData: {
reactElement: ConfigurableExtensionDataRef<JSX_2.Element, {}>;
@@ -313,8 +355,23 @@ export const coreExtensionData: {
navTarget: ConfigurableExtensionDataRef<NavTarget, {}>;
theme: ConfigurableExtensionDataRef<AppTheme, {}>;
logoElements: ConfigurableExtensionDataRef<LogoElements, {}>;
component: ConfigurableExtensionDataRef<
{
ref: ComponentRef<ComponentType<any>>;
impl: ComponentType<any>;
},
{}
>;
};
// @public (undocumented)
export type CoreNotFoundErrorPageComponent = ComponentType<
PropsWithChildren<{}>
>;
// @public (undocumented)
export type CoreProgressComponent = ComponentType<PropsWithChildren<{}>>;
// @public (undocumented)
export function createApiExtension<
TConfig extends {},
@@ -341,6 +398,31 @@ export { createApiFactory };
export { createApiRef };
// @public (undocumented)
export function createComponentExtension<
TRef extends ComponentRef<any>,
TConfig extends {},
TInputs extends AnyExtensionInputMap,
>(options: {
ref: TRef;
disabled?: boolean;
inputs?: TInputs;
configSchema?: PortableSchema<TConfig>;
component:
| {
lazy: (values: {
config: TConfig;
inputs: Expand<ExtensionInputValues<TInputs>>;
}) => Promise<TRef['T']>;
}
| {
sync: (values: {
config: TConfig;
inputs: Expand<ExtensionInputValues<TInputs>>;
}) => TRef['T'];
};
}): Extension<TConfig>;
// @public (undocumented)
export function createExtension<
TOutput extends AnyExtensionDataMap,
@@ -0,0 +1,37 @@
/*
* 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 { createApiRef } from '@backstage/core-plugin-api';
import { ComponentRef } from '../../components';
/**
* API for looking up components based on component refs.
*
* @public
*/
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;
}
/**
* The `ApiRef` of {@link ComponentsApi}.
*
* @public
*/
export const componentsApiRef = createApiRef<ComponentsApi>({
id: 'core.components',
});
@@ -34,6 +34,7 @@ export * from './auth';
export * from './AlertApi';
export * from './AppThemeApi';
export * from './ComponentsApi';
export * from './ConfigApi';
export * from './DiscoveryApi';
export * from './ErrorApi';
@@ -0,0 +1,68 @@
/*
* 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 {
CoreBootErrorPageComponent,
CoreErrorBoundaryFallbackComponent,
CoreNotFoundErrorPageComponent,
CoreProgressComponent,
} from '../types';
/** @public */
export type ComponentRef<T> = {
id: string;
T: T;
};
/** @public */
export function createComponentRef<T>(options: {
id: string;
}): ComponentRef<T> {
const { id } = options;
return {
id,
get T(): T {
throw new Error(`tried to read ComponentRef.T of ${id}`);
},
};
}
const coreProgressComponentRef = createComponentRef<CoreProgressComponent>({
id: 'core.components.progress',
});
const coreBootErrorPageComponentRef =
createComponentRef<CoreBootErrorPageComponent>({
id: 'core.components.bootErrorPage',
});
const coreNotFoundErrorPageComponentRef =
createComponentRef<CoreNotFoundErrorPageComponent>({
id: 'core.components.notFoundErrorPage',
});
const coreErrorBoundaryFallbackComponentRef =
createComponentRef<CoreErrorBoundaryFallbackComponent>({
id: 'core.components.errorBoundaryFallback',
});
/** @public */
export const coreComponentsRefs = {
progress: coreProgressComponentRef,
bootErrorPage: coreBootErrorPageComponentRef,
notFoundErrorPage: coreNotFoundErrorPageComponentRef,
errorBoundaryFallback: coreErrorBoundaryFallbackComponentRef,
};
@@ -14,10 +14,14 @@
* limitations under the License.
*/
import React, { PropsWithChildren, ReactNode, useEffect } from 'react';
import React, {
PropsWithChildren,
ReactNode,
Suspense,
useEffect,
} from 'react';
import { AnalyticsContext, useAnalytics } from '@backstage/core-plugin-api';
import { ErrorBoundary } from './ErrorBoundary';
import { ExtensionSuspense } from './ExtensionSuspense';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { routableExtensionRenderedEvent } from '../../../core-plugin-api/src/analytics/Tracker';
import { AppNode } from '../apis';
@@ -60,12 +64,12 @@ export function ExtensionBoundary(props: ExtensionBoundaryProps) {
};
return (
<ExtensionSuspense>
<Suspense fallback="Loading...">
<ErrorBoundary plugin={node.spec.source}>
<AnalyticsContext attributes={attributes}>
<RouteTracker disableTracking={!routable}>{children}</RouteTracker>
</AnalyticsContext>
</ErrorBoundary>
</ExtensionSuspense>
</Suspense>
);
}
@@ -14,6 +14,8 @@
* limitations under the License.
*/
export { coreComponentsRefs, type ComponentRef } from './ComponentRef';
export {
ExtensionBoundary,
type ExtensionBoundaryProps,
@@ -0,0 +1,86 @@
/*
* 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, { lazy } from 'react';
import {
AnyExtensionInputMap,
ExtensionInputValues,
coreExtensionData,
createExtension,
} from '../wiring';
import { Expand } from '../types';
import { PortableSchema } from '../schema';
import { ExtensionBoundary, ComponentRef } from '../components';
/** @public */
export function createComponentExtension<
TRef extends ComponentRef<any>,
TConfig extends {},
TInputs extends AnyExtensionInputMap,
>(options: {
ref: TRef;
disabled?: boolean;
inputs?: TInputs;
configSchema?: PortableSchema<TConfig>;
component:
| {
lazy: (values: {
config: TConfig;
inputs: Expand<ExtensionInputValues<TInputs>>;
}) => Promise<TRef['T']>;
}
| {
sync: (values: {
config: TConfig;
inputs: Expand<ExtensionInputValues<TInputs>>;
}) => TRef['T'];
};
}) {
const id = options.ref.id;
return createExtension({
id,
attachTo: { id: 'core', input: 'components' },
inputs: options.inputs,
disabled: options.disabled,
configSchema: options.configSchema,
output: {
component: coreExtensionData.component,
},
factory({ config, inputs, node }) {
let ExtensionComponent: TRef['T'];
if ('sync' in options.component) {
ExtensionComponent = options.component.sync({ config, inputs });
} else {
const loader = options.component.lazy({ config, inputs });
ExtensionComponent = lazy(() =>
loader.then(component => ({ default: component })),
);
}
return {
component: {
ref: options.ref,
impl: props => (
<ExtensionBoundary node={node}>
<ExtensionComponent {...props} />
</ExtensionBoundary>
),
},
};
},
});
}
@@ -19,3 +19,4 @@ export { createPageExtension } from './createPageExtension';
export { createNavItemExtension } from './createNavItemExtension';
export { createSignInPageExtension } from './createSignInPageExtension';
export { createThemeExtension } from './createThemeExtension';
export { createComponentExtension } from './createComponentExtension';
@@ -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';
+28
View File
@@ -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> = T extends infer O ? { [K in keyof O]: O[K] } : never;
/** @public */
export type CoreProgressComponent = ComponentType<PropsWithChildren<{}>>;
/** @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;
}>
>;
@@ -14,14 +14,15 @@
* limitations under the License.
*/
import { JSX } from 'react';
import { ComponentType, JSX } from 'react';
import {
AnyApiFactory,
AppTheme,
IconComponent,
} from '@backstage/core-plugin-api';
import { createExtensionDataRef } from './createExtensionDataRef';
import { RouteRef } from '../routing';
import { ComponentRef } from '../components';
import { createExtensionDataRef } from './createExtensionDataRef';
/** @public */
export type NavTarget = {
@@ -45,4 +46,8 @@ export const coreExtensionData = {
navTarget: createExtensionDataRef<NavTarget>('core.nav.target'),
theme: createExtensionDataRef<AppTheme>('core.theme'),
logoElements: createExtensionDataRef<LogoElements>('core.logos'),
component: createExtensionDataRef<{
ref: ComponentRef<ComponentType<any>>;
impl: ComponentType<any>;
}>('component.ref'),
};