From 0be714921d808ae6c34b70cf99b3e477c0d00171 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 8 Aug 2025 13:20:19 +0200 Subject: [PATCH 1/2] feat: added some docs around swappable Signed-off-by: benjdlambert --- .../building-apps/03-built-in-extensions.md | 26 ++--- .../06-swappable-components.md | 98 +++++++++++++++++++ 2 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 docs/frontend-system/building-plugins/06-swappable-components.md diff --git a/docs/frontend-system/building-apps/03-built-in-extensions.md b/docs/frontend-system/building-apps/03-built-in-extensions.md index 5d0898679b..4122e732c8 100644 --- a/docs/frontend-system/building-apps/03-built-in-extensions.md +++ b/docs/frontend-system/building-apps/03-built-in-extensions.md @@ -37,14 +37,14 @@ This extension is the first extension attached to the extension tree. It is resp #### Inputs -| Name | Description | Type | Optional | Default | Extension creator | -| ------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -| root | The app root element. | [coreExtensionData.reactElement](https://backstage.io/docs/reference/frontend-plugin-api.coreextensiondata) | false | The [`App/Root`](#app-root) extension output. | No creator available, configure or override the [`App/Root`](#app-root) extension. | -| apis | The app apis factories. | [createApiExtension.factoryDataRef](https://backstage.io/docs/reference/frontend-plugin-api.createapiextension.factorydataref) | false | See [default apis](#default-apis-extensions). | [createApiExtension](https://backstage.io/docs/reference/frontend-plugin-api.createapiextension) | -| themes | The app themes list. | [createThemeExtension.themeDataRef](https://backstage.io/docs/reference/frontend-plugin-api.createthemeextension.themedataref) | false | See [default themes](#default-theme-extensions). | [createThemeExtension](https://backstage.io/docs/reference/frontend-plugin-api.createthemeextension) | -| components | The app components list. | [createComponentExtension.componentDataRef](https://backstage.io/docs/reference/frontend-plugin-api.createcomponentextension.componentdataref) | false | See [default components](#default-components-extensions). | [createComponentExtension](https://backstage.io/docs/reference/frontend-plugin-api.createcomponentextension) | -| translations | The app translations list. | [createTranslationExtension.translationDataRef](https://backstage.io/docs/reference/frontend-plugin-api.createtranslationextension.translationdataref) | false | - | [createTranslationExtension](https://backstage.io/docs/reference/frontend-plugin-api.createtranslationextension) | -| icons | The app icons list. | [IconBundleBlueprint.dataRefs.icons](https://backstage.io/docs/reference/frontend-plugin-api.iconbundleblueprint.dataRefs.icons) | true | - | [IconBundleBlueprint](https://backstage.io/docs/reference/frontend-plugin-api.iconbundleblueprint) | +| Name | Description | Type | Optional | Default | Extension creator | +| ------------ | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| root | The app root element. | [coreExtensionData.reactElement](https://backstage.io/docs/reference/frontend-plugin-api.coreextensiondata) | false | The [`App/Root`](#app-root) extension output. | No creator available, configure or override the [`App/Root`](#app-root) extension. | +| apis | The app apis factories. | [createApiExtension.factoryDataRef](https://backstage.io/docs/reference/frontend-plugin-api.createapiextension.factorydataref) | false | See [default apis](#default-apis-extensions). | [createApiExtension](https://backstage.io/docs/reference/frontend-plugin-api.createapiextension) | +| themes | The app themes list. | [createThemeExtension.themeDataRef](https://backstage.io/docs/reference/frontend-plugin-api.createthemeextension.themedataref) | false | See [default themes](#default-theme-extensions). | [createThemeExtension](https://backstage.io/docs/reference/frontend-plugin-api.createthemeextension) | +| components | The app components list. | [SwappableComponentBlueprint.dataRefs.component](https://backstage.io/docs/reference/frontend-plugin-api.swappablecomponentblueprint.dataRefs.component) | false | See [default components](#default-components-extensions). | [SwappableComponentBlueprint](https://backstage.io/docs/reference/frontend-plugin-api.swappablecomponentblueprint) | +| translations | The app translations list. | [createTranslationExtension.translationDataRef](https://backstage.io/docs/reference/frontend-plugin-api.createtranslationextension.translationdataref) | false | - | [createTranslationExtension](https://backstage.io/docs/reference/frontend-plugin-api.createtranslationextension) | +| icons | The app icons list. | [IconBundleBlueprint.dataRefs.icons](https://backstage.io/docs/reference/frontend-plugin-api.iconbundleblueprint.dataRefs.icons) | true | - | [IconBundleBlueprint](https://backstage.io/docs/reference/frontend-plugin-api.iconbundleblueprint) | #### Default theme extensions @@ -59,11 +59,11 @@ Extensions that provides default theme inputs for the `App` extension. Extensions that provides default components inputs for the `App` extension. -| kind | namespace | name | id | -| :--------: | :-------: | :-----------------------------------: | :----------------------------------------------------: | -| components | app | core.components.progress | `components:app/core.components.progress` | -| components | app | core.components.notFoundErrorPage | `components:app/core.components.notFoundErrorPage` | -| components | app | core.components.errorBoundaryFallback | `components:app/core.components.errorBoundaryFallback` | +| kind | namespace | name | id | +| :--------: | :-------: | :-------------------------------: | :------------------------------------------------: | +| components | app | core.components.progress | `components:app/core.components.progress` | +| components | app | core.components.notFoundErrorPage | `components:app/core.components.notFoundErrorPage` | +| components | app | core.components.errorDisplay | `components:app/core.components.errorDisplay` | #### Default apis extensions diff --git a/docs/frontend-system/building-plugins/06-swappable-components.md b/docs/frontend-system/building-plugins/06-swappable-components.md new file mode 100644 index 0000000000..4c03954bb2 --- /dev/null +++ b/docs/frontend-system/building-plugins/06-swappable-components.md @@ -0,0 +1,98 @@ +--- +id: swappable-components +title: Swappable components +sidebar_label: Swappable components +description: Configuring or overriding Swappable Components +--- + +# Swappable components + +Swappable components are a feature of the frontend system that allow you to replace the implementations of components that are used in your Backstage app. +These Swappable Components are defined using `createSwappableComponent` and then can be exported from a plugins `-react` package in order to be used in both other plugins, and to be rebound to a new implementation by the Backstage Integrator. + +## Creating a Swappable Component + +In order to create a Swappable Component, you need to use the `createSwappableComponent` function from the `@backstage/frontend-plugin-api` package. You can supply a default implementation for the component, as well as a way to separate both the props of the external component and in the implementation of the component. + +```tsx title="in @internal/plugin-example-react" +import { createSwappableComponent } from '@backstage/frontend-plugin-api'; + +export const ExampleSwappableComponent = createSwappableComponent({ + name: 'example', + + // This is a loader for loading the default implementation of the component when there's no overriden + // implementation created with `SwappableComponentBlueprint`. + // It can be sync like below, but is can also be async like `loader: () => import('./DefaultImplementation').then(m => m.default)`. + loader: () => (props: { name: string }) => +
Your name is {props.name}
, + + // This is an optional function that can be used to transform the props of the external component. + // If it's not provided, the props of the external component will be passed through unchanged. + transformProps: (props: { lastName: string; firstName: string }) => ({ + name: `${props.firstName} ${props.lastName}`, + }), +}); +``` + +## Using a Swappable Component + +Using a Swappable Component is just like using any other React Component. The return of `createSwappableComponent` can be rendered directly in your plugin, or any plugins that will be consuming your `-react` package. + +```tsx title="in @internal/plugin-example" +import { ExampleSwappableComponent } from '@internal/plugin-example-react'; + +; +``` + +## Overriding a Swappable Component + +In order to override a Swappable Component, you need to create a `SwappableComponentBlueprint` and install it with the `app` plugin. + +```tsx title="in packages/app/src/App.tsx" +import { ExampleSwappableComponent } from '@internal/plugin-example-react'; + +... + + +const app = createApp({ + features: [ + // Must be installed inside the App Plugin. + appPlugin.withOverrides({ + extensions: [ + // Create a binding between the original c + SwappableComponentBlueprint.make({ + params: defineParams => defineParams({ + component: ExampleSwappableComponent, + loader: () => import('./ExampleComponent').then(m => m.SwappableComponent), + + // or sync: + // loader: () => MyNewImplementation + }) + }) + ] + }), + ... + ] +}) + +``` + +## Default Swappable Components + +Currently there is only three different built-in Swappable Components that you can replace the implementations of, and these live in `@backstage/frontend-plugin-api`. They are as follows: + +- ` +- ` +- ` + +You can see more about these components at their [definition](https://github.com/backstage/backstage/blob/master/packages/frontend-plugin-api/src/components/DefaultSwappableComponents.tsx), and their default implementations are shipped inside the [`app-plugin`](https://github.com/backstage/backstage/blob/master/plugins/app/src/extensions/components.tsx). + +## Implementations in tests + +By default, if no `loader` is passed through to `createSwappableComponent` then there's a default fallback component which will be rendered, which is mainly helpful in tests. The default implementation for a Swappable Component without a `loader` is as follows: + +```tsx +props =>
; +``` + +Which means that you can use the `getByTestId` in tests to assert that these components have been rendered on the page. From e8333c8cd47572003776586bf46f1d2081e6e3f8 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 11 Aug 2025 12:00:15 +0200 Subject: [PATCH 2/2] chore: updating doc Signed-off-by: benjdlambert --- .../06-swappable-components.md | 73 +++++++++++++------ 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/docs/frontend-system/building-plugins/06-swappable-components.md b/docs/frontend-system/building-plugins/06-swappable-components.md index 4c03954bb2..c218c65e56 100644 --- a/docs/frontend-system/building-plugins/06-swappable-components.md +++ b/docs/frontend-system/building-plugins/06-swappable-components.md @@ -22,12 +22,14 @@ export const ExampleSwappableComponent = createSwappableComponent({ // This is a loader for loading the default implementation of the component when there's no overriden // implementation created with `SwappableComponentBlueprint`. - // It can be sync like below, but is can also be async like `loader: () => import('./DefaultImplementation').then(m => m.default)`. + // It can be sync like below, but is can also be async like `loader: () => import('./DefaultImplementation').then(m => m.DefaultImplementation)`. loader: () => (props: { name: string }) =>
Your name is {props.name}
, // This is an optional function that can be used to transform the props of the external component. // If it's not provided, the props of the external component will be passed through unchanged. + // Typically this wouldn't be used and you don't need to define this initially, + // this is a nice API to evolve the Swappable Component internal API without breaking usages of it in existing plugins. transformProps: (props: { lastName: string; firstName: string }) => ({ name: `${props.firstName} ${props.lastName}`, }), @@ -47,34 +49,63 @@ import { ExampleSwappableComponent } from '@internal/plugin-example-react'; ## Overriding a Swappable Component In order to override a Swappable Component, you need to create a `SwappableComponentBlueprint` and install it with the `app` plugin. +There's two different ways to add extensions to the `app` plugin, both are documented below in an example of overriding the `Progress` Swappable Component. ```tsx title="in packages/app/src/App.tsx" -import { ExampleSwappableComponent } from '@internal/plugin-example-react'; - -... - +import { + Progress, + SwappableComponentBlueprint, + createFrontendModule, +} from '@backstage/frontend-plugin-api'; +import { MyCustomProgress } from './CustomProgress'; +import { createApp } from '@backstage/frontend-defaults'; +import appPlugin from '@backstage/plugin-app'; const app = createApp({ features: [ - // Must be installed inside the App Plugin. + // Override an existing extension by ID provided by the app plugin: appPlugin.withOverrides({ extensions: [ - // Create a binding between the original c - SwappableComponentBlueprint.make({ - params: defineParams => defineParams({ - component: ExampleSwappableComponent, - loader: () => import('./ExampleComponent').then(m => m.SwappableComponent), - - // or sync: - // loader: () => MyNewImplementation - }) - }) - ] + appPlugin + .getExtension('component:app/core.components.progress') + .override({ + params: defineParams => + defineParams({ + component: Progress, + loader: () => MyCustomProgress, + }), + }), + ], }), - ... - ] -}) - + // OR: Add another extension but with the same component ID: + appPlugin.withOverrides({ + extensions: [ + SwappableComponentBlueprint.make({ + name: 'core.components.progress', + params: defineParams => + defineParams({ + component: Progress, + loader: () => MyCustomProgress, + }), + }), + ], + }), + // OR: Use a module for the app plugin: + createFrontendModule({ + pluginId: 'app', + extensions: [ + SwappableComponentBlueprint.make({ + name: 'core.components.progress', + params: defineParams => + defineParams({ + component: Progress, + loader: () => MyCustomProgress, + }), + }), + ], + }), + ], +}); ``` ## Default Swappable Components