Merge pull request #26044 from backstage/rugvip/the-rest

docs/frontend-system: update remaining documentation section for 1.30 release
This commit is contained in:
Patrik Oldsberg
2024-08-16 11:57:29 +02:00
committed by GitHub
12 changed files with 259 additions and 285 deletions
@@ -78,7 +78,7 @@ It is possible to enable, disable and configure extensions individually in the `
### Customize or override built-in extensions
Previously you would customize the application route, components, apis, sidebar, etc. through the code in `App.tsx`. Now we want you to write less code and install more extensions to customize your Backstage instance. See [here](../building-plugins/03-extension-types.md) which types of extensions are available for you to customize your application.
Previously you would customize the application routes, components, apis, sidebar, etc. through the code in `App.tsx`. Now we want to allow the same thing to be achieved while writing less code and instead installing more extensions to customize your Backstage instance. See the [extension blueprints](../building-plugins/03-common-extension-blueprints.md) section for a list of common extension kinds that are available for you to customize and extend your application.
## Use code to customize the app at a more granular level
@@ -95,7 +95,7 @@ At this point the contents of your app should be past the initial migration stag
## Migrating `createApp` Options
Many of the `createApp` options have been migrated to use extensions instead. Each will have their own [extension creator](../architecture/20-extensions.md#extension-creators) that you use to create a custom extension. To add these standalone extensions to the app they need to be passed to `createExtensionOverrides`, which bundles them into a _feature_ that you can install in the app. See the [standalone extensions](../architecture/25-extension-overrides.md#create-standalone-extensions) section for more information.
Many of the `createApp` options have been migrated to use extensions instead. Each will have their own [extension blueprint](../architecture/23-extension-blueprints.md) that you use to create a custom extension. To add these standalone extensions to the app they need to be passed to `createExtensionOverrides`, which bundles them into a _feature_ that you can install in the app. See the [standalone extensions](../architecture/25-extension-overrides.md#creating-a-standalone-extension-bundle) section for more information.
For example, assuming you have a `lightTheme` extension that you want to add to your app, you can use the following:
@@ -115,7 +115,7 @@ You can then also add any additional extensions that you may need to create as p
### `apis`
[Utility API](../utility-apis/01-index.md) factories are now installed as extensions instead. Pass the existing factory to `createApiExtension` and install it in the app. For more information, see the section on [configuring Utility APIs](../utility-apis/04-configuring.md).
[Utility API](../utility-apis/01-index.md) factories are now installed as extensions instead. Pass the existing factory to `ApiBlueprint` and install it in the app. For more information, see the section on [configuring Utility APIs](../utility-apis/04-configuring.md).
For example, the following `apis` configuration:
@@ -134,12 +134,15 @@ const app = createApp({
Can be converted to the following extension:
```ts
const scmIntegrationsApi = createApiExtension({
factory: createApiFactory({
api: scmIntegrationsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
const scmIntegrationsApi = ApiBlueprint.make({
name: 'scm-integrations',
params: {
factory: createApiFactory({
api: scmIntegrationsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
},
});
```
@@ -221,7 +224,7 @@ Many app components are now installed as extensions instead using `createCompone
The `Router` component is now a built-in extension that you can [override](../architecture/25-extension-overrides.md) using `createRouterExtension`.
The Sign-in page is now installed as an extension using the `createSignInPageExtension` instead.
The Sign-in page is now installed as an extension, created using the `SignInPageBlueprint` instead.
For example, the following sign-in page configuration:
@@ -246,25 +249,27 @@ const app = createApp({
Can be converted to the following extension:
```tsx
const signInPage = createSignInPageExtension({
loader: async () => props =>
(
<SignInPage
{...props}
provider={{
id: 'github-auth-provider',
title: 'GitHub',
message: 'Sign in using GitHub',
apiRef: githubAuthApiRef,
}}
/>
),
const signInPage = SignInPageBlueprint.make({
params: {
loader: async () => props =>
(
<SignInPage
{...props}
provider={{
id: 'github-auth-provider',
title: 'GitHub',
message: 'Sign in using GitHub',
apiRef: githubAuthApiRef,
}}
/>
),
},
});
```
### `themes`
Themes are now installed as extensions, using `createThemeExtension`.
Themes are now installed as extensions, created using `ThemeBlueprint`.
For example, the following theme configuration:
@@ -287,14 +292,19 @@ const app = createApp({
Can be converted to the following extension:
```tsx
const lightTheme = createThemeExtension({
id: 'light',
title: 'Light Theme',
variant: 'light',
icon: <LightIcon />,
Provider: ({ children }) => (
<UnifiedThemeProvider theme={builtinThemes.light} children={children} />
),
const lightTheme = ThemeBlueprint.make({
name: 'light',
params: {
theme: {
id: 'light',
title: 'Light Theme',
variant: 'light',
icon: <LightIcon />,
Provider: ({ children }) => (
<UnifiedThemeProvider theme={builtinThemes.light} children={children} />
),
},
},
});
```
@@ -360,7 +370,7 @@ const app = createApp({
### `__experimentalTranslations`
Translations are now installed as extensions, using `createTranslationExtension`.
Translations are now installed as extensions, created using `TranslationBlueprint`.
For example, the following translations configuration:
@@ -383,11 +393,14 @@ createApp({
Can be converted to the following extension:
```tsx
createTranslationExtension({
resource: createTranslationMessages({
ref: catalogTranslationRef,
catalog_page_create_button_title: 'Create Software',
}),
TranslationBlueprint.make({
name: 'catalog-overrides',
params: {
resource: createTranslationMessages({
ref: catalogTranslationRef,
catalog_page_create_button_title: 'Create Software',
}),
},
});
```
@@ -489,17 +502,15 @@ const nav = createExtension({
namespace: 'app',
name: 'nav',
attachTo: { id: 'app/layout', input: 'nav' },
output: {
element: coreExtensionData.reactElement,
},
output: [coreExtensionData.reactElement],
factory({ inputs }) {
return {
element: (
return [
coreExtensionData.reactElement(
<Sidebar>
{/* Sidebar contents from packages/app/src/components/Root.tsx go here */}
</Sidebar>
</Sidebar>,
),
};
];
},
});
```
@@ -548,7 +559,7 @@ export default app.createRoot(
);
```
Any app root wrapper needs to be migrated to be an extension, using `createAppRootWrapperExtension`. Note that if you have multiple wrappers they must be completely independent of each other, i.e. the order in which they the appear in the React tree should not matter. If that is not the case then you should group them into a single wrapper.
Any app root wrapper needs to be migrated to be an extension, created using `AppRootWrapperBlueprint`. Note that if you have multiple wrappers they must be completely independent of each other, i.e. the order in which they the appear in the React tree should not matter. If that is not the case then you should group them into a single wrapper.
Here is an example converting the `CustomAppBarrier` into extension:
@@ -558,11 +569,13 @@ createApp({
features: [
createExtensionOverrides({
extensions: [
createAppRootWrapperExtension({
name: 'CustomAppBarrier',
// Whenever your component uses legacy core packages, wrap it with "compatWrapper"
// e.g. props => compatWrapper(<CustomAppBarrier {...props} />)
Component: CustomAppBarrier,
AppRootWrapperBlueprint.make({
name: 'custom-app-barrier',
params: {
// Whenever your component uses legacy core packages, wrap it with "compatWrapper"
// e.g. props => compatWrapper(<CustomAppBarrier {...props} />)
Component: CustomAppBarrier,
},
}),
],
}),
@@ -49,7 +49,7 @@ The plugin ID should be a lowercase dash-separated string, while the plugin inst
The plugin that we created above is empty, and doesn't provide any actual functionality. To add functionality to a plugin you need to create and provide it with one or more [extensions](../architecture/20-extensions.md). Let's continue by adding a standalone page to our plugin, as well as a navigation item that allows users to navigate to the page.
To create a new extension you typically use pre-defined [extension creators](../architecture/20-extensions.md#extension-creators), provided either by the framework itself or by other plugins. In this case we'll need to use `createPageExtension` and `createNavItemExtension`, both from `@backstage/frontend-plugin-api`. We will also need to [create a route reference](../architecture/36-routes.md#creating-a-route-reference) to use as a reference for out page, allowing us to dynamically create URLs that link to our page.
To create a new extension you typically use pre-defined [extension blueprints](../architecture/23-extension-blueprints.md), provided either by the framework itself or by other plugins. In this case we'll use `PageBlueprint` and `NavItemBlueprint`, both from `@backstage/frontend-plugin-api`. We will also need to [create a route reference](../architecture/36-routes.md#creating-a-route-reference) to use as a reference for our page, allowing us to dynamically create URLs that link to our page.
```tsx title="in src/routes.ts"
import { createRouteRef } from '@backstage/frontend-plugin-api';
@@ -71,24 +71,29 @@ import { rootRouteRef } from './routes';
// Note that these extensions aren't exported, only the plugin itself is.
// You can export it locally for testing purposes, but don't export it from the plugin package.
const examplePage = createPageExtension({
routeRef: rootRouteRef,
const examplePage = PageBlueprint.make({
params: {
routeRef: rootRouteRef,
// This is the default path of this page, but integrators are free to override it
defaultPath: '/example',
// This is the default path of this page, but integrators are free to override it
defaultPath: '/example',
// Page extensions are always dynamically loaded using React.lazy().
// All of the functionality of this page is implemented in the
// ExamplePage component, which is a regular React component.
// highlight-next-line
loader: () => import('./components/ExamplePage').then(m => <m.ExamplePage />),
// Page extensions are always dynamically loaded using React.lazy().
// All of the functionality of this page is implemented in the
// ExamplePage component, which is a regular React component.
// highlight-next-line
loader: () =>
import('./components/ExamplePage').then(m => <m.ExamplePage />),
},
});
// This nav item is provided to the app.nav extension, and will by default be rendered as a sidebar item
const exampleNavItem = createNavItemExtension({
routeRef: rootRouteRef,
title: 'Example',
icon: ExampleIcon, // Custom SvgIcon, or one from the Material UI icon library
const exampleNavItem = NavItemBlueprint.make({
params: {
routeRef: rootRouteRef,
title: 'Example',
icon: ExampleIcon, // Custom SvgIcon, or one from the Material UI icon library
},
});
// The same plugin as above, now with the extensions added
@@ -157,12 +162,15 @@ import {
import { exampleApiRef, DefaultExampleApi } from './api';
// highlight-add-start
const exampleApi = createApiExtension({
factory: createApiFactory({
api: exampleApiRef,
deps: {},
factory: () => new DefaultExampleApi(),
}),
const exampleApi = ApiBlueprint.make({
name: 'example',
params: {
factory: createApiFactory({
api: exampleApiRef,
deps: {},
factory: () => new DefaultExampleApi(),
}),
},
});
// highlight-add-end
@@ -187,26 +195,28 @@ export const examplePlugin = createFrontendPlugin({
There are many different plugins that you can extend with additional functionality through extensions. One such plugin is [the catalog plugin](../../features/software-catalog/), one of the core features of Backstage. It lets you catalog the software in your organization, where each item in the catalog has its own page that can be populated with tools and information relating to that catalog entity. In this example we will explore how our plugin can provide such a tool to display on an entity page.
```tsx title="in src/plugin.ts - An example entity content extension"
import { createEntityContentExtension } from '@backstage/plugin-catalog-react';
import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha';
// Entity content extensions are similar to page extensions in that they are rendered at a route,
// although they also have a title to support in-line navigation between the different content.
// Just like a page extension the content is lazy loaded, and you can also provide a
// route reference if you want to be able to generate a URL that links to the content.
const exampleEntityContent = createEntityContentExtension({
defaultPath: 'example',
defaultTitle: 'Example',
loader: () =>
import('./components/ExampleEntityContent').then(m => (
<m.ExampleEntityContent />
)),
const exampleEntityContent = EntityContentBlueprint.make({
params: {
defaultPath: 'example',
defaultTitle: 'Example',
loader: () =>
import('./components/ExampleEntityContent').then(m => (
<m.ExampleEntityContent />
)),
},
});
export const examplePlugin = createFrontendPlugin({
id: 'example',
extensions: [
// highlight-add-next-line
exampleEntityContent
exampleEntityContent,
exampleApi,
examplePage,
exampleNavItem,
@@ -219,4 +229,4 @@ export const examplePlugin = createFrontendPlugin({
The `ExampleEntityContent` itself is again a regular React component where you can implement any functionality you want. To access the entity that the content is being rendered for, you can use the `useEntity` hook from `@backstage/plugin-catalog-react`. You can see a full list of APIs provided by the catalog React library in [the API reference](../../reference/plugin-catalog-react.md).
For a more complete list of the different types of extensions that you can create for your plugin, see the [extension types](./03-extension-types.md) section.
For a more complete list of the different kinds of extensions that you can create for your plugin, see the [extension blueprints](./03-common-extension-blueprints.md) section.
@@ -85,6 +85,8 @@ describe('Entity details component', () => {
});
```
This pattern also works for many other context providers. An important example is the `EntityProvider` from the `@backstage/plugin-catalog-react` package, which you can use to provide a mocked entity context to the component.
## Testing extensions
To facilitate testing of frontend extensions, the `@backstage/frontend-test-utils` package provides a tester class which starts up an entire frontend harness, complete with a number of default features. You can then provide overrides for extensions whose behavior you need to adjust for the test run.
@@ -93,7 +95,7 @@ A number of features (frontend extensions and overrides) are also accepted by th
### Single extension
In order to test an extension in isolation, you simply need to pass it into the tester factory, then call the render method on the returned instance:
In order to test an extension in isolation, you can use `createExtensionTester` to create a tester instance and access the element that the extension outputs. This element can then be rendered as usual with `renderInTestApp`:
```tsx
import { screen } from '@testing-library/react';
@@ -101,90 +103,49 @@ import { createExtensionTester } from '@backstage/frontend-test-utils';
import { indexPageExtension } from './plugin';
describe('Index page', () => {
it('should render a the index page', () => {
createExtensionTester(indexPageExtension).render();
it('should render a the index page', async () => {
await renderInTestApp(
createExtensionTester(indexPageExtension).reactElement(),
);
expect(screen.getByText('Index Page')).toBeInTheDocument();
});
});
```
### Extension preset
This pattern also allows you to wrap the extension with context providers, such as the `TestApiProvider` that was introduced [above](#testing-react-components).
There are some extensions that rely on other extensions existence, such as a page that links to another page. In that case, you can add more than one extension to the preset of features you want to render in the test, as shown below:
Note that the `.reactElement()` method will look for the `coreExtensionData.reactElement` data in the extension outputs. If that doesn't exist and the extension outputs something else that you want to test, you can access the output data using the `.get(dataRef)` method instead.
### Multiple extensions
In some cases you might need to test multiple extensions together, in particular when testing inputs. In this case, you can add more extensions to the tester instance using the `.add(...)` method. It also accepts an optional options object as the second argument, which you can use to provide configuration for the extension instance.
```tsx
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createExtensionTester } from '@backstage/frontend-test-utils';
import { indexPageExtension, detailsPageExtension } from './plugin';
import { indexPageExtension, indexPageHeader } from './plugin';
describe('Index page', async () => {
it('should link to the details page', () => {
createExtensionTester(indexPageExtension)
// Adding more extensions to the preset being tested
.add(detailsPageExtension)
.render();
it('should link to the index page with header', async () => {
const tester = createExtensionTester(indexPageExtension)
// Adding the header to be rendered on the index page
.add(indexPageHeader);
await expect(screen.findByText('Index Page')).toBeInTheDocument();
await renderInTestApp(tester.reactElement());
await userEvent.click(screen.getByRole('link', { name: 'See details' }));
await expect(screen.findByText('Index page')).toBeInTheDocument();
await expect(screen.findByText('Index page header')).toBeInTheDocument();
await expect(
screen.findByText('Details Page'),
).resolves.toBeInTheDocument();
expect(
tester.query(indexPageHeader).get(headerDataRef),
).toMatchObject(/* ... */);
});
});
```
### Mocking apis
If your extensions requires implementation of APIs that aren't wired up by default, you'll have to add overrides to the preset of features being tested:
```tsx
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createApiFactory } from '@backestage/core-plugin-api';
import {
createExtensionOverrides,
configApiRef,
analyticsApiRef,
} from '@backstage/frontend-plugin-api';
import {
createExtensionTester,
MockConfigApi,
MockAnalyticsApi,
} from '@backstage/frontend-test-utils';
import { indexPageExtension } from './plugin';
describe('Index page', () => {
it('should capture click events in analytics', async () => {
// Mocking the analytics api implementation
const analyticsApiMock = new MockAnalyticsApi();
const analyticsApiOverride = createApiExtension({
factory: createApiFactory({
api: analyticsApiRef,
factory: () => analyticsApiMock,
}),
});
createExtensionTester(indexPageExtension)
// Overriding the analytics api extension
.add(analyticsApiOverride)
.render();
await userEvent.click(
await screen.findByRole('link', { name: 'See details' }),
);
expect(analyticsApiMock.getEvents()[0]).toMatchObject({
action: 'click',
subject: 'See details',
});
});
});
```
When testing multiple extensions you may sometimes want to access the output of other extensions than the main test subject. You can use the `.query(ext)` method to query a different extension that has been added to the tester, by passing the extension used with the `createExtensionTester(...).add(ext)`
### Setting configuration
@@ -198,36 +159,26 @@ import { indexPageExtension, detailsPageExtension } from './plugin';
describe('Index page', () => {
it('should accepts a custom title via config', async () => {
createExtensionTester(indexPageExtension, {
// Configuration specific of index page
config: { title: 'Custom index' },
})
.add(detailsExtensionPage, {
// Configuration specific of details page
config: { title: 'Custom details' },
})
.render({
// Configuration specific of the instance
config: {
app: {
title: 'Custom app',
},
const tester = createExtensionTester(indexPageExtension, {
// Extension configuration for the index page
config: { title: 'Custom page' },
}).add(indexPageHeader, {
// Extension configuration for the index page header
config: { title: 'Custom page header' },
});
await renderInTestApp(tester.reactElement(), {
// Global configuration for the app
config: {
app: {
title: 'Custom app',
},
});
},
});
await expect(
screen.findByRole('heading', { name: 'Custom app' }),
).resolves.toBeInTheDocument();
await expect(
screen.findByRole('heading', { name: 'Custom index' }),
).resolves.toBeInTheDocument();
await userEvent.click(screen.getByRole('link', { name: 'See details' }));
await expect(
screen.findByText('Custom details'),
).resolves.toBeInTheDocument();
await expect(screen.findByText('Custom app')).toBeInTheDocument();
await expect(screen.findByText('Custom page')).toBeInTheDocument();
await expect(screen.findByText('Custom page header')).toBeInTheDocument();
});
});
```
@@ -1,40 +1,40 @@
---
id: extension-types
title: Frontend System Extension Types
sidebar_label: Extension Types
id: common-extension-blueprints
title: Common Extension Blueprints
sidebar_label: Common Extension Blueprints
# prettier-ignore
description: Extension types provided by the frontend system and core features
description: Extension blueprints provided by the frontend system and core features
---
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
This section covers many of the [extension types](../architecture/20-extensions.md#extension-creators) available at your disposal when building Backstage frontend plugins.
This section covers many of the [extension blueprints](../architecture/23-extension-blueprints.md) available at your disposal when building Backstage frontend plugins.
## Built-in extension types
## Built-in extension blueprints
These are the extension types provided by the Backstage frontend framework itself.
These are the [extension blueprints](../architecture/23-extension-blueprints.md) provided by the Backstage frontend framework itself.
### Api - [Reference](../../reference/frontend-plugin-api.createapiextension.md)
### Api - [Reference](../../reference/frontend-plugin-api.apiblueprint.md)
An API extension is used to add or override [Utility API factories](../utility-apis/01-index.md) in the app. They are commonly used by plugins for both internal and shared APIs. There are also many built-in Api extensions provided by the framework that you are able to override.
### Component - [Reference](../../reference/frontend-plugin-api.createcomponentextension.md)
Components extensions are used to override the component associated with a component reference throughout the app.
Components extensions are used to override the component associated with a component reference throughout the app. This uses an extension creator function rather than a blueprint, but will likely be migrated to a blueprint in the future.
### NavItem - [Reference](../../reference/frontend-plugin-api.createnavitemextension.md)
### NavItem - [Reference](../../reference/frontend-plugin-api.navitemblueprint.md)
Navigation item extensions are used to provide menu items that link to different parts of the app. By default nav items are attached to the app nav extension, which by default is rendered as the left sidebar in the app.
### Page - [Reference](../../reference/frontend-plugin-api.createpageextension.md)
### Page - [Reference](../../reference/frontend-plugin-api.pageblueprint.md)
Page extensions provide content for a particular route in the app. By default pages are attached to the app routes extensions, which renders the root routes.
### SignInPage - [Reference](../../reference/frontend-plugin-api.createsigninpageextension.md)
### SignInPage - [Reference](../../reference/frontend-plugin-api.signinpageblueprint.md)
Sign-in page extension have a single purpose - to implement a custom sign-in page. They are always attached to the app root extension and are rendered before the rest of the app until the user is signed in.
### Theme - [Reference](../../reference/frontend-plugin-api.createthemeextension.md)
### Theme - [Reference](../../reference/frontend-plugin-api.themeblueprint.md)
Theme extensions provide custom themes for the app. They are always attached to the app extension and you can have any number of themes extensions installed in an app at once, letting the user choose which theme to use.
@@ -42,22 +42,22 @@ Theme extensions provide custom themes for the app. They are always attached to
Icon bundle extensions provide the ability to replace or provide new icons to the app. You can use the above blueprint to make new extension instances which can be installed into the app.
### Translation - [Reference](../../reference/frontend-plugin-api.createtranslationextension.md)
### Translation - [Reference](../../reference/frontend-plugin-api.translationblueprint.md)
Translation extension provide custom translation messages for the app. They can be used both to override the default english messages to custom ones, as well as provide translations for additional languages.
## Core feature extension types
## Core feature extension blueprints
These are the extension types provided by the Backstage core feature plugins.
These are the [extension blueprints](../architecture/23-extension-blueprints.md) provided by the Backstage core feature plugins.
### EntityCard - [Reference](https://github.com/backstage/backstage/blob/master/plugins/catalog-react/api-report-alpha.md)
Creates entity cards to be displayed on the entity pages of the catalog plugin.
Creates entity cards to be displayed on the entity pages of the catalog plugin. Exported as `EntityCardBlueprint`.
### EntityContent - [Reference](https://github.com/backstage/backstage/blob/master/plugins/catalog-react/api-report-alpha.md)
Creates entity content to be displayed on the entity pages of the catalog plugin.
Creates entity content to be displayed on the entity pages of the catalog plugin. Exported as `EntityContentBlueprint`.
### SearchResultListItem - [Reference](https://github.com/backstage/backstage/blob/master/plugins/search-react/api-report-alpha.md)
Creates search result list items for different types of search results, to be displayed in search result lists.
Creates search result list items for different types of search results, to be displayed in search result lists. Exported as `SearchResultListItemBlueprint`.
@@ -24,18 +24,15 @@ The `reactElement` data reference can be used for defining the extension input/o
```tsx
import {
createExtension,
coreExtensionData,
createExtensionInput,
createPageExtension,
} from '@backstage/frontend-plugin-api';
const homePage = createPageExtension({
defaultPath: '/home',
routeRef: rootRouteRef,
inputs: {
props: createExtensionInput({
children: coreExtensionData.reactElement.optional(),
}),
const examplePage = createExtension({
name: 'example',
output: [coreExtensionData.reactElement],
factor() {
return [coreExtensionData.reactElement(<h1>Example</h1>)];
},
});
```
@@ -8,7 +8,7 @@ description: How to migrate an existing frontend plugin to the new frontend syst
This guide allows you to migrate a frontend plugin and its own components, routes, apis to the new frontend system.
The main concept is that routes, components, apis are now extensions. You can use the appropriate extension creators to migrate all of them to extensions.
The main concept is that routes, components, apis are now extensions. You can use the appropriate [extension blueprints](../architecture/23-extension-blueprints.md) to migrate all of them to extensions.
## Migrating the plugin
@@ -77,7 +77,7 @@ The code above binds all the extensions to the plugin. _Important_: Make sure to
## Migrating Pages
Pages that were previously created using the `createRoutableExtension` extension function can be migrated to the new Frontend System using the `createPageExtension` extension creator, exported by `@backstage/frontend-plugin-api`.
Pages that were previously created using the `createRoutableExtension` extension function can be migrated to the new Frontend System using the `PageBlueprint` [extension blueprint](../architecture/23-extension-blueprints.md), exported by `@backstage/frontend-plugin-api`.
For example, given the following page:
@@ -94,28 +94,30 @@ export const FooPage = fooPlugin.provide(
it can be migrated as the following:
```tsx
import { createPageExtension } from '@backstage/frontend-plugin-api';
import { PageBlueprint } from '@backstage/frontend-plugin-api';
import {
compatWrapper,
convertLegacyRouteRef,
} from '@backstage/core-compat-api';
const fooPage = createPageExtension({
defaultPath: '/foo',
// you can reuse the existing routeRef
// by wrapping into the convertLegacyRouteRef.
routeRef: convertLegacyRouteRef(rootRouteRef),
// these inputs usually match the props required by the component.
loader: ({ inputs }) =>
import('./components/').then(m =>
// The compatWrapper utility allows you to use the existing
// legacy frontend utilities used internally by the components.
compatWrapper(<m.FooPage />),
),
const fooPage = PageBlueprint.make({
params: {
defaultPath: '/foo',
// you can reuse the existing routeRef
// by wrapping into the convertLegacyRouteRef.
routeRef: convertLegacyRouteRef(rootRouteRef),
// these inputs usually match the props required by the component.
loader: ({ inputs }) =>
import('./components/').then(m =>
// The compatWrapper utility allows you to use the existing
// legacy frontend utilities used internally by the components.
compatWrapper(<m.FooPage />),
),
},
});
```
then add the `fooPage` extension to the plugin:
Then add the `fooPage` extension to the plugin:
```ts title="my-plugin/src/alpha.ts"
import { createFrontendPlugin } from '@backstage/frontend-plugin-api';
@@ -133,7 +135,7 @@ then add the `fooPage` extension to the plugin:
## Migrating Components
The equivalent utility to replace components created with `createComponentExtension` is `createExtension` from `@backstage/frontend-plugin-api`. However, we recommend searching for a more appropriate extension creator first.
The equivalent utility to replace components created with `createComponentExtension` depends on the context within which the component is used, typically indicated by the naming pattern of the export. Many of these can be migrated to one of the [existing blueprints](03-common-extension-blueprints.md), but in rare cases it may be necessary to use [`createExtension`](../architecture/20-extensions.md#creating-an-extension) directly.
## Migrating APIs
@@ -182,7 +184,7 @@ const exampleWorkApi = createApiFactory({
The major changes we'll make are
- Change the old `@backstage/core-plugin-api` imports to the new `@backstage/frontend-plugin-api` package as per the top section of this guide
- Wrap the existing API factory in a `createApiExtension`
- Wrap the existing API factory in a `ApiBlueprint`
The end result, after simplifying imports and cleaning up a bit, might look like this:
@@ -190,17 +192,19 @@ The end result, after simplifying imports and cleaning up a bit, might look like
import {
storageApiRef,
createApiFactory,
createApiExtension,
ApiBlueprint,
} from '@backstage/frontend-plugin-api';
import { workApiRef } from '@internal/plugin-example-react';
import { WorkImpl } from './WorkImpl';
const exampleWorkApi = createApiExtension({
factory: createApiFactory({
api: workApiRef,
deps: { storageApi: storageApiRef },
factory: ({ storageApi }) => new WorkImpl({ storageApi }),
}),
const exampleWorkApi = ApiBlueprint.make({
params: {
factory: createApiFactory({
api: workApiRef,
deps: { storageApi: storageApiRef },
factory: ({ storageApi }) => new WorkImpl({ storageApi }),
}),
},
});
```
@@ -45,7 +45,7 @@ The plugin itself now wants to provide this API and its default implementation,
```tsx title="in @internal/plugin-example"
import {
createApiExtension,
ApiBlueprint,
createApiFactory,
createFrontendPlugin,
storageApiRef,
@@ -62,14 +62,17 @@ class WorkImpl implements WorkApi {
}
}
const exampleWorkApi = createApiExtension({
factory: createApiFactory({
api: workApiRef,
deps: { storageApi: storageApiRef },
factory: ({ storageApi }) => {
return new WorkImpl({ storageApi });
},
}),
const workApi = ApiBlueprint.make({
name: 'work',
params: {
factory: createApiFactory({
api: workApiRef,
deps: { storageApi: storageApiRef },
factory: ({ storageApi }) => {
return new WorkImpl({ storageApi });
},
}),
},
});
/**
@@ -86,43 +89,37 @@ For illustration we make a skeleton implementation class and the API extension a
The code also illustrates how the API factory declares a dependency on another utility API - the core storage API in this case. An instance of that utility API is then provided to the factory function.
The resulting extension ID of the work API will be the kind `api:` followed by the plugin ID as the namespace, in this case ending up as `api:plugin.example.work`. Check out [the naming patterns doc](../architecture/50-naming-patterns.md) for more information on how this works. You can now use this ID to refer to the API in app-config and elsewhere.
The extension ID of the work API will be the kind `api:` followed by the plugin ID as the namespace, a `/` separator, and lastly the name we used of the extension. In this case we end up with `api:example/work`. Check out [the naming patterns doc](../architecture/50-naming-patterns.md) for more information on how this works. You can now use this ID to refer to the API in app-config and elsewhere. In case there is a single API that is a central to the functionality of the plugin, most typically an API client, you can choose to omit the name of the extension so that you end up with just `api:<pluginId>`.
## Adding configurability
Here we will describe how to amend a utility API with the capability of having extension config, which is driven by [your app-config](../../conf/writing.md). You do this by giving an extension config schema to your API extension factory function. Let's make the required additions to our original work example API.
Here we will describe how to amend a utility API with the capability of having extension config, which is driven by [your app-config](../../conf/writing.md). You do this by giving an extension config schema to your API extension factory function. Let's refactory the example above to also accept configuration, which will require us to use the [override method of the blueprint](../architecture/23-extension-blueprints.md#creating-an-extension-from-a-blueprint-with-overrides).
```tsx title="in @internal/plugin-example"
/* highlight-add-next-line */
import { createSchemaFromZod } from '@backstage/frontend-plugin-api';
const exampleWorkApi = createApiExtension({
/* highlight-add-start */
api: workApiRef,
configSchema: createSchemaFromZod(z =>
z.object({
goSlow: z.boolean().default(false),
}),
),
/* highlight-add-end */
/* highlight-remove-next-line */
factory: createApiFactory({
/* highlight-add-next-line */
factory: ({ config }) => createApiFactory({
api: workApiRef,
deps: { storageApi: storageApiRef },
factory: ({ storageApi }) => {
/* highlight-add-start */
if (config.goSlow) {
/* ... */
}
/* highlight-add-end */
const exampleWorkApi = ApiBlueprint.makeWithOverrides({
config: {
schema: {
goSlow: z => z.boolean().default(false),
},
}),
},
factory(originalFactory, { config }) {
return originalFactory({
factory: createApiFactory({
api: workApiRef,
deps: { storageApi: storageApiRef },
factory: ({ storageApi }) => {
return new WorkImpl({
storageApi,
goSlow: config.goSlow,
});
},
}),
});
},
});
```
We wanted users to be able to set a `goSlow` extension config parameter for our API instances. So we passed in a `configSchema` to `createApiExtension` which matches that interface. This example builds it using [the zod library](https://zod.dev/). The actual extension config values will then be passed in a type safe manner in to the `factory` which is now a callback, wherein we can do what we wish with them. When changing to the callback form, we also had to add a top level `api: workApiRef` under `createApiExtension`.
We wanted users to be able to set a `goSlow` extension config parameter for our API instances, which we declared in our new configuration schema. The actual extension config values will then be passed in a type safe manner in to the blueprint `factory`, wherein we can use them to create our API factory and pass as our blueprint parameters.
Note that the expression "extension config" as used here, is _not_ the same thing as the `configApi` which gives you access to the full app-config. The extension config discussed here is instead the particular configuration settings given to your utility API instance. This is discussed more [in the Configuring section](./04-configuring.md).
@@ -130,11 +127,11 @@ Note also that the extension config schema contained a default value fo the `goS
## Adding inputs
Inputs are added to Utility APIs in the same way as other extension types:
Inputs are added to Utility APIs in the same way as other extension blueprints:
- Declaring a set of `inputs` on your extension
- If needed, create custom extension data types to be used in those inputs
- If needed, export an extension creator function for creating that particular attachment type
- Use `.makeWithOverrides` and declare a set of `inputs` for your extension.
- If needed, create custom extension data types to be used in those inputs.
- If needed, create and export an [extension blueprint](../architecture/23-extension-blueprints.md#creating-an-extension-blueprint) for creating that particular attachment type.
This is a power use case and not very commonly used.
@@ -46,23 +46,25 @@ Your utility APIs can depend on other utility APIs in their factories. You do th
```tsx
import {
configApiRef,
createApiExtension,
ApiBlueprint,
createApiFactory,
discoveryApiRef,
} from '@backstage/frontend-plugin-api';
import { MyApiImpl } from './MyApiImpl';
const myApi = createApiExtension({
factory: createApiFactory({
api: myApiRef,
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
},
factory: ({ configApi, discoveryApi }) => {
return new MyApiImpl({ configApi, discoveryApi });
},
}),
const myApi = ApiBlueprint.make({
params: {
factory: createApiFactory({
api: myApiRef,
deps: {
configApi: configApiRef,
discoveryApi: discoveryApiRef,
},
factory: ({ configApi, discoveryApi }) => {
return new MyApiImpl({ configApi, discoveryApi });
},
}),
},
});
```
@@ -49,13 +49,13 @@ class CustomWorkImpl implements WorkApi {
const myOverrides = createExtensionOverrides({
extensions: [
createApiExtension({
api: workApiRef,
factory: () =>
createApiFactory({
ApiBlueprint.make({
params: {
factory: createApiFactory({
api: workApiRef,
factory: () => new CustomWorkImpl(),
}),
},
}),
],
});
+1 -1
View File
@@ -469,7 +469,7 @@
"items": [
"frontend-system/building-plugins/index",
"frontend-system/building-plugins/testing",
"frontend-system/building-plugins/extension-types",
"frontend-system/building-plugins/common-extension-blueprints",
"frontend-system/building-plugins/built-in-data-refs",
"frontend-system/building-plugins/migrating"
]
+1 -1
View File
@@ -126,7 +126,7 @@ The Catalog graphics plugin provides extensions for each of its features, see be
#### Catalog Entity Relations Graph Card
An [entity card](https://backstage.io/docs/frontend-system/building-plugins/extension-types#entitycard---reference) extension that renders the relation graph for an entity on the Catalog entity page and has an action that redirects users to the more advanced [relations graph page](#catalog-entity-relations-graph-page).
An [entity card](https://backstage.io/docs/frontend-system/building-plugins/extension-blueprints#entitycard---reference) extension that renders the relation graph for an entity on the Catalog entity page and has an action that redirects users to the more advanced [relations graph page](#catalog-entity-relations-graph-page).
| kind | namespace | name | id | Default enabled |
| ----------- | ------------- | ---------------- | ------------------------------------- | --------------- |