Merge pull request #20849 from backstage/camilaibs/di-migrate-app-components
[DI] Start app components migration
This commit is contained in:
@@ -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`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': patch
|
||||
---
|
||||
|
||||
Create factories for overriding default core components extensions.
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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;
|
||||
},
|
||||
[],
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user