refactor: use component ref

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2023-10-30 12:13:38 +01:00
parent a159a0498e
commit 52cbee4025
12 changed files with 217 additions and 375 deletions
@@ -15,7 +15,10 @@
*/
import React from 'react';
import { createNotFoundErrorPageExtension } from '@backstage/frontend-plugin-api';
import {
createComponentExtension,
coreNotFoundErrorPageComponentRef,
} from '@backstage/frontend-plugin-api';
import { Box, Typography } from '@material-ui/core';
import { Button } from '@backstage/core-components';
@@ -53,6 +56,7 @@ function CustomNotFoundErrorPage() {
);
}
export default createNotFoundErrorPageExtension({
export default createComponentExtension({
ref: coreNotFoundErrorPageComponentRef,
component: async () => CustomNotFoundErrorPage,
});
@@ -14,8 +14,6 @@
* limitations under the License.
*/
import React from 'react';
import {
coreExtensionData,
createExtension,
@@ -32,12 +30,9 @@ export const Core = createExtension({
themes: createExtensionInput({
theme: coreExtensionData.theme,
}),
components: createExtensionInput(
{
provider: coreExtensionData.reactComponent,
},
{ singleton: true },
),
components: createExtensionInput({
component: coreExtensionData.component,
}),
root: createExtensionInput(
{
element: coreExtensionData.reactElement,
@@ -50,11 +45,7 @@ export const Core = createExtension({
},
factory({ inputs }) {
return {
root: (
<inputs.components.provider>
{inputs.root.element}
</inputs.components.provider>
),
root: inputs.root.element,
};
},
});
@@ -1,144 +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, { PropsWithChildren, useMemo } from 'react';
import {
createExtension,
coreExtensionData,
createExtensionInput,
createProgressExtension,
createBootErrorPageExtension,
createNotFoundErrorPageExtension,
createErrorBoundaryFallbackExtension,
} 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
import { AppContextProvider } from '../../../core-app-api/src/app/AppContext';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { useApp } from '../../../core-plugin-api/src/app/useApp';
export const DefaultProgressComponent = createProgressExtension({
component: async () => defaultComponents.Progress,
});
export const DefaultBootErrorPageComponent = createBootErrorPageExtension({
component: async () => defaultComponents.BootErrorPage,
});
export const DefaultNotFoundErrorPageComponent =
createNotFoundErrorPageExtension({
component: async () => defaultComponents.NotFoundErrorPage,
});
export const DefaultErrorBoundaryComponent =
createErrorBoundaryFallbackExtension({
component: async () => defaultComponents.ErrorBoundaryFallback,
});
export const CoreComponents = createExtension({
id: 'core.components',
attachTo: { id: 'core', input: 'components' },
inputs: {
progress: createExtensionInput(
{
component: coreExtensionData.components.progress,
},
{
optional: true,
singleton: true,
},
),
bootErrorPage: createExtensionInput(
{
component: coreExtensionData.components.bootErrorPage,
},
{
optional: true,
singleton: true,
},
),
notFoundErrorPage: createExtensionInput(
{
component: coreExtensionData.components.notFoundErrorPage,
},
{
optional: true,
singleton: true,
},
),
errorBoundaryFallback: createExtensionInput(
{
component: coreExtensionData.components.errorBoundaryFallback,
},
{
optional: true,
singleton: true,
},
),
},
output: {
provider: coreExtensionData.reactComponent,
},
factory({ bind, inputs }) {
bind({
provider: function Provider(props: PropsWithChildren<{}>) {
const { children } = props;
const app = useApp();
const context = useMemo(
() => ({
...app,
// Only override components
getComponents: () => {
const {
progress,
bootErrorPage,
notFoundErrorPage,
errorBoundaryFallback,
} = inputs;
const {
Progress,
BootErrorPage,
NotFoundErrorPage,
ErrorBoundaryFallback,
...components
} = app.getComponents();
return {
...components,
Progress: progress?.component ?? Progress,
BootErrorPage: bootErrorPage?.component ?? BootErrorPage,
NotFoundErrorPage:
notFoundErrorPage?.component ?? NotFoundErrorPage,
ErrorBoundaryFallback:
errorBoundaryFallback?.component ?? ErrorBoundaryFallback,
};
},
}),
[app],
);
return (
<AppContextProvider appContext={context}>
{children}
</AppContextProvider>
);
},
});
},
});
@@ -0,0 +1,46 @@
/*
* 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,
coreProgressComponentRef,
coreBootErrorPageComponentRef,
coreNotFoundErrorPageComponentRef,
coreErrorBoundaryFallbackComponentRef,
} 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: coreProgressComponentRef,
component: async () => defaultComponents.Progress,
});
export const DefaultBootErrorPageComponent = createComponentExtension({
ref: coreBootErrorPageComponentRef,
component: async () => defaultComponents.BootErrorPage,
});
export const DefaultNotFoundErrorPageComponent = createComponentExtension({
ref: coreNotFoundErrorPageComponentRef,
component: async () => defaultComponents.NotFoundErrorPage,
});
export const DefaultErrorBoundaryComponent = createComponentExtension({
ref: coreErrorBoundaryFallbackComponentRef,
component: async () => defaultComponents.ErrorBoundaryFallback,
});
@@ -30,12 +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 { CoreComponents } from '../extensions/CoreComponents';
import { builtinExtensions } from '../wiring/createApp';
const ref1 = createRouteRef();
const ref2 = createRouteRef();
@@ -81,15 +76,8 @@ function routeInfoFromExtensions(extensions: Extension<unknown>[]) {
extensions,
});
const tree = createAppTree({
builtinExtensions,
config: new MockConfigApi({}),
builtinExtensions: [
Core,
CoreRoutes,
CoreNav,
CoreLayout,
CoreRouter,
CoreComponents,
],
features: [plugin],
});
@@ -20,7 +20,12 @@ import {
AppTree,
appTreeApiRef,
BackstagePlugin,
ComponentRef,
coreBootErrorPageComponentRef,
coreErrorBoundaryFallbackComponentRef,
coreExtensionData,
coreNotFoundErrorPageComponentRef,
coreProgressComponentRef,
ExtensionDataRef,
ExtensionOverrides,
RouteRef,
@@ -90,12 +95,11 @@ import { resolveRouteBindings } from '../routing/resolveRouteBindings';
import { collectRouteIds } from '../routing/collectRouteIds';
import { createAppTree } from '../tree';
import {
CoreComponents,
DefaultProgressComponent,
DefaultErrorBoundaryComponent,
DefaultBootErrorPageComponent,
DefaultNotFoundErrorPageComponent,
} from '../extensions/CoreComponents';
} from '../extensions/components';
import { AppNode } from '@backstage/frontend-plugin-api';
import { toLegacyPlugin } from '../routing/toLegacyPlugin';
import { InternalAppContext } from './InternalAppContext';
@@ -105,13 +109,12 @@ import { toInternalBackstagePlugin } from '../../../frontend-plugin-api/src/wiri
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides';
const builtinExtensions = [
export const builtinExtensions = [
Core,
CoreRouter,
CoreRoutes,
CoreNav,
CoreLayout,
CoreComponents,
DefaultProgressComponent,
DefaultErrorBoundaryComponent,
DefaultBootErrorPageComponent,
@@ -316,6 +319,7 @@ export function createSpecializedApp(options?: {
features.filter(
(f): f is BackstagePlugin => f.$$type === '@backstage/BackstagePlugin',
),
tree.root,
);
const appIdentityProxy = new AppIdentityProxy();
@@ -371,7 +375,49 @@ export function createSpecializedApp(options?: {
};
}
function createLegacyAppContext(plugins: BackstagePlugin[]): AppContext {
function createLegacyAppContext(
plugins: BackstagePlugin[],
core: AppNode,
): AppContext {
const components = core.edges.attachments
.get('components')
?.map(e => e.instance?.getData(coreExtensionData.component));
function getComponent<TRef extends ComponentRef<unknown>>(
ref: TRef,
): TRef['T'] | undefined {
return components?.find(component => component?.ref.id === ref.id)?.impl;
}
const {
Progress: DefaultProgress,
BootErrorPage: DefaultBootErrorPage,
NotFoundErrorPage: DefaultNotFoundErrorPage,
ErrorBoundaryFallback: DefaultErrorBoundaryFallback,
...rest
} = defaultComponents;
const CustomProgress = getComponent(coreProgressComponentRef);
const CustomBootErrorPage = getComponent(coreBootErrorPageComponentRef);
const CustomNotFoundErrorPage = getComponent(
coreNotFoundErrorPageComponentRef,
);
const CustomErrorBoundaryFallback = getComponent(
coreErrorBoundaryFallbackComponentRef,
);
const overriddenComponents = {
...rest,
Progress: CustomProgress ?? DefaultProgress,
BootErrorPage: CustomBootErrorPage ?? DefaultBootErrorPage,
NotFoundErrorPage: CustomNotFoundErrorPage ?? DefaultNotFoundErrorPage,
ErrorBoundaryFallback:
CustomErrorBoundaryFallback ?? DefaultErrorBoundaryFallback,
};
return {
getPlugins(): LegacyBackstagePlugin[] {
return plugins.map(toLegacyPlugin);
@@ -388,14 +434,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,
ErrorBoundaryFallback: () => null,
};
return overriddenComponents;
},
};
}
@@ -0,0 +1,63 @@
/*
* 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}`);
},
};
}
/** @public */
export const coreProgressComponentRef =
createComponentRef<CoreProgressComponent>({ id: 'core.components.progress' });
/** @public */
export const coreBootErrorPageComponentRef =
createComponentRef<CoreBootErrorPageComponent>({
id: 'core.components.bootErrorPage',
});
/** @public */
export const coreNotFoundErrorPageComponentRef =
createComponentRef<CoreNotFoundErrorPageComponent>({
id: 'core.components.notFoundErrorPage',
});
/** @public */
export const coreErrorBoundaryFallbackComponentRef =
createComponentRef<CoreErrorBoundaryFallbackComponent>({
id: 'core.components.errorBoundaryFallback',
});
@@ -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,14 @@
* limitations under the License.
*/
export {
coreProgressComponentRef,
coreBootErrorPageComponentRef,
coreErrorBoundaryFallbackComponentRef,
coreNotFoundErrorPageComponentRef,
type ComponentRef,
} from './ComponentRef';
export { ExtensionError } from './ExtensionError';
export {
@@ -21,40 +21,36 @@ import {
coreExtensionData,
createExtension,
} from '../wiring';
import {
Expand,
CoreProgressComponent,
CoreBootErrorPageComponent,
CoreNotFoundErrorPageComponent,
CoreErrorBoundaryFallbackComponent,
} from '../types';
import { Expand } from '../types';
import { PortableSchema } from '../schema';
import { ExtensionError, ExtensionBoundary } from '../components';
import { ExtensionError, ExtensionBoundary, ComponentRef } from '../components';
/** @public */
export function createProgressExtension<
export function createComponentExtension<
TRef extends ComponentRef<any>,
TConfig extends {},
TInputs extends AnyExtensionInputMap,
>(options: {
ref: TRef;
disabled?: boolean;
inputs?: TInputs;
configSchema?: PortableSchema<TConfig>;
component: (options: {
component: (values: {
config: TConfig;
inputs: Expand<ExtensionInputValues<TInputs>>;
}) => Promise<CoreProgressComponent>;
}) => Promise<TRef['T']>;
}) {
const id = 'core.components.progress';
const id = options.ref.id;
return createExtension({
id,
attachTo: { id: 'core.components', input: 'progress' },
attachTo: { id: 'core', input: 'components' },
inputs: options.inputs,
disabled: options.disabled,
configSchema: options.configSchema,
output: {
component: coreExtensionData.components.progress,
component: coreExtensionData.component,
},
factory({ bind, config, inputs, source }) {
factory({ config, inputs, source }) {
const ExtensionComponent = lazy(() =>
options
.component({ config, inputs })
@@ -64,145 +60,16 @@ export function createProgressExtension<
})),
);
bind({
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>
),
});
return {
component: {
ref: options.ref,
impl: props => (
<ExtensionBoundary id={id} source={source}>
<ExtensionComponent {...props} />
</ExtensionBoundary>
),
},
};
},
});
}
@@ -19,9 +19,4 @@ export { createPageExtension } from './createPageExtension';
export { createNavItemExtension } from './createNavItemExtension';
export { createSignInPageExtension } from './createSignInPageExtension';
export { createThemeExtension } from './createThemeExtension';
export {
createProgressExtension,
createBootErrorPageExtension,
createNotFoundErrorPageExtension,
createErrorBoundaryFallbackExtension,
} from './createComponentExtension';
export { createComponentExtension } from './createComponentExtension';
@@ -14,19 +14,14 @@
* limitations under the License.
*/
import { JSX } from 'react';
import { ComponentType, JSX } from 'react';
import {
AnyApiFactory,
AppTheme,
IconComponent,
} from '@backstage/core-plugin-api';
import { RouteRef } from '../routing';
import {
CoreProgressComponent,
CoreBootErrorPageComponent,
CoreNotFoundErrorPageComponent,
CoreErrorBoundaryFallbackComponent,
} from '../types';
import { ComponentRef } from '../components';
import { createExtensionDataRef } from './createExtensionDataRef';
/** @public */
@@ -44,9 +39,6 @@ export type LogoElements = {
/** @public */
export const coreExtensionData = {
reactComponent: createExtensionDataRef<
<T extends {}>(props: T) => JSX.Element
>('core.reactComponent'),
reactElement: createExtensionDataRef<JSX.Element>('core.reactElement'),
routePath: createExtensionDataRef<string>('core.routing.path'),
apiFactory: createExtensionDataRef<AnyApiFactory>('core.api.factory'),
@@ -54,19 +46,8 @@ export const coreExtensionData = {
navTarget: createExtensionDataRef<NavTarget>('core.nav.target'),
theme: createExtensionDataRef<AppTheme>('core.theme'),
logoElements: createExtensionDataRef<LogoElements>('core.logos'),
components: {
progress: createExtensionDataRef<CoreProgressComponent>(
'core.components.progress',
),
bootErrorPage: createExtensionDataRef<CoreBootErrorPageComponent>(
'core.components.bootErrorPage',
),
notFoundErrorPage: createExtensionDataRef<CoreNotFoundErrorPageComponent>(
'core.components.notFoundErrorPage',
),
errorBoundaryFallback:
createExtensionDataRef<CoreErrorBoundaryFallbackComponent>(
'core.components.errorBoundary',
),
},
component: createExtensionDataRef<{
ref: ComponentRef<ComponentType<any>>;
impl: ComponentType<any>;
}>('component.ref'),
};