refactor: lazy load core components

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2023-10-27 09:48:12 +02:00
parent c26c409566
commit 04c8b48fb9
11 changed files with 327 additions and 119 deletions
+1 -37
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';
@@ -29,7 +30,6 @@ import {
createExtension,
createApiExtension,
createExtensionOverrides,
createNotFoundErrorPageExtension,
} from '@backstage/frontend-plugin-api';
import techdocsPlugin from '@backstage/plugin-techdocs/alpha';
import { homePage } from './HomePage';
@@ -49,8 +49,6 @@ import {
} from '@backstage/integration-react';
import { createSignInPageExtension } from '@backstage/frontend-plugin-api';
import { SignInPage } from '@backstage/core-components';
import { Box, Typography } from '@material-ui/core';
import { Button } from '@backstage/core-components';
/*
@@ -111,40 +109,6 @@ const scmIntegrationApi = createApiExtension({
}),
});
const customNotFoundErrorPage = createNotFoundErrorPageExtension({
component: () => (
<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>
),
});
const collectedLegacyPlugins = collectLegacyRoutes(
<FlatRoutes>
<Route path="/catalog-import" element={<CatalogImportPage />} />
@@ -0,0 +1,58 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { createNotFoundErrorPageExtension } from '@backstage/frontend-plugin-api';
import { Box, Typography } from '@material-ui/core';
import { Button } from '@backstage/core-components';
function CustomNotFoundErrorPage() {
return (
<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 createNotFoundErrorPageExtension({
component: async () => CustomNotFoundErrorPage,
});
@@ -32,21 +32,21 @@ import { AppContextProvider } from '../../../core-app-api/src/app/AppContext';
import { useApp } from '../../../core-plugin-api/src/app/useApp';
export const DefaultProgressComponent = createProgressExtension({
component: defaultComponents.Progress,
component: async () => defaultComponents.Progress,
});
export const DefaultBootErrorPageComponent = createBootErrorPageExtension({
component: defaultComponents.BootErrorPage,
component: async () => defaultComponents.BootErrorPage,
});
export const DefaultNotFoundErrorPageComponent =
createNotFoundErrorPageExtension({
component: defaultComponents.NotFoundErrorPage,
component: async () => defaultComponents.NotFoundErrorPage,
});
export const DefaultErrorBoundaryComponent =
createErrorBoundaryFallbackExtension({
component: defaultComponents.ErrorBoundaryFallback,
component: async () => defaultComponents.ErrorBoundaryFallback,
});
export const CoreComponents = createExtension({
@@ -390,6 +390,7 @@ function createLegacyAppContext(plugins: BackstagePlugin[]): AppContext {
getComponents(): AppComponents {
return {
...defaultComponents,
// The default nullable components are overridden by the CoreComponents built-in extension
Progress: () => null,
BootErrorPage: () => null,
NotFoundErrorPage: () => null,
@@ -0,0 +1,27 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { useApp } from '../../../core-plugin-api/src/app/useApp';
/** @public */
export function ExtensionError(props: { error: Error }) {
const { error } = props;
const app = useApp();
const { BootErrorPage } = app.getComponents();
return <BootErrorPage step="load-chunk" error={error} />;
}
@@ -14,6 +14,8 @@
* limitations under the License.
*/
export { ExtensionError } from './ExtensionError';
export {
ExtensionBoundary,
type ExtensionBoundaryProps,
@@ -14,81 +14,194 @@
* limitations under the License.
*/
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppComponents } from '../../../core-app-api/src/app/types';
import { coreExtensionData, createExtension } from '../wiring';
import React, { lazy } from 'react';
import {
AnyExtensionInputMap,
ExtensionInputValues,
coreExtensionData,
createExtension,
} from '../wiring';
import {
Expand,
CoreProgressComponent,
CoreBootErrorPageComponent,
CoreNotFoundErrorPageComponent,
CoreErrorBoundaryFallbackComponent,
} from '../types';
import { PortableSchema } from '../schema';
import { ExtensionError, ExtensionBoundary } from '../components';
/** @public */
export function createBootErrorPageExtension(options: {
component: AppComponents['BootErrorPage'];
export function createProgressExtension<
TConfig extends {},
TInputs extends AnyExtensionInputMap,
>(options: {
disabled?: boolean;
inputs?: TInputs;
configSchema?: PortableSchema<TConfig>;
component: (options: {
config: TConfig;
inputs: Expand<ExtensionInputValues<TInputs>>;
}) => Promise<CoreProgressComponent>;
}) {
const id = 'core.components.progress';
return createExtension({
id: `core.components.bootErrorPage`,
attachTo: { id: 'core.components', input: 'bootErrorPage' },
inputs: {},
output: {
component: coreExtensionData.components.bootErrorPage,
},
factory({ bind }) {
bind({
component: options.component,
});
},
});
}
/** @public */
export function createNotFoundErrorPageExtension(options: {
component: AppComponents['NotFoundErrorPage'];
}) {
return createExtension({
id: `core.components.notFoundErrorPage`,
attachTo: { id: 'core.components', input: 'notFoundErrorPage' },
inputs: {},
output: {
component: coreExtensionData.components.notFoundErrorPage,
},
factory({ bind }) {
bind({
component: options.component,
});
},
});
}
/** @public */
export function createErrorBoundaryFallbackExtension(options: {
component: AppComponents['ErrorBoundaryFallback'];
}) {
return createExtension({
id: `core.components.errorBoundaryFallback`,
attachTo: { id: 'core.components', input: 'errorBoundaryFallback' },
inputs: {},
output: {
component: coreExtensionData.components.errorBoundaryFallback,
},
factory({ bind }) {
bind({
component: options.component,
});
},
});
}
/** @public */
export function createProgressExtension(options: {
component: AppComponents['Progress'];
}) {
return createExtension({
id: `core.components.progress`,
id,
attachTo: { id: 'core.components', input: 'progress' },
inputs: {},
inputs: options.inputs,
disabled: options.disabled,
configSchema: options.configSchema,
output: {
component: coreExtensionData.components.progress,
},
factory({ bind }) {
factory({ bind, config, inputs, source }) {
const ExtensionComponent = lazy(() =>
options
.component({ config, inputs })
.then(component => ({ default: component }))
.catch(error => ({
default: () => <ExtensionError error={error} />,
})),
);
bind({
component: options.component,
component: props => (
<ExtensionBoundary id={id} source={source}>
<ExtensionComponent {...props} />
</ExtensionBoundary>
),
});
},
});
}
/** @public */
export function createBootErrorPageExtension<
TConfig extends {},
TInputs extends AnyExtensionInputMap,
>(options: {
disabled?: boolean;
inputs?: TInputs;
configSchema?: PortableSchema<TConfig>;
component: (options: {
config: TConfig;
inputs: Expand<ExtensionInputValues<TInputs>>;
}) => Promise<CoreBootErrorPageComponent>;
}) {
const id = 'core.components.bootErrorPage';
return createExtension({
id,
attachTo: { id: 'core.components', input: 'bootErrorPage' },
inputs: options.inputs,
disabled: options.disabled,
configSchema: options.configSchema,
output: {
component: coreExtensionData.components.bootErrorPage,
},
factory({ bind, config, inputs, source }) {
const ExtensionComponent = lazy(() =>
options
.component({ config, inputs })
.then(component => ({ default: component }))
.catch(error => ({
default: () => <ExtensionError error={error} />,
})),
);
bind({
component: props => (
<ExtensionBoundary id={id} source={source}>
<ExtensionComponent {...props} />
</ExtensionBoundary>
),
});
},
});
}
/** @public */
export function createNotFoundErrorPageExtension<
TConfig extends {},
TInputs extends AnyExtensionInputMap,
>(options: {
disabled?: boolean;
inputs?: TInputs;
configSchema?: PortableSchema<TConfig>;
component: (options: {
config: TConfig;
inputs: Expand<ExtensionInputValues<TInputs>>;
}) => Promise<CoreNotFoundErrorPageComponent>;
}) {
const id = 'core.components.notFoundErrorPage';
return createExtension({
id,
attachTo: { id: 'core.components', input: 'notFoundErrorPage' },
inputs: options.inputs,
disabled: options.disabled,
configSchema: options.configSchema,
output: {
component: coreExtensionData.components.notFoundErrorPage,
},
factory({ bind, config, inputs, source }) {
const ExtensionComponent = lazy(() =>
options
.component({ config, inputs })
.then(component => ({ default: component }))
.catch(error => ({
default: () => <ExtensionError error={error} />,
})),
);
bind({
component: props => (
<ExtensionBoundary id={id} source={source}>
<ExtensionComponent {...props} />
</ExtensionBoundary>
),
});
},
});
}
/** @public */
export function createErrorBoundaryFallbackExtension<
TConfig extends {},
TInputs extends AnyExtensionInputMap,
>(options: {
disabled?: boolean;
inputs?: TInputs;
configSchema?: PortableSchema<TConfig>;
component: (options: {
config: TConfig;
inputs: Expand<ExtensionInputValues<TInputs>>;
}) => Promise<CoreErrorBoundaryFallbackComponent>;
}) {
const id = 'core.components.errorBoundaryFallback';
return createExtension({
id,
attachTo: { id: 'core.components', input: 'errorBoundaryFallback' },
inputs: options.inputs,
disabled: options.disabled,
configSchema: options.configSchema,
output: {
component: coreExtensionData.components.errorBoundaryFallback,
},
factory({ bind, config, inputs, source }) {
const ExtensionComponent = lazy(() =>
options
.component({ config, inputs })
.then(component => ({ default: component }))
.catch(error => ({
default: () => <ExtensionError error={error} />,
})),
);
bind({
component: props => (
<ExtensionBoundary id={id} source={source}>
<ExtensionComponent {...props} />
</ExtensionBoundary>
),
});
},
});
@@ -15,7 +15,7 @@
*/
import React, { lazy } from 'react';
import { ExtensionBoundary } from '../components';
import { ExtensionError, ExtensionBoundary } from '../components';
import { createSchemaFromZod, PortableSchema } from '../schema';
import {
coreExtensionData,
@@ -79,7 +79,10 @@ export function createPageExtension<
const ExtensionComponent = lazy(() =>
options
.loader({ config, inputs })
.then(element => ({ default: () => element })),
.then(element => ({ default: () => element }))
.catch(error => ({
default: () => <ExtensionError error={error} />,
})),
);
return {
@@ -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;
}>
>;
@@ -20,9 +20,13 @@ import {
AppTheme,
IconComponent,
} from '@backstage/core-plugin-api';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppComponents } from '../../../core-app-api/src/app/types';
import { RouteRef } from '../routing';
import {
CoreProgressComponent,
CoreBootErrorPageComponent,
CoreNotFoundErrorPageComponent,
CoreErrorBoundaryFallbackComponent,
} from '../types';
import { createExtensionDataRef } from './createExtensionDataRef';
/** @public */
@@ -51,17 +55,18 @@ export const coreExtensionData = {
theme: createExtensionDataRef<AppTheme>('core.theme'),
logoElements: createExtensionDataRef<LogoElements>('core.logos'),
components: {
progress: createExtensionDataRef<AppComponents['Progress']>(
progress: createExtensionDataRef<CoreProgressComponent>(
'core.components.progress',
),
bootErrorPage: createExtensionDataRef<AppComponents['BootErrorPage']>(
bootErrorPage: createExtensionDataRef<CoreBootErrorPageComponent>(
'core.components.bootErrorPage',
),
notFoundErrorPage: createExtensionDataRef<
AppComponents['NotFoundErrorPage']
>('core.components.notFoundErrorPage'),
errorBoundaryFallback: createExtensionDataRef<
AppComponents['ErrorBoundaryFallback']
>('core.components.errorBoundary'),
notFoundErrorPage: createExtensionDataRef<CoreNotFoundErrorPageComponent>(
'core.components.notFoundErrorPage',
),
errorBoundaryFallback:
createExtensionDataRef<CoreErrorBoundaryFallbackComponent>(
'core.components.errorBoundary',
),
},
};