Merge branch 'backstage:master' into gke-service-account

This commit is contained in:
Armando Comellas
2024-01-18 17:20:08 +00:00
committed by GitHub
806 changed files with 14714 additions and 4495 deletions
@@ -7,8 +7,7 @@ description: Architecture Decision Record (ADR) for Plugin Package Structure
## Context
A core feature of Backstage is the extensibility via plugins. The Backstage
repository is open for contributions of plugins. Even most of the core features
A core feature of Backstage is the extensibility via plugins. Even most of the core features
are implemented as plugins. A plugin consists of one or multiple packages in the
`plugins/` directory. Up till now, we have a simple conventions for naming
plugin packages: Plugins are named `x`, with the option of having a related
@@ -757,7 +757,7 @@ implementations that they represent, and being exported from there.
### The Auth Plugin
A basic installation of the auth plugin with a microsoft provider will look as follows.
A basic installation of the auth plugin with a Microsoft provider will look as follows.
```ts title="packages/backend/src/index.ts"
const backend = createBackend();
+64 -6
View File
@@ -65,20 +65,72 @@ This is an array used to determine where to retrieve cluster configuration from.
Valid cluster locator methods are:
- [`catalog`](#catalog)
- [`localKubectlProxy`](#localkubectlproxy)
- [`config`](#config)
- [`gke`](#gke)
- [`localKubectlProxy`](#localkubectlproxy)
- [custom `KubernetesClustersSupplier`](#custom-kubernetesclusterssupplier)
#### `catalog`
This cluster locator method will read cluster information from the catalog.
This cluster locator method will gather
[Resources](https://backstage.io/docs/features/software-catalog/system-model#resource)
of
[type](https://backstage.io/docs/features/software-catalog/descriptor-format#spectype-required-4)
`kubernetes-cluster` from the catalog and treat them as clusters for the
purposes of the Kubernetes plugin. In order for a resource to be detected by
this method, it must also have the following
[annotations](https://backstage.io/docs/features/software-catalog/descriptor-format#annotations-optional)
(as seen
[here](https://github.com/backstage/backstage/blob/86baccb2d7d378baed74eaebf017c60b410986e5/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.ts#L51-L61)
in the code):
#### `localKubectlProxy`
- [`kubernetes.io/api-server`](https://backstage.io/docs/reference/plugin-kubernetes-common.annotation_kubernetes_api_server/),
denoting the base URL of the Kubernetes control plane
- [`kubernetes.io/api-server-certificate-authority`](https://backstage.io/docs/reference/plugin-kubernetes-common.annotation_kubernetes_api_server_ca/),
containing a base64-encoded certificate authority bundle in PEM format;
Backstage will check that the control plane presents a certificate signed by
this authority.
- [`kubernetes.io/auth-provider`](https://backstage.io/docs/reference/plugin-kubernetes-common.annotation_kubernetes_auth_provider/),
denoting the strategy to use to authenticate with the control plane.
This cluster locator method will assume a locally running [`kubectl proxy`](https://kubernetes.io/docs/tasks/extend-kubernetes/http-proxy-access-api/#using-kubectl-to-start-a-proxy-server) process using the default port (8001).
There are many other annotations that can be applied to a cluster resource to
configure the way Backstage communicates, documented
[here](https://backstage.io/docs/reference/plugin-kubernetes-common#variables)
in the API reference. Here is a YAML snippet illustrating an example of a
cluster in the catalog:
NOTE: This cluster locator method is for local development only and should not be used in production.
```yaml
apiVersion: backstage.io/v1alpha1
kind: Resource
metadata:
name: my-cluster
annotations:
kubernetes.io/api-server: 'https://127.0.0.1:53725'
kubernetes.io/api-server-certificate-authority: # base64-encoded CA
kubernetes.io/auth-provider: 'oidc'
kubernetes.io/oidc-token-provider: 'microsoft'
kubernetes.io/skip-metrics-lookup: 'true'
spec:
type: kubernetes-cluster
owner: user:guest
```
Note that it is insecure to store a Kubernetes service account token in an
annotation on a catalog entity (where it could easily be accidentally revealed
by the catalog API) -- therefore there is no annotation corresponding to the
[`serviceAccountToken` field](#clustersserviceaccounttoken-optional) used by
the [`config`](#config) cluster locator. Accordingly, the catalog cluster
locator does not support the [`serviceAccount`](#clustersauthprovider) auth
strategy.
This method can be quite helpful when used in combination with an ingestion
procedure like the
[`GkeEntityProvider`](https://backstage.io/docs/reference/plugin-catalog-backend-module-gcp.gkeentityprovider/)
(installation documented
[here](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-gcp#installation))
or the
[`AwsEKSClusterProcessor`](https://backstage.io/docs/reference/plugin-catalog-backend-module-aws.awseksclusterprocessor/)
to automatically update the set of clusters tracked by Backstage.
#### `config`
@@ -401,6 +453,12 @@ Defaults to `false`.
Array of key value labels used to filter out clusters which don't have the matching
[resource labels](https://cloud.google.com/resource-manager/docs/creating-managing-labels).
#### `localKubectlProxy`
This cluster locator method will assume a locally running [`kubectl proxy`](https://kubernetes.io/docs/tasks/extend-kubernetes/http-proxy-access-api/#using-kubectl-to-start-a-proxy-server) process using the default port (8001).
NOTE: This cluster locator method is for local development only and should not be used in production.
#### Custom `KubernetesClustersSupplier`
If the configuration-based cluster locators do not work for your use-case,
@@ -606,7 +664,7 @@ SingleTenant Cluster:
In the example above, we configured the "backstage.io/kubernetes-cluster" annotation on the entity `catalog-info.yaml` file to specify that the current component is running in a single cluster called "dice-cluster", so this cluster must have been specified in the `app-config.yaml`, under the Kubernetes clusters configuration (for more details, see [`Configuring Kubernetes clusters`](#configuring-kubernetes-clusters)).
If you do not specify the annotation by `default Backstage fetches all` defined Kubernetes cluster.
If you do not specify the annotation, by default Backstage fetches from all defined Kubernetes clusters.
[1]: https://cloud.google.com/kubernetes-engine
[2]: https://cloud.google.com/docs/authentication/production#linux-or-macos
@@ -10,38 +10,158 @@ description: Frontend extension overrides
## Introduction
<!--
An extension override is a building block of the frontend system that allows you to programmatically override app or plugin extensions anywhere in your application. Since the entire application is built mostly out of extensions from the bottom up, this is a powerful feature. You can use it for example to provide your own app root layout, to replace the implementation of a Utility API with a custom one, to override how the catalog page renders itself, and much more.
Introduce extension overrides and how they can be shipped in separate packages but also internally within a project too.
In general, most features should have a good level of customization built into them, so that users do not have to leverage extension overrides to achieve common goals. A well written feature often has [configuration](../../conf/) settings, or uses extension inputs for extensibility where applicable. An example of this is the search plugin, which allows you to provide result renderers as inputs rather than replacing the result page wholesale just to tweak how results are shown. Adopters should take advantage of those when possible, and only use extension overrides when it's necessary to entirely replace the extension. Check the respective extension documentation for guidance.
Talk about how extension overrides
## Override App Extensions
-->
In order to override an app extension, you must create a new extension and add it to the list of overridden features. The steps are: create your extension overrides and use them in Backstage.
## Creating a Extension Override
### Example
<!--
In the example below, we create a file that exports custom extensions for the app's `light` and `dark` themes:
How to create an extension override and export it from a package.A
```tsx title="packages/app/src/themes.ts"
import {
createThemeExtension,
createExtensionOverrides
} from '@backstage/frontend-plugin-api';
import { apertureThemes } from './themes';
import { ApertureLightIcon, ApertureDarkIcon } from './icons';
- Example using the default exports
// Creating a light theme extension
const apertureLightTheme = createThemeExtension({
// highlight-start
namespace: 'app',
name: 'light',
// highlight-end
title: 'Aperture Light Theme',
variant: 'light',
icon: <ApertureLightIcon />,
Provider: ({ children }) => (
<UnifiedThemeProvider theme={apertureThemes.light} children={children} />
),
});
You can also create them directly withing the app for local overrides.
// Creating a dark theme extension
const apertureDarkTheme = createThemeExtension({
// highlight-start
namespace: 'app',
name: 'dark',
// highlight-end
title: 'Aperture Dark Theme',
variant: 'dark',
icon: <ApertureDarkIcon />,
Provider: ({ children }) => (
<UnifiedThemeProvider theme={apertureThemes.dark} children={children} />
),
});
- Example of installing an override directly into an app
// Creating an extension overrides preset
export createExtensionOverrides({
extensions: [apertureLightTheme, apertureDarkTheme]
});
```
Mention that in can still be a good idea to split your overrides out into separate packages in large projects. But it's up to you to decide how to group the extensions into extension overrides.
Note that we declare `namespace` as `'app'` while creating the themes, so the system knows we are overriding app extensions. Additionally, to specifically override the `light` and `dark` theme extensions, we set the `name` option to `light` and `dark`. Therefore, to override app theme extensions, we ensure that the extension `namespace` and `name` match those of the default app theme extension definitions.
-->
Now we are able to use the overrides in a Backstage app:
## Overriding Existing Extensions
```tsx title="packages/app/src/App.tsx"
import { createApp } from '@backstage/frontend-app-api';
import themeOverrides from './themes';
<!--
const app = createApp({
// highlight-next-line
features: [themeOverrides],
});
To override an existing extension you need to provide an extension through an extension override that has the same ID as the existing extension. That is, all of kind, namespace and name must match the extension you want to override.
export default app.createRoot().
```
This means that you typically need to provide an explicit namespace when overriding extensions from a plugin.
If the plugin you want to change is internal to your company or you just want to replace one of the application's core extensions, you can decide to store the overrides code directly in the app package or extract them to a separate package.
- Example of how to create an extension override for an existing extension from some plugin
Note that it can still be a good idea to split your overrides out into separate packages in large projects. But it's up to you to decide how to group the extensions into extension overrides.
-->
## Override Plugin Extensions
To override an extension that is provided by a plugin, you need to provide a new extension that has the same ID as the existing extension. That is, all kind, namespace, and name options must match the extension you want to replace. This means that you typically need to provide an explicit `namespace` when overriding extensions from a plugin.
:::info
We recommend that plugin developers share the extension IDs in their plugin documentation, but usually you can infer the ID by following the [naming patterns](./08-naming-patterns.md) documentation.
:::
### Example
Imagine you have a plugin with the ID `'search'`, and the plugin provides a page extension that you want to fully override with your own custom component. To do so, you need to create your page extension with an explicit `namespace` option that matches that of the plugin that you want to override, in this case `'search'`. If the existing extension also has an explicit `name` you'd need to set the `name` of your override extension to the same value as well.
```tsx title="packages/app/src/search.ts"
import { createPageExtension } from '@backstage/frontend-plugin-api';
// Creating a custom search page extension
const customSearchPage = createPageExtension({
// highlight-next-line
namespace: 'search',
// Omitting name since it is the index plugin page
defaultPath: '/search',
loader: () => import('./SearchPage').then(m => m.<SearchPage/>),
});
export createExtensionOverrides({
extensions: [customSearchPage]
});
```
Don't forget to configure your overrides in the `createApp` function:
```tsx title="packages/app/src/App.tsx"
import { createApp } from '@backstage/frontend-app-api';
import searchOverrides from './search';
const app = createApp({
// highlight-next-line
features: [searchOverrides],
});
export default app.createRoot();
```
Now let's talk about the last override case, orphan extensions.
## Create Standalone Extensions
Sometimes you just need to quickly create a new extension and not overwrite an app extension or plugin. You can also use overrides to create extensions, but remember that if you want to make this extension available for installation by other users, we recommend providing it via a plugin in a separate package.
### Example
Imagine you want to create a page that is currently only used by your application, like an Institutional page, for example. You can use overrides to extend the Backstage app to render it. To do so, simply create a page extension and pass it to the app as an override:
```tsx title="packages/app/src/App.ts"
import { createApp } from '@backstage/frontend-app-api';
import {
createPageExtension,
createExtensionOverrides,
} from '@backstage/frontend-plugin-api';
const app = createApp({
features: [
createExtensionOverrides({
extensions: [
// highlight-start
createPageExtension({
name: 'institutional',
defaultPath: '/institutional',
loader: () =>
import('./institutional').then(m => <m.InstitutionalPage />),
}),
// highlight-end
],
}),
],
});
export default app.createRoot();
```
Note that we are omitting `namespace` when creating the page extension. When we omit `namespace`, we are telling the system the new extension is standalone and not an application or plugin extension!
+390 -103
View File
@@ -8,143 +8,430 @@ description: Frontend routes
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
See [routing system docs](../../plugins/composability.md#routing-system)
## Introduction
<!--
Each Backstage plugin is an isolated piece of functionality that doesn't typically communicate directly with other plugins. In order to achieve this, there are many parts of the frontend system that provide a layer of indirection for cross-plugin communication, and the routing system is one of them.
Routing system of backstage - what's the prupose
Explain the indirection needed to route between plugins. For example what do we do if plugin A want to link to plugin B, but plugin B is not installed? How can we support this in an app?
How can we let an integrator decice whether plugin A should link to plugin B or plugin C? (with some good real-work examples, e.g. catalog -> scaffolder create page / catalog-import plugin)
-->
The Backstage routing system makes it possible to implement navigation across plugin boundaries, without each individual plugin knowing the concrete path or location of other plugins in the routing hierarchy, or even its own. This is achieved through the concept of route references, which are opaque reference values that can be shared and used to create concrete links to different parts of an app. The route ref paths can be configured both at plugin level (by plugin developers) and at the app level (by integrators). It is up to plugin developers to create route references for any page content in their plugin that they want it to be possible to link to or from.
## Route References
<!--
Plugin developers create a `RouteRef` to expose a path in Backstage's routing system. You will see below how routes are defined programmatically, but before diving into code, let us explain how to configure them at the app level. In spite of the fact that plugin developers choose a default route path for the routes their plugin provides, paths are configurable, so app integrators can set a custom path to a route whenever they like to (more information in the following sections).
Explain what a route reference is - then explain how it solves the problem outlined above.
Explain that we use route references to represent a path in an app. Each referenced path is unique for each app, but in different apps the same route ref might point to different paths.
-->
There are three types of route references: regular route, sub route, and external route, and we will cover both the concept and code definition for each.
### Creating a Route Reference
<!--
Route references, also known as "absolute" or "regular" routes, are created as follows:
Example using `createRouteRef` + passing it to a page extension
```tsx title="plugins/catalog/src/routes.ts"
import { createRouteRef } from '@backstage/frontend-plugin-api';
-->
// Creates a route reference, which is not yet associated with any plugin page
export const indexRouteRef = createRouteRef();
```
### Using a Route Reference
Note that you often want to create the route references themselves in a different file than the one that creates the plugin instance, for example a top-level `routes.ts`. This is to avoid circular imports when you use the route references from other parts of the same plugin.
<!--
Example using `useRouteRef` in a component
-->
### Route Path Parameters
<!--
Example using path parameters, both createRouteRef and useRouteRef
-->
Route refs do not have any behavior themselves. They are an opaque value that represents route targets in an app, which are bound to specific paths at runtime. Their role is to provide a level of indirection to help link together different pages that otherwise wouldn't know how to route to each other.
### Providing Route References to Plugins
<!--
Show how to provide routes through createPlugin({ routes: })
-->
The code snippet in the previous section does not indicate which plugin the route belongs to. To do so, you have to use it in the creation of any kind of routable extension, such as a page extension:
## External Router References
```tsx title="plugins/catalog/src/plugin.tsx"
import React from 'react';
import {
createPlugin,
createPageExtension,
} from '@backstage/frontend-plugin-api';
import { indexRouteRef } from './routes';
<!--
Explain the need for external route refs
const catalogIndexPage = createPageExtension({
// The `name` option is omitted because this is an index page
defaultPath: '/entities',
// highlight-next-line
routeRef: indexRouteRef,
loader: () => import('./components').then(m => <m.IndexPage />),
});
Explain all of create, use and provide
export default createPlugin({
id: 'catalog',
// highlight-start
routes: {
index: indexRouteRef,
},
// highlight-end
extensions: [catalogIndexPage],
});
```
-->
In the example above we associated the `indexRouteRef` with the `catalogIndexPage` extension and provided both the route ref and page via the Catalog plugin. So, When this plugin is installed in the app, the index page will become associated with the newly created `RouteRef`, making it possible to use the route ref to navigate the page extension.
It may seem unclear why we configure the `routes` option when creating a plugin as the route has already been passed to the extension. We do that to make it possible for other plugins to route to our page, which is explained in detail in the [binding routes](#binding-external-route-references) section.
### Defining References with Path Parameters
Route references optionally accept a `params` option, which will require the listed parameter names to be present in the route path. Here is how you create a reference for a route that requires a `kind`, `namespace` and `name` parameters, like in this path `/entities/:kind/:namespace/:name`:
```tsx title="plugins/catalog/src/routes.ts"
import { createRouteRef } from '@backstage/frontend-plugin-api';
export const detailsRouteRef = createRouteRef({
// The parameters that must be included in the path of this route reference
// highlight-next-line
params: ['kind', 'namespace', 'name'],
});
```
### Using a Route Reference
Route references can be used to link to page in the same plugin, or to pages in different plugins. In this section we will cover the first scenario. If you are interested in linking to a page of a different plugin, please go to the [external routes](#external-route-references) section below.
Suppose we are creating a plugin that renders a Catalog index page with a link to a "Foo" component details page. Here is the code for the index page:
```tsx title="plugins/catalog/src/components/IndexPage.tsx"
import React from 'react';
import { useRouteRef } from '@backstage/frontend-plugin-api';
import { detailsRouteRef } from '../routes';
export const IndexPage = () => {
// highlight-next-line
const getDetailsPath = useRouteRef(detailsRouteRef);
return (
<div>
<h1>Index Page</h1>
<a
{/* highlight-start */}
href={getDetailsPath({
kind: 'component',
namespace: 'default',
name: 'foo',
})}
{/* highlight-end */}
>
See "Foo" details
</a>
</div>
);
};
```
We use the `useRouteRef` hook to create a link generator function that returns the details page path. We then call the link generator, passing it an object with the kind, namespace, and name. These parameters are used to construct a concrete path to the "Foo" details page.
Let's see how the details page can get the parameters from the URL:
```tsx title="plugins/catalog/src/components/DetailsPage.tsx"
import React from 'react';
import { useRouteRefParams } from '@backstage/frontend-plugin-api';
import { detailsRouteRef } from '../routes';
export const DetailsPage = () => {
// highlight-next-line
const params = useRouteRefParams(detailsRouteRef);
return (
<div>
<h1>Details Page</h1>
<ul>
{/* highlight-start */}
<li>Kind: {params.kind}</li>
<li>Namespace: {params.namespace}</li>
<li>Name: {params.name}</li>
{/* highlight-end */}
</ul>
</div>
);
};
```
In the code above, we are using the `useRouteRefParams` hook to retrieve the entity-composed id from the URL. The parameter object contains three values: kind, namespace, and name. We can display these values or call an API using them.
Since we are linking to pages of the same package, we are using a route ref directly. However, in the following sections, you will see how to link to pages of different plugins.
## External Route References
External routes are made for linking to a page of an external plugin.
For this section example, let's assume that we want to link from the Catalog entities list page to the Scaffolder create component page.
We don't want to reference the Scaffolder plugin directly, since that would create an unnecessary dependency. It would also provide little flexibility in allowing the app to tie plugins together, with the links instead being dictated by the plugins themselves. To solve this, we use an `ExternalRouteRef`. Much like regular route references, they can be passed to `useRouteRef` to create concrete URLs, but they can not be used in page extensions and instead have to be associated with a target route using route bindings in the app.
We create a new `RouteRef` inside the Scaffolder plugin, using a neutral name that describes its role in the plugin rather than a specific plugin page that it might be linking to, allowing the app to decide the final target. If the Catalog entity list page for example wants to link the Scaffolder create component page in the header, it might declare an `ExternalRouteRef` similar to this:
```tsx title="plugins/catalog/src/routes.ts"
import { createExternalRouteRef } from '@backstage/frontend-plugin-api';
export const createComponentExternalRouteRef = createExternalRouteRef();
```
External routes are also used in a similar way as regular routes:
```tsx title="plugins/catalog/src/components/IndexPage.tsx"
import React from 'react';
import { useRouteRef } from '@backstage/frontend-plugin-api';
import { createComponentExternalRouteRef } from '../routes';
export const IndexPage = () => {
// highlight-next-line
const getCreateComponentPath = useRouteRef(createComponentExternalRouteRef);
return (
<div>
<h1>Index Page</h1>
{/* highlight-next-line */}
<a href={getCreateComponentPath()}>Create Component</a>
</div>
);
};
```
Given the above binding, using `useRouteRef(createComponentExternalRouteRef)` within the Catalog plugin will let us create a link to whatever path the Scaffolder create component page is mounted at. Note that there is no direct dependency between the Catalog plugin and Scaffolder, that is, we are not importing the `createComponentExternalRouteRef` from the Scaffolder package.
Now the only thing left is to provide the page and external route via a plugin:
```tsx title="plugins/catalog/src/plugin.tsx"
import React from 'react';
import {
createPlugin,
createPageExtension,
useRouteRef,
} from '@backstage/frontend-plugin-api';
import { indexRouteRef, createComponentExternalRouteRef } from './routes';
const catalogIndexPage = createPageExtension({
defaultPath: '/entities',
routeRef: indexRouteRef,
loader: () => import('./components').then(m => <m.IndexPage />),
});
export default createPlugin({
id: 'catalog',
routes: {
index: indexRouteRef,
},
// highlight-start
externalRoutes: {
createComponent: createComponentExternalRouteRef,
},
extensions: [catalogIndexPage],
// highlight-end
});
```
External routes can also have parameters. For example, if you want to link to an entity's details page from Scaffolder, you'll need to create an external route that receives the same parameters the Catalog details page expects:
```tsx title="plugins/scaffolder/src/routes.ts"
import { createExternalRouteRef } from '@backstage/frontend-plugin-api';
export const entityDetailsExternalRouteRef = createExternalRouteRef({
// highlight-next-line
params: ['kind', 'namespace', 'name'],
});
```
Now let's move on and configure the app to resolve these external routes, so that the Scaffolder links to the Catalog entity page, and the Catalog links to the Scaffolder page.
### Binding External Route References
<!--
Example using `createApp`
The association of external routes is controlled by the app. Each `ExternalRouteRef` of a plugin should be bound to an actual `RouteRef`, usually from another plugin. The binding process happens once at app startup, and is then used through the lifetime of the app to help resolve concrete route paths.
Example using config
Using the above example of the Catalog entities list page to the Scaffolder create component page, we might do something like this in the app configuration file:
-->
```yaml title="app-config.yaml"
app:
routes:
bindings:
# point to the Scaffolder create component page when the Catalog create component ref is used
# highlight-next-line
plugin.catalog.externalRoutes.createComponent: plugin.scaffolder.routes.index
# point to the Catalog details page when the Scaffolder component details ref is used
# highlight-next-line
plugin.scaffolder.externalRoutes.componentDetails: plugin.catalog.routes.details
```
We also have the ability to express this in code as an option to `createApp`, but you of course only need to use one of these two methods:
```tsx title="packages/app/src/App.tsx"
import { createApp } from '@backstage/frontend-app-api';
import catalog from '@backstage/plugin-catalog';
import scaffolder from '@backstage/plugin-scaffolder';
const app = createApp({
// highlight-start
bindRoutes({ bind }) {
bind(catalog.externalRoutes, {
createComponent: scaffolder.routes.createComponent,
});
bind(scaffolder.externalRoutes, {
componentDetails: catalog.routes.details,
});
},
// highlight-end
});
export default app.createRoot();
```
Note that we are not importing and using the `RouteRef`s directly in the app, and instead rely on the plugin instance to access routes of the plugins. This provides better namespacing and discoverability of routes, as well as reduce the number of separate exports from each plugin package.
Another thing to note is that this indirection in the routing is particularly useful for open source plugins that need to provide flexibility in how they are integrated. For plugins that you build internally for your own Backstage application, you can choose to use direct imports or even concrete route path strings directly. Although there can be some benefits to using the full routing system even in internal plugins: it can help you structure your routes, and as you will see further down it also helps you manage route parameters.
### Optional External Route References
<!--
Talk about how external routes must be bound or app will crash, but you can make them optional too
It is possible to define an `ExternalRouteRef` as optional, so it is not required to bind it in the app.
useRouteRef can return undefined for optional external routes
-->
```tsx title="plugins/catalog/src/routes.ts"
import { createExternalRouteRef } from '@backstage/frontend-plugin-api';
export const createComponentExternalRouteRef = createExternalRouteRef({
// highlight-next-line
optional: true,
});
```
When calling `useRouteRef` with an optional external route, its return signature is changed to `RouteFunc | undefined`, and the returned value can be used to decide whether a certain link should be displayed or if an action should be taken:
```tsx title="plugins/catalog/src/components/IndexPage.tsx"
import React from 'react';
import { useRouteRef } from '@backstage/frontend-plugin-api';
import { createComponentExternalRouteRef } from '../routes';
export const IndexPage = () => {
const getCreateComponentPath = useRouteRef(createComponentExternalRouteRef);
return (
<div>
<h1>Index Page</h1>
{/* Rendering the link only if the getCreateComponentPath is defined */}
{/* highlight-start */}
{getCreateComponentPath && (
<a href={getCreateComponentPath()}>Create Component</a>
)}
{/* highlight-end */}
</div>
);
};
```
## Sub Route References
<!--
The last kind of route ref that can be created is a `SubRouteRef`, which can be used to create a route ref with a fixed path relative to an absolute `RouteRef`. They are useful if you have a page that internally is mounted at a sub route of a page extension component, and you want other plugins to be able to route to that page. And they can be a useful utility to handle routing within a plugin itself as well.
Explain the need for external route refs - both as a tool for routing within a plugin, but also allow external routes to point to sub routes
For example:
Talk about how sub routes declare an explicit path - can't be decided by the app/integrator. They are hard-coded in the plugin in parallel to the internal routing structure of the plugin itself.
```tsx title ="plugins/catalog/src/routes.ts"
import {
createRouteRef,
createSubRouteRef,
} from '@backstage/frontend-plugin-api';
Explain all of create, use and provide
-->
```ts
/*
Some examples
export const indexPageRouteRef = createRouteRef()
export const catalogPlugin = createPlugin({
id: 'catalog,
routes: {
index: indexPageRouteRef,
},
})
// in catalog plugin
import {indexPageRouteRef} from '../../routes
const link = useRouteRef(indexPageRouteRef)
// scaffolder
export const catalogIndexPageRouteRef = createExternalRouteRef({
defaultTarget: 'catalog/index',
})
export const scaffolderPlugin = createPlugin({
id: 'scaffolder,
externalRoutes: {
catalogIndex: catalogIndexPageRouteRef,
},
})
import {catalogIndexPageRouteRef} from '../../routes
const link = useRouteRef(catalogIndexPageRouteRef)
// app
import {catalogPlugin} from '@backstage/plugin-catalog'
const app = createApp({
bindRoutes({bind}) {
bind(scaffolderPlugin, {
catalogIndex: catalogPlugin.routes.index,
})
},
})
*/
export const indexRouteRef = createRouteRef();
// highlight-start
export const detailsSubRouteRef = createSubRouteRef({
parent: indexRouteRef,
path: '/details',
});
// highlight-end
```
There are substantial differences between creating subroutes and regular or external routes because subroutes are associated with regular routes, and the sub route path must be specified. The path string must include the parameters if this sub route has them:
```tsx title ="plugins/catalog/src/routes.ts"
// Omitting rest of the previous example file
export const detailsSubRouteRef = createSubRouteRef({
parent: indexRouteRef,
// highlight-next-line
path: '/:name/:namespace/:kind',
});
```
Using subroutes in a page extension is as simple as this:
```tsx title="plugins/catalog/src/components/IndexPage.ts"
import React from 'react';
import { Routes, Route, useLocation } from 'react-router-dom';
import { useRouteRef } from '@backstage/frontend-plugin-api';
import { indexRouteRef, detailsSubRouteRef } from '../routes';
import { DetailsPage } from './DetailsPage';
export const IndexPage = () => {
const { pathname } = useLocation();
const getIndexPath = useRouteRef(indexRouteRef);
const getDetailsPath = useRouteRef(detailsSubRouteRef);
return (
<div>
<h1>Index Page</h1>
{/* Linking to the details sub route */}
{pathname === getIndexPath() ? (
// highlight-start
<a
{/* Setting the details sub route params */}
href={getDetailsPath({
kind: 'component',
namespace: 'default',
name: 'foo',
})}
>
Show details
</a>
// highlight-end
) : (
// highlight-next-line
<a href={getIndexPath()}>Hide details</a>
)}
{/* Registering the details sub route */}
<Routes>
<Route path={detailsSubRouteRef.path} element={<DetailsPage />} />
</Routes>
</div>
);
};
```
This is how you can get the parameters of a sub route URL:
```tsx title="plugins/catalog/src/components/DetailsPage.ts"
import React from 'react';
import { useParams } from 'react-router-dom';
export const DetailsPage = () => {
// highlight-next-line
const params = useParams();
return (
<div>
<h1>Details Sub Page</h1>
<ul>
{/* highlight-start */}
<li>Kind: {params.kind}</li>
<li>Namespace: {params.namespace}</li>
<li>Name: {params.name}</li>
{/* highlight-end */}
</ul>
</div>
);
};
```
Finally, see how a plugin can provide subroutes:
```tsx title="plugins/catalog/src/plugin.ts"
import React from 'react';
import {
createPlugin,
createPageExtension,
} from '@backstage/frontend-plugin-api';
import { indexRouteRef, detailsSubRouteRef } from './routes';
const catalogIndexPage = createPageExtension({
defaultPath: '/entities',
routeRef: indexRouteRef,
loader: () => import('./components').then(m => <m.IndexPage />),
});
export default createPlugin({
id: 'catalog',
routes: {
index: indexRouteRef,
// highlight-next-line
details: detailsSubRouteRef,
},
extensions: [catalogIndexPage],
});
```
@@ -0,0 +1,222 @@
---
id: index
title: Building Frontend Plugins
sidebar_label: Overview
# prettier-ignore
description: Building frontend plugins using the new frontend system
---
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
This section covers how to build your own frontend [plugins](../architecture/04-plugins.md) and
[overrides](../architecture/05-extension-overrides.md). They are sometimes collectively referred to as
frontend _features_, and what you install to build up a Backstage frontend [app](../architecture/02-app.md).
## Creating a new plugin
This guide assumes that you already have a Backstage project set up. Even if you only want to develop a single plugin for publishing, we still recommend that you do so in a standard Backstage monorepo project, as you often end up needing multiple packages. For instructions on how to set up a new project, see our [getting started](../../getting-started/index.md#prerequisites) documentation.
To create a frontend plugin, run `yarn new`, select `plugin`, and fill out the rest of the prompts. This will create a new package at `plugins/<pluginId>`, which will be the main entrypoint for your plugin.
:::info
The created plugin will currently be templated for use in the legacy frontend system, and you will need to replace the existing plugin wiring code.
:::
## The plugin instance
The starting point of a frontend plugin is the `createPlugin` function, which accepts a single options object as its only parameter. It is imported from `@backstage/frontend-plugin-api`, which is where you will find most of the common APIs for building plugins.
This is how to create a minimal plugin:
```tsx title="in src/plugin.ts"
import { createPlugin } from '@backstage/frontend-plugin-api';
export const examplePlugin = createPlugin({
id: 'example',
extensions: [],
});
```
```tsx title="in src/index.ts"
export { examplePlugin as default } from './plugin';
```
Note that we export the plugin as the default export of our package from `src/index.ts`. This is important, as it is how users of our plugin are able to seamlessly install the plugin package in a Backstage app without having to reference the plugin instance through code.
The plugin ID should be a lowercase dash-separated string, while the plugin instance variable should be the camel case version of the ID with a `Plugin` suffix. For more details on naming patterns within the frontend system, see [the article on naming patterns](../architecture/08-naming-patterns.md). By sticking to these naming patterns you ensure that users of your plugin more easily recognize the exports and features provided by your plugin.
## Adding extensions
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/03-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/03-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/07-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.
```tsx title="in src/routes.ts"
import { createRouteRef } from '@backstage/frontend-plugin-api';
// Typically all routes are defined in src/routes.ts, in order to avoid circular imports.
// This will be the route reference for our example page. If you want to link
// to the page from somewhere else, you can use this reference to generate the target path.
export const rootRouteRef = createRouteRef();
```
```tsx title="in src/plugin.ts"
import {
createPlugin,
createPageExtension,
createNavItemExtension,
} from '@backstage/frontend-plugin-api';
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,
// 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 />),
});
// 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
});
// The same plugin as above, now with the extensions added
export const examplePlugin = createPlugin({
id: 'example',
extensions: [examplePage, exampleNavItem],
// We can also make routes available to other plugins.
// highlight-start
routes: {
root: rootRouteRef,
},
// highlight-end
});
```
What we've built here is a very common type of plugin. It's a top-level tool that provides a single page, along with a method for navigating to that page. The implementation of the page component, in this case the highlighted `ExamplePage`, can be arbitrarily complex. It can be anything from a single simple information page, to a full-blown application with multiple sub-pages.
We have also provided external access to our route reference by passing it to the plugin `routes` option. This makes it possible for app integrators to bind an external link from a different plugin to our plugin page. You can read more about how this works in the [External Route References](../architecture/07-routes.md#external-route-references) section.
## Utility APIs
Another type of extensions that is commonly used are [Utility APIs](../utility-apis/01-index.md). They can encapsulate shared pieces of functionality of your plugin, for example an API client for a backend service. You can optionally export your Utility API for other plugins to use, or allow integrators to replace the implementation of your Utility API with their own. For details on how to define and provide your own Utility API in your plugin, see the section on [creating Utility APIs](../utility-apis/02-creating.md).
What we'll show here is a complete example of a simple Utility API used only within the plugin itself:
```tsx title="src/api.ts - Defining an interface, API reference, and default implementation"
import { createApiRef } from '@backstage/frontend-plugin-api';
export interface ExampleApi {
getExample(): { example: string };
}
export const exampleApiRef = createApiRef<ExampleApi>({
id: 'plugin.example',
});
export class DefaultExampleApi implements ExampleApi {
getExample(): { example: string } {
return { example: 'Hello World!' };
}
}
```
```tsx title="src/components/ExamplePage.tsx - Using the API in our page component"
import { useApi } from '@backstage/frontend-plugin-api';
import { exampleApiRef } from '../api';
export function ExamplePage() {
// highlight-next-line
const exampleApi = useApi(exampleApiRef);
return (
<div>
<h1>Example Page</h1>
<p>Example: {exampleApi.getExample().example}</p>
</div>
);
}
```
```tsx title="in src/plugin.ts - Registering a factory for our API"
import {
createApiFactory,
createApiExtension,
} from '@backstage/frontend-plugin-api';
import { exampleApiRef, DefaultExampleApi } from './api';
// highlight-add-start
const exampleApi = createApiExtension({
factory: createApiFactory({
api: exampleApiRef,
deps: {},
factory: () => new DefaultExampleApi(),
}),
});
// highlight-add-end
/* Omitted definitions for examplePage, exampleNavItem, and rootRouteRef. */
export const examplePlugin = createPlugin({
id: 'example',
extensions: [
// highlight-add-next-line
exampleApi,
examplePage,
exampleNavItem,
],
routes: {
root: rootRouteRef,
},
});
```
## Plugin specific extensions
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';
// 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 />
)),
});
export const examplePlugin = createPlugin({
id: 'example',
extensions: [
// highlight-add-next-line
exampleEntityContent
exampleApi,
examplePage,
exampleNavItem,
],
routes: {
root: rootRouteRef,
},
});
```
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.
@@ -0,0 +1,59 @@
---
id: extension-types
title: Frontend System Extension Types
sidebar_label: Extension Types
# prettier-ignore
description: Extension types 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/03-extensions.md#extension-creators) available at your disposal when building Backstage frontend plugins.
## Built-in extension types
These are the extension types provided by the Backstage frontend framework itself.
### Api - [Reference](../../reference/frontend-plugin-api.createapiextension.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.
### NavItem - [Reference](../../reference/frontend-plugin-api.createnavitemextension.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 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)
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 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.
### Translation - [Reference](../../reference/frontend-plugin-api.createtranslationextension.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
These are the extension types 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.
### 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.
### 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.
+1 -1
View File
@@ -12,4 +12,4 @@ description: The Frontend System
The new frontend system is in alpha, and only a few plugins support the system so far. We do not yet recommend migrating any apps to the new system. If you add support for the new system to your plugin, please do so under a `/alpha` sub-path export.
You can find an example app setup in [the `app-next` package](https://github.com/backstage/backstage/tree/master/packages/app-next).
You can find an example app setup in the [`app-next` package](https://github.com/backstage/backstage/tree/master/packages/app-next).
+17
View File
@@ -71,3 +71,20 @@ In your `launch.json`, add a new entry with the following,
"console": "integratedTerminal"
},
```
### WebStorm
This section describes the process for enabling run configurations for Backstage in WebStorm.
Run configurations enable the use of debugging functionality such as steppers and breakpoints.
1. Select `Edit Configurations` in the `Run` dropdown menu. Click the plus sign to add a new
configuration, then select `Node.js`.
2. In `Working directory`, input `{PROJECT_DIR}/packages/backend`.
Replace `{PROJECT_DIR}` with the path to your Backstage repo.
3. In `JavaScript file`, input `{PROJECT_DIR}/node_modules/@backstage/cli/bin/backstage-cli`.
Replace `{PROJECT_DIR}` with the path to your Backstage repo.
4. In `Application parameters`, input `package start`.
5. Optionally, for `Environment Variables`, input `LOG_LEVEL=debug`.
6. Click `Apply` to save the changes.
7. With the newly-created configuration selected, use the `Run` or `Debug` icons on the
toolbar to execute the newly created configuration.
+1
View File
@@ -53,6 +53,7 @@ Additional steps for the main line release
- Add the release note file as [`/docs/releases/vx.y.0.md`](./releases)
- Add an entry to [`/microsite/sidebar.json`](https://github.com/backstage/backstage/blob/master/microsite/sidebars.json) for the release note
- Update the navigation bar item in [`/microsite/docusaurus.config.js`](https://github.com/backstage/backstage/blob/master/microsite/docusaurus.config.js) to point to the new release note
- Finally copy the content, without the metadata header, into the description of the [`Version Packages` Pull Request](https://github.com/backstage/backstage/pulls?q=is%3Aopen+is%3Apr+in%3Atitle+%22Version+Packages)
Once the release has been published edit the newly created release in the [GitHub repository](https://github.com/backstage/backstage/releases) and replace the text content with the release notes.
File diff suppressed because it is too large Load Diff
+69
View File
@@ -0,0 +1,69 @@
---
id: v1.22.0
title: v1.22.0
description: Backstage Release v1.22.0
---
These are the release notes for the v1.22.0 release of [Backstage](https://backstage.io/).
A huge thanks to the whole team of maintainers and contributors as well as the amazing Backstage Community for the hard work in getting this release developed and done.
## Highlights
### Updates to new frontend system
There have been several updates to alpha packages in the new frontend system including a breaking change where the `app/router` extension was renamed to `app/root`. Furthermore `elements`, `wrappers`, and `router` were added as inputs to `app/root` making it possible to pass extensions into the root of the app.
### Plugins and modules migrated to the New Backend System
Some more features have been migrated to the new backend system:
- `@backstage/plugin-auth-backend-module-microsoft-provider`
- `@backstage/plugin-auth-backend-module-pinniped-provider`
- `@backstage/plugin-catalog-backend-module-openapi`
- `@backstage/plugin-events-backend-module-azure`
- `@backstage/plugin-events-backend-module-bitbucket-cloud`
- `@backstage/plugin-events-backend-module-gerrit`
- `@backstage/plugin-linguist`
### New plugin: App Visualizer
This release includes the new `@backstage/plugin-app-visualizer` package. This plugin for the new frontend system lets you browse and view the extension structure of your app as a graph, detailed list, or in text form.
### New feature: Dynamic Feature Service
This release includes the new `@backstage/backend-dynamic-feature-service` package.
It is a new and experimental service that lets you dynamically detect and load local plugins and modules in your Backstage instance.
Contributed by [@davidfestal](https://github.com/davidfestal) in [#18862](https://github.com/backstage/backstage/pull/18862)
### New Scaffolder action `gitlab:issues:create`
You can now create GitHub issues in your scaffolder flows! Contributed by [@elaine-mattos](https://github.com/elaine-mattos) in [#21929](https://github.com/backstage/backstage/pull/21929)
### New Scaffolder action `gitlab:repo:push`
You can now push raw branches to GitLab in your scaffolder flows! Contributed by [@gavlyukovskiy](https://github.com/gavlyukovskiy) in [#21977](https://github.com/backstage/backstage/pull/21977)
## Security Fixes
This release does not contain any security fixes.
However, some updates were made to the build facilities in the CLI and the caches in the backend system, such that you can now perform builds on FIPS compliant systems. This may lead to some internal cache invalidation happening, since the hashing algorithms used were updated. This should not pose a problem unless caches were being used as reliable persistent storage systems. Please let us know if you encounter any issues that may be related to this.
## Upgrade path
We recommend that you keep your Backstage project up to date with this latest release. For more guidance on how to upgrade, check out the documentation for [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated).
## Links and References
Below you can find a list of links and references to help you learn about and start using this new release.
- [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/)
- [GitHub repository](https://github.com/backstage/backstage)
- Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy)
- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support
- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.22.0-changelog.md)
- Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins)
Sign up for our [newsletter](https://info.backstage.spotify.com/newsletter_subscribe) if you want to be informed about what is happening in the world of Backstage.
+1 -1
View File
@@ -56,7 +56,7 @@ For current known issues with the Material UI v5 migration, follow our [Mileston
To migrate your plugin to Material UI v5, you can build on the resources available.
1. Manually fix the imports from named to default imports to match the new [linting rules for minimizing bundle size](https://mui.com/material-ui/guides/minimizing-bundle-size).
1. Manually fix the imports from named to default imports to match the new [linting rules for minimizing bundle size](https://mui.com/material-ui/guides/minimizing-bundle-size). Note: you can use the [new `@backstage/no-top-level-material-ui-4-imports` ESLint](https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-top-level-material-ui-4-imports.md) rule to help with this.
2. Run the migration `codemod` for the path of the specific plugin: `npx @mui/codemod v5.0.0/preset-safe plugins/<path>`.
3. Take a look at possible `TODO:` items the `codemod` could not fix.
4. Remove types & methods from `@backstage/theme` which are marked as `@deprecated`.
+117
View File
@@ -0,0 +1,117 @@
---
id: react18-migration
title: Migrating to React 18
description: Additional resources for the Material UI v5 migration guide specifically for Backstage
---
The Backstage core libraries and plugins are compatible with all versions of React from v16.8 to v18. This means that you can migrate projects at your own pace. We do however encourage you to do so sooner rather than later, both to keep up with the evolving ecosystem, but also because React 18 brings performance improvements, in particular in tests.
## Migration
_Before diving in, this is a heads-up that for large projects this can be a tricky migration due to the fact that it is hard to break down into a gradual migration. In practice the difficult part of this migration is switching to the new version of the `@testing-library/react` package for tests, since there is no overlapping support across major React versions, more on that later._
### Switching to React 18
To switch a project to React 18, there are generally three changes that need to be made.
1. Update the resolutions in your root `package.json` to the new versions of `@types/react` and `@types/react-dom`:
```json title="package.json"
"resolutions": {
// highlight-remove-next-line
"@types/react": "^17",
// highlight-remove-next-line
"@types/react": "^17",
// highlight-add-next-line
"@types/react-dom": "^18",
// highlight-add-next-line
"@types/react-dom": "^18",
},
```
2. Update the `react` and `react-dom` dependencies in your `packages/app/package.json` to the new versions:
```json title="packages/app/package.json"
"dependencies": {
...
// highlight-remove-next-line
"react": "^17.0.2",
// highlight-remove-next-line
"react-dom": "^17.0.2",
// highlight-add-next-line
"react": "^18.0.2",
// highlight-add-next-line
"react-dom": "^18.0.2",
...
},
```
3. Update `packages/app/src/index.tsx` to use the new `react-dom/client` API to render the app:
```tsx title="packages/app/src/index.tsx"
import '@backstage/cli/asset-types';
import React from 'react';
// highlight-remove-next-line
import ReactDOM from 'react-dom';
// highlight-add-next-line
import ReactDOM from 'react-dom/client';
import App from './App';
// highlight-remove-next-line
ReactDOM.render(<App />, document.getElementById('root'));
// highlight-add-next-line
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
```
Once these steps are done you should be able to run your app and see it working as before, except now using React 18.
### TypeScript Errors
When upgrading to React 18 you are likely to see a fair number of TypeScript type errors. A summary of the breaking changes can be found in the [Pull Request that introduced them](https://github.com/DefinitelyTyped/DefinitelyTyped/pull/56210). A codemod is also provided to help with the migration.
Run `yarn tsc:full` to asses the damage.
The good news is that these errors can be fixed while still staying on React 17. If you have a large number of errors to fix you can address as few or many is you like at a time and merge them into your main branch **without** the version bumps from step 1. This lets you gradually migrate the types in your project while not yet fully moving over to React 18. Once all type breakages are fixed you can move on to the next step of migrating tests.
### Migrating Tests
At this point the app hopefully works and you have no type errors, but if you run your tests you may see that a lot of them are failing. This is because the current version of the `@testing-library/react` package does not support React 18. Unfortunately the new version that we will be moving to does not support React 17, which is why we need to do this all at once.
:::info
If migrating your entire project at once is not feasible, you can try to add `devDependencies` for `react` and `react-dom` v17 to individual plugins to be migrated later. This is not something we have tried ourselves in practice, so let us know in the community Discord if you attempt this and how it goes.
:::
#### Dependency Upgrades
To get the tests working again we need to update `@testing-library/react` to at least v13, although while at it is sensible to move at least all the way to v14 since the additional breaking changes have low impact. For more information on the changes in v13, see the [release notes](https://github.com/testing-library/react-testing-library/releases/tag/v13.0.0).
In addition to bumping `@testing-library/react` you also need to remove the `@testing-library/react-hooks` package, since it is now included in `@testing-library/react` itself. You can find more information on this change in the `@testing-library/react-hooks` [README.md](https://github.com/testing-library/react-hooks-testing-library?tab=readme-ov-file#a-note-about-react-18-support).
The following search-and-replace RegEx patterns may by helpful in updating your `package.json` files:
Find: `"@testing-library/react": ".*"`<br>
Replace: `"@testing-library/react": "^14.0.0"`
Find: `"@testing-library/react-hooks": ".*",?`<br>
Replace: `<nothing>`
#### Test Updates
Once you have installed the new versions of the dependencies this turns into a fairly mechanical process of updating the tests. Use your own favorite method for this, running all tests once to find the breakages and then focusing on one test file at a time was fairly smooth.
When updating the tests in the Backstage project we found the following patterns to be useful:
- Many existing `act(...)` calls can be removed, it is built into most testing utilities like `waitFor`, `.findBy*`, and `@testing-library/user-event`.
- Use `.findBy*` to wait for elements to appear.
- Use `waitFor(...)` to wait for any other expected state changes or multiple elements.
- Use `@testing-library/user-event` for user interactions.
- The `renderHook` API has changes in several ways:
- It no longer returns `waitForValueToChange` or `waitForNextUpdate`, you'll likely want to use `waitFor` instead.
- It now throws errors rather than returns them as part of the result.
- It no longer forwards `initialProps` to the `wrapper`, a workaround for this is provided in the [docs](https://testing-library.com/docs/react-testing-library/api/#renderhook-options-initialprops).
- Waiting for mock functions to be called by a component and then expecting render state to be updated is no longer reliable.
- Rendered components often don't immediately update on user input, it's more common to need to use `waitFor` or other utilities to wait for the expected state to be reached.
You can also refer to the test changes in this [PR](https://github.com/backstage/backstage/pull/20598/files?file-filters%5B%5D=.ts&file-filters%5B%5D=.tsx), which was the migration to React 18 for the Backstage project itself.
Best of luck! For question please join the [community Discord](https://discord.gg/backstage-687207715902193673). If you think this documentation could be improved we welcome you to [open an issue](https://github.com/backstage/backstage/issues/new/choose) or submit a pull request.