refactor: restructure the application migration guide
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
@@ -9,7 +9,44 @@ description: How to migrate existing apps to the new frontend system
|
||||
|
||||
This section describes how to migrate an existing Backstage app package to use the new frontend system. The app package is typically found at `packages/app` in your project and is responsible for wiring together the Backstage frontend application.
|
||||
|
||||
## Switching out `createApp`
|
||||
> **Who is this for?**
|
||||
> This guide is intended for maintainers of Backstage app packages (`packages/app`) who want to upgrade from the legacy frontend system to the new extension-based architecture.
|
||||
|
||||
> **Prerequisites:**
|
||||
>
|
||||
> - Familiarity with your app’s current structure and configuration
|
||||
> - Yarn workspaces and monorepo setup
|
||||
> - Access to run `yarn` commands and update dependencies
|
||||
|
||||
## Migration
|
||||
|
||||
We recommend a **two-phase migration process** to ensure a smooth and manageable transition:
|
||||
|
||||
- **Phase 1: Minimal Changes for Hybrid Configuration**
|
||||
In this phase, you make the smallest set of changes necessary to enable your app to run in a hybrid mode. This allows you to start using the new frontend system while still relying on compatibility helpers and legacy code. The goal is to unblock your migration quickly, so you can benefit from the new system without a full rewrite.
|
||||
|
||||
- **Phase 2: Complete Transition to the New Frontend System**
|
||||
After your app is running in hybrid mode, you can gradually refactor your codebase to remove legacy code and compatibility helpers. This phase focuses on fully adopting the new frontend architecture, ensuring your codebase is clean, maintainable, and takes full advantage of the new features.
|
||||
|
||||
> **Note:**
|
||||
> Staying in hybrid mode for too long is not recommended. Support for the legacy version and compatibility helpers will be dropped in the future, so you should plan to fully migrate your codebase as soon as possible.
|
||||
|
||||
## Checklist
|
||||
|
||||
Before you begin, review this checklist to track your progress:
|
||||
|
||||
- [ ] Complete minimal changes for hybrid configuration (Phase 1)
|
||||
- [ ] App starts and works in hybrid mode
|
||||
- [ ] Gradually migrate and remove legacy code and helpers (Phase 2)
|
||||
- [ ] App runs fully on the new frontend system
|
||||
|
||||
## Phase 1: Minimal Changes for Hybrid Configuration
|
||||
|
||||
There are 5 steps to minimally change your app to start experimenting with the new frontend system in a hybrid mode.
|
||||
|
||||
After completing these steps you should be able to start up the app and see that it still works.
|
||||
|
||||
### 1) Switching out `createApp`
|
||||
|
||||
To start we'll need to add the new `@backstage/frontend-defaults` package:
|
||||
|
||||
@@ -26,23 +63,132 @@ import { createApp } from '@backstage/app-defaults';
|
||||
import { createApp } from '@backstage/frontend-defaults';
|
||||
```
|
||||
|
||||
This immediate switch will lead to a lot of breakages that we need to fix.
|
||||
This immediate switch will lead to a lot of breakages that will be fixed in the upcoming steps.
|
||||
|
||||
Let's start by addressing the change to `app.createRoot(...)`, which no longer accepts any arguments. This represents a fundamental change that the new frontend system introduces. In the old system the app element tree that you passed to `app.createRoot(...)` was the primary way that you installed and configured plugins and features in your app. In the new system this is instead replaced by extensions that are wired together into an extension tree. Much more responsibility has now been shifted to plugins, for example you no longer have to manually provide the route path for each plugin page, but instead only configure it if you want to override the default. For more information on how the new system works, see the [architecture](../architecture/00-index.md) section.
|
||||
### 2) Converting the `createApp` options
|
||||
|
||||
Given that the app element tree is most of what builds up the app, it's likely also going to be the majority of the migration effort. In order to make the migration as smooth as possible we have provided a helper that lets you convert an existing app element tree into plugins that you can install in a new app. This in turn allows for a gradual migration of individual plugins, rather than needing to migrate the entire app structure at once.
|
||||
Most of the legacy options `createApp` options — with the exception of `bindRoutes` — can be converted into features that are compatible with the new frontend system. This is accomplished using the `convertLegacyAppOptions` helper, which allows you to continue using your existing configuration while gradually migrating to the new architecture.
|
||||
|
||||
The helper is called `convertLegacyAppRoot` and is exported from the `@backstage/core-compat-api` package. We will also be using the `convertLegacyAppOptions` helper that lets us re-use the existing app options, also exported from the same package. You will need to add it as a dependency to your app package:
|
||||
To do so, start by adding this dependency to your app package:
|
||||
|
||||
```bash
|
||||
yarn --cwd packages/app add @backstage/core-compat-api
|
||||
```
|
||||
|
||||
Once installed, import `convertLegacyAppRoot`. If your app currently looks like this:
|
||||
Open the file where your application was created. Currently, you should be doing something like this:
|
||||
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
const app = createApp({
|
||||
/* other options */
|
||||
apis,
|
||||
icons: {
|
||||
// Custom icon example
|
||||
alert: AlarmIcon,
|
||||
},
|
||||
featureFlags: [
|
||||
{
|
||||
name: 'scaffolder-next-preview',
|
||||
description: 'Preview the new Scaffolder Next',
|
||||
pluginId: '',
|
||||
},
|
||||
],
|
||||
components: {
|
||||
SignInPage: props => {
|
||||
return (
|
||||
<SignInPage
|
||||
{...props}
|
||||
providers={['guest', 'custom', ...providers]}
|
||||
title="Select a sign-in method"
|
||||
align="center"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Migrate it to the following:
|
||||
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
import { createApp } from '@backstage/frontend-defaults';
|
||||
import { convertLegacyAppOptions } from '@backstage/core-compat-api';
|
||||
|
||||
const convertedOptionsFeatures = convertLegacyAppOptions({
|
||||
/* legacy options such as apis, icons, plugins, components, themes and featureFlags */
|
||||
apis,
|
||||
icons: {
|
||||
// Custom icon example
|
||||
alert: AlarmIcon,
|
||||
},
|
||||
featureFlags: [
|
||||
{
|
||||
name: 'scaffolder-next-preview',
|
||||
description: 'Preview the new Scaffolder Next',
|
||||
pluginId: '',
|
||||
},
|
||||
],
|
||||
components: {
|
||||
SignInPage: props => {
|
||||
return (
|
||||
<SignInPage
|
||||
{...props}
|
||||
providers={['guest', 'custom', ...providers]}
|
||||
title="Select a sign-in method"
|
||||
align="center"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
}});
|
||||
|
||||
const app = createApp({
|
||||
features: [
|
||||
// ...
|
||||
...convertedOptionsFeatures,
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
If you were binding routes from a legacy `createApp`, you will need to use the `convertLegacyRouteRefs` and/or `convertLegacyRouteRef` to convert the routes to be compatible with the new system.
|
||||
|
||||
For example, if both the `catalogPlugin` and `scaffolderPlugin` are legacy plugins, you can bind their routes like this:
|
||||
|
||||
```ts
|
||||
import { createApp } from '@backstage/frontend-defaults';
|
||||
import {
|
||||
// ...
|
||||
convertLegacyRouteRefs,
|
||||
convertLegacyRouteRef,
|
||||
} from '@backstage/core-compat-api';
|
||||
|
||||
// Ommitting converted options changes
|
||||
//...
|
||||
|
||||
const app = createApp({
|
||||
features: [
|
||||
// ...
|
||||
...convertedOptionsFeatures,
|
||||
],
|
||||
// highlight-add-start
|
||||
bindRoutes({ bind }) {
|
||||
bind(convertLegacyRouteRefs(catalogPlugin.externalRoutes), {
|
||||
createComponent: convertLegacyRouteRef(scaffolderPlugin.routes.root),
|
||||
});
|
||||
},
|
||||
// highlight-add-end
|
||||
});
|
||||
```
|
||||
|
||||
### 3) Fixing the `app.createRoot` call
|
||||
|
||||
The `app.createRoot(...)` no longer accepts any arguments. This represents a fundamental change that the new frontend system introduces. In the old system the app element tree that you passed to `app.createRoot(...)` was the primary way that you installed and configured plugins and features in your app. In the new system this is instead replaced by extensions that are wired together into an extension tree. Much more responsibility has now been shifted to plugins, for example you no longer have to manually provide the route path for each plugin page, but instead only configure it if you want to override the default. For more information on how the new system works, see the [architecture](../architecture/00-index.md) section.
|
||||
|
||||
Given that the app element tree is most of what builds up the app, it's likely also going to be the majority of the migration effort. In order to make the migration as smooth as possible we have provided a helper that lets you convert an existing app element tree into plugins that you can install in a new app. This in turn allows for a gradual migration of individual plugins, rather than needing to migrate the entire app structure at once.
|
||||
|
||||
The helper is called `convertLegacyAppRoot` and is exported from the `@backstage/core-compat-api` package. Once installed, import `convertLegacyAppRoot`. If your app currently looks like this:
|
||||
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
const app = createApp({
|
||||
/* All legacy options except route bindings */
|
||||
});
|
||||
|
||||
export default app.createRoot(
|
||||
@@ -60,11 +206,11 @@ Migrate it to the following:
|
||||
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
import {
|
||||
// ...
|
||||
convertLegacyAppRoot,
|
||||
convertLegacyAppOptions,
|
||||
} from '@backstage/core-compat-api';
|
||||
|
||||
const legacyFeatures = convertLegacyAppRoot(
|
||||
const convertedRootFeatures = convertLegacyAppRoot(
|
||||
<>
|
||||
<AlertDisplay />
|
||||
<OAuthRequestDialog />
|
||||
@@ -74,12 +220,11 @@ const legacyFeatures = convertLegacyAppRoot(
|
||||
</>,
|
||||
);
|
||||
|
||||
const optionsModule = convertLegacyAppOptions({
|
||||
/* other options such as apis, plugins, components, and themes */
|
||||
});
|
||||
|
||||
const app = createApp({
|
||||
features: [optionsModule, ...legacyFeatures],
|
||||
features: [
|
||||
// ...
|
||||
...convertedRootFeatures,
|
||||
],
|
||||
});
|
||||
|
||||
export default app.createRoot();
|
||||
@@ -87,6 +232,8 @@ export default app.createRoot();
|
||||
|
||||
We've taken all the elements that were previously passed to `app.createRoot(...)`, and instead passed them to `convertLegacyAppRoot(...)`. We then pass the features returned by `convertLegacyAppRoot` and forward them to the `features` option of the new `createApp`.
|
||||
|
||||
### 4) Adjusting the app rendering
|
||||
|
||||
There is one more detail that we need to deal with before moving on. The `app.createRoot()` function now returns a React element rather than a component, so we need to update our app `index.tsx` as follows:
|
||||
|
||||
```tsx title="in packages/app/src/index.tsx"
|
||||
@@ -103,6 +250,8 @@ ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(app);
|
||||
```
|
||||
|
||||
### 5) Updating the app test files
|
||||
|
||||
You'll also need to make similar changes to your `App.test.tsx` file as well:
|
||||
|
||||
```tsx
|
||||
@@ -141,9 +290,13 @@ describe('App', () => {
|
||||
|
||||
Then you'll want to follow the section on [migrating `bindRoutes`](#bindroutes).
|
||||
|
||||
At this point the contents of your app should be past the initial migration stage, and we can move on to migrating any remaining options that you may have passed to `createApp`.
|
||||
## Phase 2: Complete Transition to the New Frontend System
|
||||
|
||||
## Migrating `createApp` Options
|
||||
If your app starts and works in hybrid mode, you’re ready to begin Phase 2. If not, review the error messages, check the [GitHub issues](https://github.com/backstage/backstage/issues), or ask for help in our [community Discord](https://discord.gg/backstage-687207715902193673).
|
||||
|
||||
At this point the contents of your app should be past the initial migration stage, let's continue by gradully get rid of the legacy code and helpers to fully use the new system.
|
||||
|
||||
### Migrating `createApp` options
|
||||
|
||||
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 `createFrontendModule`, which bundles them into a _feature_ that you can install in the app. See the [frontend module](../architecture/25-extension-overrides.md#creating-a-frontend-module) section for more information.
|
||||
|
||||
@@ -174,7 +327,7 @@ const app = createApp({
|
||||
|
||||
You can then also add any additional extensions that you may need to create as part of this migration to the `extensions` array as well.
|
||||
|
||||
### `apis`
|
||||
#### `apis`
|
||||
|
||||
[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).
|
||||
|
||||
@@ -210,7 +363,7 @@ const scmIntegrationsApi = ApiBlueprint.make({
|
||||
|
||||
You would then add `scmIntegrationsApi` as an `extension` like you did with `lightTheme` in the [Migrating `createApp` Options](#migrating-createapp-options) section.
|
||||
|
||||
### `plugins`
|
||||
#### `plugins`
|
||||
|
||||
Plugins are now passed through the `features` options instead.
|
||||
|
||||
@@ -247,7 +400,7 @@ app:
|
||||
packages: all # ✨
|
||||
```
|
||||
|
||||
### `featureFlags`
|
||||
#### `featureFlags`
|
||||
|
||||
Declaring features flags in the app is no longer supported, move these declarations to the appropriate plugins instead.
|
||||
|
||||
@@ -282,7 +435,7 @@ createFrontendPlugin({
|
||||
|
||||
This would get added to the `features` array as part of your `createApp` options.
|
||||
|
||||
### `components`
|
||||
#### `components`
|
||||
|
||||
Many app components are now installed as extensions instead using `createComponentExtension`. See the section on [configuring app components](./01-index.md#configure-your-app) for more information.
|
||||
|
||||
@@ -335,7 +488,7 @@ const signInPage = SignInPageBlueprint.make({
|
||||
|
||||
You would then add `signInPage` as an `extension` like you did with `lightTheme` in the [Migrating `createApp` Options](#migrating-createapp-options) section.
|
||||
|
||||
### `themes`
|
||||
#### `themes`
|
||||
|
||||
Themes are now installed as extensions, created using `ThemeBlueprint`.
|
||||
|
||||
@@ -381,7 +534,7 @@ const customLightThemeExtension = ThemeBlueprint.make({
|
||||
|
||||
You would then add `customLightThemeExtension` as an `extension` like you did with `lightTheme` in the [Migrating `createApp` Options](#migrating-createapp-options) section.
|
||||
|
||||
### `configLoader`
|
||||
#### `configLoader`
|
||||
|
||||
The config loader API has been slightly changed. Rather than returning a promise for an array of `AppConfig` objects, it should now return the `ConfigApi` directly.
|
||||
|
||||
@@ -399,7 +552,7 @@ const app = createApp({
|
||||
});
|
||||
```
|
||||
|
||||
### `icons`
|
||||
#### `icons`
|
||||
|
||||
Icons are now installed as extensions, using the `IconBundleBlueprint` to make new instances which can be added to the app.
|
||||
|
||||
@@ -425,7 +578,7 @@ const app = createApp({
|
||||
});
|
||||
```
|
||||
|
||||
### `bindRoutes`
|
||||
#### `bindRoutes`
|
||||
|
||||
Route bindings can still be done using this option, but you now also have the ability to bind routes using static configuration instead. See the section on [binding routes](../architecture/36-routes.md#binding-external-route-references) for more information.
|
||||
|
||||
@@ -444,7 +597,7 @@ const app = createApp({
|
||||
});
|
||||
```
|
||||
|
||||
### `__experimentalTranslations`
|
||||
#### `__experimentalTranslations`
|
||||
|
||||
Translations are now installed as extensions, created using `TranslationBlueprint`.
|
||||
|
||||
@@ -488,173 +641,91 @@ const catalogTranslations = TranslationBlueprint.make({
|
||||
|
||||
You would then add `catalogTranslations` as an `extension` like you did with `lightTheme` in the [Migrating `createApp` Options](#migrating-createapp-options) section.
|
||||
|
||||
## Gradual Migration
|
||||
### Migrating `createRoot` components
|
||||
|
||||
After updating all `createApp` options as well as using `convertLegacyAppRoot` to use your existing app structure, you should be able to start up the app and see that it still works. If that is not the case, make sure you read any error messages that you may see in the app as they can provide hints on what you need to fix. If you are still stuck, you can check if anyone else ran into the same issue in our [GitHub issues](https://github.com/backstage/backstage/issues), or ask for help in our [community Discord](https://discord.gg/backstage-687207715902193673).
|
||||
This will remove many extension overrides that `convertLegacyAppRoot` put in place, and switch over the shell of the app to the new system. This includes the root layout of the app along with the elements, router, and sidebar. The app will likely not look the same as before, and you'll need to refer to the [sidebar](#app-root-sidebar), [app root elements](#app-root-elements) and [app root wrappers](#app-root-wrappers) sections below for information on how to migrate those.
|
||||
|
||||
Assuming your app is now working, let's continue by migrating the rest of the app element tree to use the new system.
|
||||
Once that step is complete the work that remains is to migrate all of the [routes](#app-root-routes) and [entity pages](#catalog-entity-page) in the app, including any plugins that do not yet support the new system. For information on how to migrate your own internal plugins, refer to the [plugin migration guide](../building-plugins/05-migrating.md). For external plugins you will need to check the migration status of each plugin and potentially contribute to the effort.
|
||||
|
||||
First off we'll want to trim away any top-level elements in the app so that only the `routes` are left. For example, continuing where we left off with the following elements:
|
||||
Once these migrations are complete you should be left with an empty `convertLegacyAppRoot(...)` call that you can now remove, and your app should be fully migrated to the new system! 🎉
|
||||
|
||||
#### App Root Elements
|
||||
|
||||
App root elements are React elements that are rendered adjacent to your current `Root` component. For example, in this snippet `AlertDisplay`, `OAuthRequestDialog` and `VisitListener` are all app root elements:
|
||||
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
const legacyFeatures = convertLegacyAppRoot(
|
||||
const convertedRootFeatures = convertLegacyAppRoot(
|
||||
<>
|
||||
<AlertDisplay />
|
||||
{/* highlight-next-line */}
|
||||
<AlertDisplay transientTimeoutMs={2500} />
|
||||
{/* highlight-next-line */}
|
||||
<OAuthRequestDialog />
|
||||
<AppRouter>
|
||||
{/* highlight-next-line */}
|
||||
<VisitListener />
|
||||
<Root>{routes}</Root>
|
||||
</AppRouter>
|
||||
</>,
|
||||
);
|
||||
```
|
||||
|
||||
You can remove all surrounding elements and just keep the `routes`:
|
||||
The `AlertDisplay` and `OAuthRequestDialog` are already provided as built-in extensions, and so will `VisitListener`, so ou can remove all surrounding elements and just keep the `routes`:
|
||||
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
const legacyFeatures = convertLegacyAppRoot(routes);
|
||||
const convertedRootFeatures = convertLegacyAppRoot(routes);
|
||||
```
|
||||
|
||||
This will remove many extension overrides that `convertLegacyAppRoot` put in place, and switch over the shell of the app to the new system. This includes the root layout of the app along with the elements, router, and sidebar. The app will likely not look the same as before, and you'll need to refer to the [sidebar](#sidebar), [app root elements](#app-root-elements) and [app root wrappers](#app-root-wrappers) sections below for information on how to migrate those.
|
||||
But, if you have your own custom root elements you will need to migrate them to be extensions that you install in the app instead. Use `createAppRootElementExtension` to create said extension and then install it in the app.
|
||||
|
||||
Once that step is complete the work that remains is to migrate all of the [routes](#top-level-routes) and [entity pages](#entity-pages) in the app, including any plugins that do not yet support the new system. For information on how to migrate your own internal plugins, refer to the [plugin migration guide](../building-plugins/05-migrating.md). For external plugins you will need to check the migration status of each plugin and potentially contribute to the effort.
|
||||
Whether the element used to be rendered as a child of the `AppRouter` or not doesn't matter. All new root app elements will be rendered as a child of the app router.
|
||||
|
||||
Once these migrations are complete you should be left with an empty `convertLegacyAppRoot(...)` call that you can now remove, and your app should be fully migrated to the new system! 🎉
|
||||
#### App Root Wrappers
|
||||
|
||||
### Top-level Routes
|
||||
App root wrappers are React elements that are rendered as a parent of the current `Root` elements. For example, in this snippet the `CustomAppBarrier` is an app root wrapper:
|
||||
|
||||
Your top-level routes are the routes directly under the `AppRouter` component with the `<FlatRoutes>` element. In a small app they might look something like this:
|
||||
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
<Route path="/catalog" element={<CatalogIndexPage />} />
|
||||
<Route
|
||||
path="/catalog/:namespace/:kind/:name"
|
||||
element={<CatalogEntityPage />}
|
||||
>
|
||||
{entityPage}
|
||||
</Route>
|
||||
<Route path="/create" element={<ScaffolderPage />} />
|
||||
<Route
|
||||
path="/tech-radar"
|
||||
element={<TechRadarPage width={1500} height={800} />}
|
||||
/>
|
||||
</FlatRoutes>
|
||||
```tsx
|
||||
const convertedRootFeatures = convertLegacyAppRoot(
|
||||
<>
|
||||
<AlertDisplay transientTimeoutMs={2500} />
|
||||
<OAuthRequestDialog />
|
||||
<AppRouter>
|
||||
{/* highlight-next-line */}
|
||||
<CustomAppBarrier>
|
||||
<Root>{routes}</Root>
|
||||
{/* highlight-next-line */}
|
||||
</CustomAppBarrier>
|
||||
</AppRouter>
|
||||
</>,
|
||||
);
|
||||
```
|
||||
|
||||
Each of these routes needs to be migrated to the new system. You can do it as gradually as you want, with the only restriction being that **all routes from a single plugin must be migrated at once**. This is because plugins discovered from these legacy routes will override any plugins that are installed in your app. If you for example only migrate one of the two routes defined by a plugin, the other route will remain and still override any plugin with the same ID, and you're left with a partial and likely broken plugin.
|
||||
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.
|
||||
|
||||
To migrate a route, you need to remove it from your list of routes and instead install the new version of the plugin in your app. Before doing this you should make sure that the plugin supports the new system. Let's remove the scaffolder route as an example:
|
||||
Here is an example converting the `CustomAppBarrier` into extension:
|
||||
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
<Route path="/catalog" element={<CatalogIndexPage />} />
|
||||
<Route
|
||||
path="/catalog/:namespace/:kind/:name"
|
||||
element={<CatalogEntityPage />}
|
||||
>
|
||||
{entityPage}
|
||||
</Route>
|
||||
{/* highlight-remove-next-line */}
|
||||
<Route path="/create" element={<ScaffolderPage />} />
|
||||
<Route
|
||||
path="/tech-radar"
|
||||
element={<TechRadarPage width={1500} height={800} />}
|
||||
/>
|
||||
</FlatRoutes>
|
||||
);
|
||||
```
|
||||
|
||||
If you are using [app feature discovery](../architecture/10-app.md#feature-discovery) the installation step is simple, it's already done! The new version of the scaffolder plugin was already discovered and present in the app, it was simply disabled because the plugin created from the legacy route had higher priority. If you do not use feature discovery, you will instead need to manually install the new scaffolder plugin in your app through the `features` option of `createApp`.
|
||||
|
||||
Continue this process for each of your legacy routes until you have migrated all of them. For any plugin with additional extensions installed as children of the `Route`, refer to the plugin READMEs for more detailed instructions. For the entity pages, refer to the [separate section](#entity-pages).
|
||||
|
||||
### Entity Pages
|
||||
|
||||
The entity pages are typically defined in `packages/app/src/components/catalog` and rendered as a child of the `/catalog/:namespace/:kind/:name` route. The entity pages are typically quite large and bringing in content from quite a lot of different plugins. To help gradually migrate entity pages we provide the `entityPage` option in the `convertLegacyAppRoot` helper. This option lets you pass in an entity page app element tree that will be converted to extensions that are added to the features returned from `convertLegacyAppRoot`.
|
||||
|
||||
To start the gradual migration of entity pages, add your `entityPages` to the `convertLegacyAppRoot` call:
|
||||
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
/* highlight-remove-next-line */
|
||||
const legacyFeatures = convertLegacyAppRoot(routes);
|
||||
/* highlight-add-next-line */
|
||||
const legacyFeatures = convertLegacyAppRoot(routes, { entityPage });
|
||||
```
|
||||
|
||||
Next, you will need to fully migrate the catalog plugin itself. This is because only a single version of a plugin can be installed in the app at a time, so in order to start using the new version of the catalog plugin you need to remove all usage of the old one. This includes both the routes and entity pages. You will need to keep the structural helpers for the entity pages, such as `EntityLayout` and `EntitySwitch`, but remove any extensions like the `<CatalogIndexPage/>` and entity cards and content like `<EntityAboutCard/>` and `<EntityOrphanWarning/>`.
|
||||
|
||||
Remove the following routes:
|
||||
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
...
|
||||
{/* highlight-remove-start */}
|
||||
<Route path="/catalog" element={<CatalogIndexPage />} />
|
||||
<Route
|
||||
path="/catalog/:namespace/:kind/:name"
|
||||
element={<CatalogEntityPage />}
|
||||
>
|
||||
{entityPage}
|
||||
</Route>
|
||||
{/* highlight-remove-end */}
|
||||
...
|
||||
</FlatRoutes>
|
||||
);
|
||||
```
|
||||
|
||||
And explicitly install the catalog plugin before the converted legacy features:
|
||||
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
/* highlight-add-next-line */
|
||||
import { default as catalogPlugin } from '@backstage/plugin-catalog/alpha';
|
||||
|
||||
const app = createApp({
|
||||
/* highlight-remove-next-line */
|
||||
features: [optionsModule, ...legacyFeatures],
|
||||
/* highlight-add-next-line */
|
||||
features: [catalogPlugin, optionsModule, ...legacyFeatures],
|
||||
});
|
||||
```
|
||||
|
||||
If you are not using the default `<CatalogIndexPage />` you can install your custom catalog page as an override for now instead, and fully migrate it to the new system later.
|
||||
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
/* highlight-remove-start */
|
||||
const catalogPluginOverride = catalogPlugin.withOverrides({
|
||||
extensions: [
|
||||
catalogPlugin.getExtension('page:catalog').override({
|
||||
params: {
|
||||
loader: async () => (
|
||||
<CatalogIndexPage
|
||||
pagination={{ mode: 'offset', limit: 20 }}
|
||||
filters={<>{/* ... */}</>}
|
||||
/>
|
||||
),
|
||||
},
|
||||
```tsx
|
||||
createApp({
|
||||
// ...
|
||||
features: [
|
||||
createFrontendModule({
|
||||
pluginId: 'app',
|
||||
extensions: [
|
||||
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,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
/* highlight-remove-end */
|
||||
|
||||
const app = createApp({
|
||||
/* highlight-remove-next-line */
|
||||
features: [catalogPlugin, optionsModule, ...legacyFeatures],
|
||||
/* highlight-add-next-line */
|
||||
features: [catalogPluginOverride, optionsModule, ...legacyFeatures],
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
At this point you should be able to run the app and see that you're not using the new version of the catalog plugin. If you navigate to the entity pages you will likely see a lot of duplicate content at the bottom of the page. These are the duplicates of the entity cards provided by the catalog plugin itself that we mentioned earlier that you need to remove. Clean up the entity pages by removing cards and content from the catalog plugin such as `<EntityAboutCard/>` and `<EntityOrphanWarning/>`.
|
||||
|
||||
Once the cleanup is complete you should be left with clean entity pages that are built using a mix of the old and new frontend system. From this point you can continue to gradually migrate plugins that provide content for the entity pages, until all plugins have been fully moved to the new system and the `entityPage` option can be removed.
|
||||
|
||||
Migrating across the tabs for the Entity Pages should be as simple as removing the `EntityLayout.Route` for each of the plugins that provide tab content, and then this tab should be sourced from the `EntityContent` extensions created by the plugins themselves which will be automatically detected and added to the App.
|
||||
|
||||
### Sidebar
|
||||
#### App Root Sidebar
|
||||
|
||||
New apps feature a built-in sidebar extension which is created by using the `NavContentBlueprint` in `src/modules/nav/Sidebar.tsx`. The default implementation of the sidebar in this blueprint will render some items explicitly in different groups, and then render the rest of the items which are the other `NavItem` extensions provided by the system.
|
||||
|
||||
@@ -725,76 +796,146 @@ app:
|
||||
- nav-item:search
|
||||
```
|
||||
|
||||
### App Root Elements
|
||||
#### App Root Routes
|
||||
|
||||
App root elements are React elements that are rendered adjacent to your current `Root` component. For example, in this snippet `AlertDisplay`, `OAuthRequestDialog` and `VisitListener` are all app root elements:
|
||||
Your top-level routes are the routes directly under the `AppRouter` component with the `<FlatRoutes>` element. In a small app they might look something like this:
|
||||
|
||||
```tsx
|
||||
export default app.createRoot(
|
||||
<>
|
||||
{/* highlight-next-line */}
|
||||
<AlertDisplay transientTimeoutMs={2500} />
|
||||
{/* highlight-next-line */}
|
||||
<OAuthRequestDialog />
|
||||
<AppRouter>
|
||||
{/* highlight-next-line */}
|
||||
<VisitListener />
|
||||
<Root>{routes}</Root>
|
||||
</AppRouter>
|
||||
</>,
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
<Route path="/catalog" element={<CatalogIndexPage />} />
|
||||
<Route
|
||||
path="/catalog/:namespace/:kind/:name"
|
||||
element={<CatalogEntityPage />}
|
||||
>
|
||||
{entityPage}
|
||||
</Route>
|
||||
<Route path="/create" element={<ScaffolderPage />} />
|
||||
<Route
|
||||
path="/tech-radar"
|
||||
element={<TechRadarPage width={1500} height={800} />}
|
||||
/>
|
||||
</FlatRoutes>
|
||||
);
|
||||
```
|
||||
|
||||
The `AlertDisplay` and `OAuthRequestDialog` are already provided as built-in extensions, and so will `VisitListener`. But, if you have your own custom root elements you will need to migrate them to be extensions that you install in the app instead. Use `createAppRootElementExtension` to create said extension and then install it in the app.
|
||||
Each of these routes needs to be migrated to the new system. You can do it as gradually as you want, with the only restriction being that **all routes from a single plugin must be migrated at once**. This is because plugins discovered from these legacy routes will override any plugins that are installed in your app. If you for example only migrate one of the two routes defined by a plugin, the other route will remain and still override any plugin with the same ID, and you're left with a partial and likely broken plugin.
|
||||
|
||||
Whether the element used to be rendered as a child of the `AppRouter` or not doesn't matter. All new root app elements will be rendered as a child of the app router.
|
||||
To migrate a route, you need to remove it from your list of routes and instead install the new version of the plugin in your app. Before doing this you should make sure that the plugin supports the new system. Let's remove the scaffolder route as an example:
|
||||
|
||||
### App Root Wrappers
|
||||
|
||||
App root wrappers are React elements that are rendered as a parent of the current `Root` elements. For example, in this snippet the `CustomAppBarrier` is an app root wrapper:
|
||||
|
||||
```tsx
|
||||
export default app.createRoot(
|
||||
<>
|
||||
<AlertDisplay transientTimeoutMs={2500} />
|
||||
<OAuthRequestDialog />
|
||||
<AppRouter>
|
||||
{/* highlight-next-line */}
|
||||
<CustomAppBarrier>
|
||||
<Root>{routes}</Root>
|
||||
{/* highlight-next-line */}
|
||||
</CustomAppBarrier>
|
||||
</AppRouter>
|
||||
</>,
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
<Route path="/catalog" element={<CatalogIndexPage />} />
|
||||
<Route
|
||||
path="/catalog/:namespace/:kind/:name"
|
||||
element={<CatalogEntityPage />}
|
||||
>
|
||||
{entityPage}
|
||||
</Route>
|
||||
{/* highlight-remove-next-line */}
|
||||
<Route path="/create" element={<ScaffolderPage />} />
|
||||
<Route
|
||||
path="/tech-radar"
|
||||
element={<TechRadarPage width={1500} height={800} />}
|
||||
/>
|
||||
</FlatRoutes>
|
||||
);
|
||||
```
|
||||
|
||||
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.
|
||||
If you are using [app feature discovery](../architecture/10-app.md#feature-discovery) the installation step is simple, it's already done! The new version of the scaffolder plugin was already discovered and present in the app, it was simply disabled because the plugin created from the legacy route had higher priority. If you do not use feature discovery, you will instead need to manually install the new scaffolder plugin in your app through the `features` option of `createApp`.
|
||||
|
||||
Here is an example converting the `CustomAppBarrier` into extension:
|
||||
Continue this process for each of your legacy routes until you have migrated all of them. For any plugin with additional extensions installed as children of the `Route`, refer to the plugin READMEs for more detailed instructions. For the entity pages, refer to the [separate section](#catalog-entity-page).
|
||||
|
||||
```tsx
|
||||
createApp({
|
||||
// ...
|
||||
features: [
|
||||
createFrontendModule({
|
||||
pluginId: 'app',
|
||||
extensions: [
|
||||
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,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
// ...
|
||||
### Migrating core, internal and third-party plugins
|
||||
|
||||
For certain core plugins—such as the Catalog plugin's entity page—we provide a dedicated step-by-step migration guide, since these plugins often require a more gradual approach due to their complexity.
|
||||
|
||||
For your custom internal plugins, refer to the [plugin migration guide](../building-plugins/05-migrating.md) for instructions on migrating internal plugins. For external plugins, check their migration status and contribute if needed.
|
||||
|
||||
#### Catalog Entity Page
|
||||
|
||||
The entity pages are typically defined in `packages/app/src/components/catalog` and rendered as a child of the `/catalog/:namespace/:kind/:name` route. The entity pages are typically quite large and bringing in content from quite a lot of different plugins. To help gradually migrate entity pages we provide the `entityPage` option in the `convertLegacyAppRoot` helper. This option lets you pass in an entity page app element tree that will be converted to extensions that are added to the features returned from `convertLegacyAppRoot`.
|
||||
|
||||
To start the gradual migration of entity pages, add your `entityPages` to the `convertLegacyAppRoot` call:
|
||||
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
/* highlight-remove-next-line */
|
||||
const convertedRootFeatures = convertLegacyAppRoot(routes);
|
||||
/* highlight-add-next-line */
|
||||
const convertedRootFeatures = convertLegacyAppRoot(routes, { entityPage });
|
||||
```
|
||||
|
||||
Next, you will need to fully migrate the catalog plugin itself. This is because only a single version of a plugin can be installed in the app at a time, so in order to start using the new version of the catalog plugin you need to remove all usage of the old one. This includes both the routes and entity pages. You will need to keep the structural helpers for the entity pages, such as `EntityLayout` and `EntitySwitch`, but remove any extensions like the `<CatalogIndexPage/>` and entity cards and content like `<EntityAboutCard/>` and `<EntityOrphanWarning/>`.
|
||||
|
||||
Remove the following routes:
|
||||
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
...
|
||||
{/* highlight-remove-start */}
|
||||
<Route path="/catalog" element={<CatalogIndexPage />} />
|
||||
<Route
|
||||
path="/catalog/:namespace/:kind/:name"
|
||||
element={<CatalogEntityPage />}
|
||||
>
|
||||
{entityPage}
|
||||
</Route>
|
||||
{/* highlight-remove-end */}
|
||||
...
|
||||
</FlatRoutes>
|
||||
);
|
||||
```
|
||||
|
||||
And explicitly install the catalog plugin before the converted legacy features:
|
||||
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
/* highlight-add-next-line */
|
||||
import { default as catalogPlugin } from '@backstage/plugin-catalog/alpha';
|
||||
|
||||
const app = createApp({
|
||||
/* highlight-remove-next-line */
|
||||
features: [...convertedOptionsFeatures, ...convertedRootFeatures],
|
||||
/* highlight-add-next-line */
|
||||
features: [catalogPlugin, ...convertedOptionsFeatures, ...convertedRootFeatures],
|
||||
});
|
||||
```
|
||||
|
||||
If you are not using the default `<CatalogIndexPage />` you can install your custom catalog page as an override for now instead, and fully migrate it to the new system later.
|
||||
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
/* highlight-remove-start */
|
||||
const catalogPluginOverride = catalogPlugin.withOverrides({
|
||||
extensions: [
|
||||
catalogPlugin.getExtension('page:catalog').override({
|
||||
params: {
|
||||
loader: async () => (
|
||||
<CatalogIndexPage
|
||||
pagination={{ mode: 'offset', limit: 20 }}
|
||||
filters={<>{/* ... */}</>}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
/* highlight-remove-end */
|
||||
|
||||
const app = createApp({
|
||||
/* highlight-remove-next-line */
|
||||
features: [catalogPlugin, ...convertedOptionsFeatures, ...convertedRootFeatures],
|
||||
/* highlight-add-next-line */
|
||||
features: [catalogPluginOverride, ...convertedOptionsFeatures, ...convertedRootFeatures],
|
||||
});
|
||||
```
|
||||
|
||||
At this point you should be able to run the app and see that you're not using the new version of the catalog plugin. If you navigate to the entity pages you will likely see a lot of duplicate content at the bottom of the page. These are the duplicates of the entity cards provided by the catalog plugin itself that we mentioned earlier that you need to remove. Clean up the entity pages by removing cards and content from the catalog plugin such as `<EntityAboutCard/>` and `<EntityOrphanWarning/>`.
|
||||
|
||||
Once the cleanup is complete you should be left with clean entity pages that are built using a mix of the old and new frontend system. From this point you can continue to gradually migrate plugins that provide content for the entity pages, until all plugins have been fully moved to the new system and the `entityPage` option can be removed.
|
||||
|
||||
Migrating across the tabs for the Entity Pages should be as simple as removing the `EntityLayout.Route` for each of the plugins that provide tab content, and then this tab should be sourced from the `EntityContent` extensions created by the plugins themselves which will be automatically detected and added to the App.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
We'd recommend that you install the `app-visualizer` plugin to help your troubleshooting. If you run `yarn add @backstage/plugin-app-visualizer` in `packages/app` it should be automatically added to the sidebar, and available on `/visualizer`.
|
||||
@@ -814,3 +955,9 @@ To fix this, simply remove the card definitions from your old Entity Page compon
|
||||
This means that the `Routes` inside `FlatRoutes` contains something other than a `Route` element. This could be for example a `FeatureFlag` or `RequirePermissions` element. These are not currently supported by the new frontend system. Workarounds include pushing this logic down from the `App.tsx` routes into the plugins themselves as these elements no longer need to live in the `App.tsx` for the system to be able to walk and collect the plugins and routes that are available in the App.
|
||||
|
||||
If you have a use case where these are required, please reach out to us either through a [bug report](https://github.com/backstage/backstage/issues/new/choose) or the [community Discord](https://discord.gg/backstage-687207715902193673).
|
||||
|
||||
## Next Steps
|
||||
|
||||
- See [architecture docs](../architecture/00-index.md) for more on the new system.
|
||||
- If you encounter issues, check [GitHub issues](https://github.com/backstage/backstage/issues) or ask in [Discord](https://discord.gg/backstage-687207715902193673).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user