Merge branch 'master' into catalog/github-app-discovery
This commit is contained in:
@@ -129,3 +129,24 @@ Responds with
|
||||
- `200 OK` if successful
|
||||
- `404 Not Found` if there was no such registered task for this plugin
|
||||
- `409 Conflict` if the task was already in a running state
|
||||
|
||||
## Testing
|
||||
|
||||
The `@backstage/backend-test-utils` package provides `mockServices.scheduler`, which provides a mocked implementation of the scheduler service that can be used in tests. This mocked implementation is used by default in `startTestBackend`, and it will immediately run any registered tasks on startup as long as they're not configured to run manually or with an initial delay.
|
||||
|
||||
A dedicated instance can be used for more control during testing:
|
||||
|
||||
```ts
|
||||
it('should trigger a task', async () => {
|
||||
const scheduler = mockServices.scheduler();
|
||||
|
||||
const { server } = await startTestBackend({
|
||||
features: [scheduler.factory()],
|
||||
});
|
||||
|
||||
await scheduler.triggerTask('some-task-id');
|
||||
|
||||
// Next verify that the plugin state is updated accordingly
|
||||
// e.g. by calling the API or verifying database state
|
||||
});
|
||||
```
|
||||
|
||||
@@ -87,6 +87,7 @@ import {
|
||||
ClusterDetails,
|
||||
KubernetesClustersSupplier,
|
||||
kubernetesClusterSupplierExtensionPoint,
|
||||
kubernetesServiceLocatorExtensionPoint,
|
||||
} from '@backstage/plugin-kubernetes-node';
|
||||
|
||||
export class CustomClustersSupplier implements KubernetesClustersSupplier {
|
||||
@@ -119,12 +120,26 @@ export const kubernetesModuleCustomClusterDiscovery = createBackendModule({
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
kubernetes: kubernetesClusterSupplierExtensionPoint,
|
||||
clusterSupplier: kubernetesClusterSupplierExtensionPoint,
|
||||
serviceLocator: kubernetesServiceLocatorExtensionPoint,
|
||||
},
|
||||
async init({ kubernetes }) {
|
||||
kubernetes.addClusterSupplier(
|
||||
async init({ clusterSupplier, serviceLocator }) {
|
||||
// simple replace of the internal dependency
|
||||
clusterSupplier.addClusterSupplier(
|
||||
CustomClustersSupplier.create(Duration.fromObject({ minutes: 60 })),
|
||||
);
|
||||
|
||||
// there's also the ability to get access to some of the default implementations of the extension points where
|
||||
// neccessary:
|
||||
serviceLocator.addServiceLocator(
|
||||
async ({ getDefault, clusterSupplier }) => {
|
||||
// get access to the default service locator:
|
||||
const defaultImplementation = await getDefault();
|
||||
|
||||
// build your own with the clusterSupplier dependency:
|
||||
return new MyNewServiceLocator({ clusterSupplier });
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -170,7 +170,7 @@ export const exampleCustomCatalogFiltering = createBackendModule({
|
||||
|
||||
Here are some of the known Search Collators available in from the Backstage Community:
|
||||
|
||||
- [`@backstage/plugin-search-backend-module-explore`](https://github.com/backstage/backstage/tree/master/plugins/search-backend-module-explore): will index content from the [Explore plugin](https://github.com/backstage/community-plugins/tree/main/workspaces/explore/plugins/explore).
|
||||
- [`@backstage-community/plugin-search-backend-module-explore`](https://github.com/backstage/community-plugins/tree/main/workspaces/explore/plugins/search-backend-module-explore): will index content from the [Explore plugin](https://github.com/backstage/community-plugins/tree/main/workspaces/explore/plugins/explore).
|
||||
- [`@backstage/plugin-search-backend-module-stack-overflow-collator`](https://github.com/backstage/backstage/tree/master/plugins/search-backend-module-stack-overflow-collator): will index content from Stack Overflow.
|
||||
- [`@backstage-community/search-backend-module-adr`](https://github.com/backstage/community-plugins/tree/main/workspaces/adr/plugins/search-backend-module-adr): will index content from the [ADR plugin](https://github.com/backstage/community-plugins/tree/main/workspaces/adr/plugins/adr).
|
||||
|
||||
|
||||
@@ -125,6 +125,15 @@ all of the processors' contributions to one step are run in the order that the
|
||||
processors were registered, then all of their contributions to the next step in
|
||||
the same order, and so on.
|
||||
|
||||
:::note Technical note
|
||||
|
||||
Processors registered from the same catalog module always run
|
||||
in the registration order, but this does not apply over multiple catalog modules as
|
||||
the order of registration depends on the order in which the modules are loaded
|
||||
by the framework.
|
||||
|
||||
:::
|
||||
|
||||
Each step has the opportunity to optionally modify the entity, and to optionally
|
||||
emit other information. For example, the processor might look at information in
|
||||
the `spec` field of the entity, and emit relations that correspond to those
|
||||
@@ -182,6 +191,19 @@ The diagram shows how the stitcher reads from several sources:
|
||||
The last part is noteworthy: This is how the stitcher is able to collect all of
|
||||
the relation edges, both incoming and outgoing, no matter who produced them.
|
||||
|
||||
:::note Technical note
|
||||
|
||||
Whether the entity will be stitched depends on the entity hash value,
|
||||
which is calculated based on the entity body, relations, errors, referred entities,
|
||||
and entity parents after the processing. The hash value changes if any of these components
|
||||
change. The hash value is then compared to hash value from the previous processing of the
|
||||
entity and if they are different, the entity is stitched again.
|
||||
In entity body, changes in array order, for example `metadata.tags`, changes the hash
|
||||
value. It is a good idea to monitor the stitched entities count in the catalog as it
|
||||
might cause performance issues if the number of entities gets too high.
|
||||
|
||||
:::
|
||||
|
||||
The stitching is currently a fixed process, that cannot be modified or extended.
|
||||
This means that any modifications you want to make on the final result, has to
|
||||
happen during ingestion or processing.
|
||||
|
||||
@@ -127,20 +127,42 @@ Start by using the `yarn backstage-cli new` command to generate a scaffolder mod
|
||||
```
|
||||
$ yarn backstage-cli new
|
||||
? What do you want to create?
|
||||
> backend-module - A new backend module that extends an existing backend plugin with additional features
|
||||
frontend-plugin - A new frontend plugin
|
||||
backend-plugin - A new backend plugin
|
||||
plugin - A new frontend plugin
|
||||
node-library - A new node-library package, exporting shared functionality for backend plugins and modules
|
||||
plugin-common - A new isomorphic common plugin package
|
||||
plugin-node - A new Node.js library plugin package
|
||||
plugin-react - A new web library plugin package
|
||||
scaffolder-module - An module exporting custom actions for @backstage/plugin-scaffolder-backend
|
||||
❯ backend-plugin-module - A new backend module that extends an existing backend plugin
|
||||
plugin-web-library - A new web library plugin package
|
||||
plugin-node-library - A new Node.js library plugin package
|
||||
plugin-common-library - A new isomorphic common plugin package
|
||||
web-library - A library package, exporting shared functionality for web environments
|
||||
```
|
||||
|
||||
When prompted, select the option to generate a backend module.
|
||||
Since we want to extend the Scaffolder backend, enter `scaffolder` when prompted for the plugin to extend.
|
||||
Next, enter a name for your module (relative to the generated `scaffolder-backend-module-` prefix),
|
||||
and the CLI will generate the required files and directory structure.
|
||||
When prompted, use the arrow keys to select the option to generate a `backend-plugin-module`.
|
||||
Since we want to extend the Scaffolder backend, enter `scaffolder` when prompted for the ID of the plugin to extend.
|
||||
Next, enter the ID (name) of your module. This will be appended to the `scaffolder-backend-module-` prefix. The CLI will then generate the required files and directory structure, for example:
|
||||
|
||||
```
|
||||
? Enter the ID of the plugin [required] scaffolder
|
||||
? Enter the ID of the module [required] foo-bar
|
||||
templating plugins/scaffolder-backend-module-foo-bar ✔
|
||||
backend adding @internal/plugin-scaffolder-backend-module-foo-bar ✔
|
||||
executing yarn install ✔
|
||||
executing yarn lint --fix ✔
|
||||
|
||||
🎉 Successfully created backend-plugin-module
|
||||
```
|
||||
|
||||
**Directory Structure**
|
||||
|
||||
```
|
||||
plugins
|
||||
├── README.md
|
||||
├── scaffolder-backend-module-foo-bar
|
||||
│ ├── package.json
|
||||
│ ├── README.md
|
||||
│ └── src
|
||||
│ ├── index.ts
|
||||
│ └── module.ts
|
||||
```
|
||||
|
||||
## Writing your Module
|
||||
|
||||
@@ -148,8 +170,8 @@ Once the CLI has generated the essential structure for your new scaffolder
|
||||
module, it's time to implement our templating extensions. Here we'll demonstrate
|
||||
how to create each of the supported extension types.
|
||||
|
||||
`src/module.ts` is where the magic happens. First we prepare to utilize the
|
||||
associated (_**alpha** phase_) API extension point by adding:
|
||||
`src/module.ts` is where the magic happens. First, we prepare to utilize the
|
||||
associated (_**alpha** phase_) API extension point by adding the below import to `src/module.ts`:
|
||||
|
||||
```ts
|
||||
import { scaffolderTemplatingExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
|
||||
|
||||
@@ -5,8 +5,6 @@ sidebar_label: Overview
|
||||
description: The structure and architecture of the new Frontend System
|
||||
---
|
||||
|
||||
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
|
||||
|
||||
## Building Blocks
|
||||
|
||||
This section introduces the high-level building blocks upon which this new
|
||||
|
||||
@@ -5,8 +5,6 @@ sidebar_label: App
|
||||
description: App instances
|
||||
---
|
||||
|
||||
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
|
||||
|
||||
## The App Instance
|
||||
|
||||
The app instance is the main entry point for creating a frontend app. It doesn't do much on its own, but is instead responsible for wiring things together that have been provided as features from other parts of the system.
|
||||
|
||||
@@ -5,8 +5,6 @@ sidebar_label: Plugins
|
||||
description: Frontend plugins
|
||||
---
|
||||
|
||||
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
|
||||
|
||||
## Introduction
|
||||
|
||||
Frontend plugins are a foundational building block in Backstage and the frontend system. They are used to encapsulate and provide functionality for a Backstage app, such as new pages, navigational elements, and APIs; as well as extensions and features for other plugins, such as entity page cards and content for the Software Catalog, or result list items for the search plugin.
|
||||
|
||||
@@ -5,8 +5,6 @@ sidebar_label: Extensions
|
||||
description: Frontend extensions
|
||||
---
|
||||
|
||||
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
|
||||
|
||||
As mentioned in the [previous section](./10-app.md), Backstage apps are built up from a tree of extensions. This section will go into more detail about what extensions are, how to create and use them, and how to create your own extensibility patterns.
|
||||
|
||||
## Extension Structure
|
||||
|
||||
@@ -5,8 +5,6 @@ sidebar_label: Extensions Blueprints
|
||||
description: Frontend extensions
|
||||
---
|
||||
|
||||
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
|
||||
|
||||
The `createExtension` function and related APIs is considered a low-level building and fairly advanced building block, and is not typically what you would use when building plugins and features. Instead, the core APIs and plugins provide extension blueprints that makes it easier to create extensions for specific usages. These blueprints accept a number of parameters that is up to each blueprint to define, and then creates a new extension using the provided parameters. New blueprints are created using the `createExtensionBlueprint` function, and are by convention exported with the symbol `<Kind>Blueprint`. If you are curious about what blueprints are available from a plugin or package, look for `*Blueprint` exports in the package's API, for plugins these are typically found in the `*-react` package.
|
||||
|
||||
## Creating an extension from a blueprint
|
||||
|
||||
@@ -5,8 +5,6 @@ sidebar_label: Extension Overrides
|
||||
description: Frontend extension overrides
|
||||
---
|
||||
|
||||
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
|
||||
|
||||
## Introduction
|
||||
|
||||
An important customization point in the frontend system is the ability to override existing extensions. It can be used for anything from slight tweaks to the extension logic, to completely replacing an extension with a custom implementation. While extensions are encouraged to make themselves configurable, there are many situations where you need to override an extension to achieve the desired behavior. The ability to override extensions should be kept in mind when building plugins, and can be a powerful tool to allow for deeper customizations without the need to re-implement large parts of the plugin.
|
||||
|
||||
@@ -5,8 +5,6 @@ sidebar_label: Value References
|
||||
description: Value References
|
||||
---
|
||||
|
||||
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
|
||||
|
||||
<!--
|
||||
|
||||
Describe the generic concept of references as used for ApiRef, ExtensionDataRef, RouteRef, ComponentRef, etc.
|
||||
|
||||
@@ -5,10 +5,6 @@ sidebar_label: Utility APIs
|
||||
description: Utility APIs
|
||||
---
|
||||
|
||||
:::note Note
|
||||
The new frontend system is in alpha and is only supported by a small number of plugins.
|
||||
:::
|
||||
|
||||
## Overview
|
||||
|
||||
Utility APIs are pieces of standalone functionality, interfaces that can be requested by plugins to use. They are defined by a TypeScript interface as well as a reference (an "API ref") used to access its implementation. They can be provided both by plugins and the core framework, and are themselves [extensions](../architecture/20-extensions.md) that can have inputs, be replaced, and be declaratively configured in your app-config.
|
||||
|
||||
@@ -5,8 +5,6 @@ sidebar_label: Routes
|
||||
description: Frontend routes
|
||||
---
|
||||
|
||||
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
|
||||
|
||||
## 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.
|
||||
@@ -279,7 +277,7 @@ Another thing to note is that this indirection in the routing is particularly us
|
||||
|
||||
### Default Targets for External Route References
|
||||
|
||||
It is possible to define a default target for an external route reference, potentially removing the need to bind the route in the app. This reduces the need for configuration when installing new plugins through providing a sensible default. It is of course still possible to override the route binding in the app.
|
||||
It is possible to define a default target for an external route reference, potentially removing the need to bind the route in the app. This reduces the need for configuration when installing new plugins through providing a sensible default. It is of course still possible to override the route binding in the app, as long as the external route ref is exported via the `externalRoutes` property of the plugin instance.
|
||||
|
||||
The default target uses the same syntax as the route binding configuration, and will only be used if the target plugin and route exist. For example, this is how the catalog can define a default target for the create component external route in a way that removes the need for the binding in the previous example:
|
||||
|
||||
@@ -422,7 +420,7 @@ export default createFrontendPlugin({
|
||||
|
||||
## Route Aliases - Overriding Routed Extensions in Modules
|
||||
|
||||
It is possible to [override extensions of a plugin using a module](./25-extension-overrides.md#creating-a-frontend-module). In some cases the extension you're overriding may require a route reference. You could import import the plugin instance and access the it via the `routes` property, but this creates a direct dependency on the plugin and risks leading to package duplication issues that would also break the route reference.
|
||||
It is possible to [override extensions of a plugin using a module](./25-extension-overrides.md#creating-a-frontend-module). In some cases the extension you're overriding may require a route reference. You could import the plugin instance and access the it via the `routes` property, but this creates a direct dependency on the plugin and risks leading to package duplication issues that would also break the route reference.
|
||||
|
||||
Instead of accessing the route reference directly, you can create a new route reference that acts as an alias for the original one from the plugin. For example, you can override the catalog index page with a custom one like this:
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ sidebar_label: Naming Patterns
|
||||
description: Naming patterns in the frontend system
|
||||
---
|
||||
|
||||
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
|
||||
|
||||
These are the naming patterns to adhere to within the frontend system. They help us keep exports and IDs consistent across packages and make it easier to understand the usage and intent of exports and IDs.
|
||||
|
||||
As a rule, all names should be camel case, with the exceptions of plugin and extension IDs, which should use kebab case.
|
||||
|
||||
@@ -5,8 +5,6 @@ sidebar_label: Changelog
|
||||
description: Changelog documentation for different versions of the frontend system core APIs.
|
||||
---
|
||||
|
||||
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
|
||||
|
||||
## Introduction
|
||||
|
||||
This section provides migration guides for different versions of the frontend system core APIs. Each guide will provide a summary of the changes that have been made for a particular Backstage release, and how to update your usage of the core APIs.
|
||||
|
||||
@@ -5,8 +5,6 @@ sidebar_label: Overview
|
||||
description: Building frontend apps using the new frontend system
|
||||
---
|
||||
|
||||
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
|
||||
|
||||
To get set up quickly with your own Backstage project you can create a Backstage App.
|
||||
|
||||
A Backstage App is a monorepo setup that includes everything you need to run Backstage in your own environment.
|
||||
|
||||
@@ -9,7 +9,53 @@ 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.
|
||||
|
||||
:::warning
|
||||
|
||||
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 we recommend planning 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
|
||||
|
||||
:::info
|
||||
|
||||
If you encounter issues, check [GitHub issues](https://github.com/backstage/backstage/issues) or ask in [Discord](https://discord.gg/backstage-687207715902193673).
|
||||
|
||||
:::
|
||||
|
||||
## 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:
|
||||
|
||||
@@ -17,7 +63,7 @@ To start we'll need to add the new `@backstage/frontend-defaults` package:
|
||||
yarn --cwd packages/app add @backstage/frontend-defaults
|
||||
```
|
||||
|
||||
The next step in migrating an app is to switch out the `createApp` function for the new one from `@backstage/frontend-defaults`:
|
||||
The next step is to switch out the `createApp` function for the new one from `@backstage/frontend-defaults`:
|
||||
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
// highlight-remove-next-line
|
||||
@@ -26,23 +72,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 `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 `convertLegacyApp` 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 `convertLegacyApp`. 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 convertedOptionsModule = 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: [
|
||||
// ...
|
||||
convertedOptionsModule,
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
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: [
|
||||
// ...
|
||||
convertedOptionsModule,
|
||||
],
|
||||
// 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 +215,11 @@ Migrate it to the following:
|
||||
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
import {
|
||||
convertLegacyApp,
|
||||
convertLegacyAppOptions,
|
||||
// ...
|
||||
convertLegacyAppRoot,
|
||||
} from '@backstage/core-compat-api';
|
||||
|
||||
const legacyFeatures = convertLegacyApp(
|
||||
const convertedRootFeatures = convertLegacyAppRoot(
|
||||
<>
|
||||
<AlertDisplay />
|
||||
<OAuthRequestDialog />
|
||||
@@ -74,18 +229,19 @@ const legacyFeatures = convertLegacyApp(
|
||||
</>,
|
||||
);
|
||||
|
||||
const optionsModule = convertLegacyAppOptions({
|
||||
/* other options such as apis, plugins, components, and themes */
|
||||
});
|
||||
|
||||
const app = createApp({
|
||||
features: [optionsModule, ...legacyFeatures],
|
||||
features: [
|
||||
// ...
|
||||
...convertedRootFeatures,
|
||||
],
|
||||
});
|
||||
|
||||
export default app.createRoot();
|
||||
```
|
||||
|
||||
We've taken all the elements that were previously passed to `app.createRoot(...)`, and instead passed them to `convertLegacyApp(...)`. We then pass the features returned by `convertLegacyApp` and forward them to the `features` option of the new `createApp`.
|
||||
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:
|
||||
|
||||
@@ -103,6 +259,8 @@ ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(app);
|
||||
```
|
||||
|
||||
### 5) Updating the app test file
|
||||
|
||||
You'll also need to make similar changes to your `App.test.tsx` file as well:
|
||||
|
||||
```tsx
|
||||
@@ -139,11 +297,13 @@ describe('App', () => {
|
||||
});
|
||||
```
|
||||
|
||||
Then you'll want to follow the section on [migrating `bindRoutes`](#bindroutes).
|
||||
## Phase 2: Complete Transition to the New Frontend System
|
||||
|
||||
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`.
|
||||
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).
|
||||
|
||||
## Migrating `createApp` Options
|
||||
At this point, the contents of your app should have moved past the initial migration stage. Let's continue by gradually removing legacy code and helpers to fully adopt 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.
|
||||
|
||||
@@ -162,6 +322,7 @@ import { createFrontendModule } from '@backstage/frontend-plugin-api';
|
||||
|
||||
const app = createApp({
|
||||
features: [
|
||||
// ...
|
||||
// highlight-add-start
|
||||
createFrontendModule({
|
||||
pluginId: 'app',
|
||||
@@ -174,7 +335,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 +371,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 +408,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 +443,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 +496,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 +542,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 +560,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,17 +586,17 @@ 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.
|
||||
|
||||
Note that if you are binding routes from a legacy plugin that was converted using `convertLegacyApp`, you will need to use the `convertLegacyRouteRefs` and/or `convertLegacyRouteRef` to convert the routes to be compatible with the new system.
|
||||
Note that if you are binding routes from a legacy plugin that was converted using `convertLegacyAppRoot`, 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
|
||||
const app = createApp({
|
||||
features: convertLegacyApp(...),
|
||||
features: convertLegacyAppRoot(...),
|
||||
bindRoutes({ bind }) {
|
||||
bind(convertLegacyRouteRefs(catalogPlugin.externalRoutes), {
|
||||
createComponent: convertLegacyRouteRef(scaffolderPlugin.routes.root),
|
||||
@@ -444,7 +605,7 @@ const app = createApp({
|
||||
});
|
||||
```
|
||||
|
||||
### `__experimentalTranslations`
|
||||
#### `__experimentalTranslations`
|
||||
|
||||
Translations are now installed as extensions, created using `TranslationBlueprint`.
|
||||
|
||||
@@ -488,39 +649,162 @@ 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 `convertLegacyApp` 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 = convertLegacyApp(
|
||||
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 you can remove all surrounding elements and just keep the `routes`:
|
||||
|
||||
```tsx title="in packages/app/src/App.tsx"
|
||||
const legacyFeatures = convertLegacyApp(routes);
|
||||
const convertedRootFeatures = convertLegacyAppRoot(routes);
|
||||
```
|
||||
|
||||
This will remove many extension overrides that `convertLegacyApp` 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 `convertLegacyApp(...)` 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:
|
||||
|
||||
```tsx
|
||||
const convertedRootFeatures = convertLegacyAppRoot(
|
||||
<>
|
||||
<AlertDisplay transientTimeoutMs={2500} />
|
||||
<OAuthRequestDialog />
|
||||
<AppRouter>
|
||||
{/* highlight-next-line */}
|
||||
<CustomAppBarrier>
|
||||
<Root>{routes}</Root>
|
||||
{/* highlight-next-line */}
|
||||
</CustomAppBarrier>
|
||||
</AppRouter>
|
||||
</>,
|
||||
);
|
||||
```
|
||||
|
||||
Any app root wrapper needs to be migrated to be an extension, created using `AppRootWrapperBlueprint`. Note that if you have multiple wrappers they must be completely independent of each other, i.e. the order in which they the appear in the React tree should not matter. If that is not the case then you should group them into a single wrapper.
|
||||
|
||||
Here is an example converting the `CustomAppBarrier` into extension:
|
||||
|
||||
```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,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
#### 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.
|
||||
|
||||
In order to migrate your existing sidebar, you will want to create an override for the `app/nav` extension. You can do this by copying the standard of having a `src/modules/nav/` folder, which can contain an extension which you can install into the `app` in the form of a `module`.
|
||||
|
||||
```tsx title="in packages/app/src/modules/nav/index.ts"
|
||||
import { createFrontendModule } from '@backstage/frontend-plugin-api';
|
||||
import { SidebarContent } from './Sidebar';
|
||||
|
||||
export const navModule = createFrontendModule({
|
||||
pluginId: 'app',
|
||||
extensions: [SidebarContent],
|
||||
});
|
||||
```
|
||||
|
||||
Then in the actual implementation for the `SidebarContent` extension, you can provide something like the following, where the component that is passed to the `compatWrapper` is the entire `Sidebar` component from your `Root` component.
|
||||
|
||||
The `compatWrapper` is there to ensure that any legacy plugins using things like `useRouteRef` work well in the new system, so if you run into some errors which look like compatibility issues, make sure that this wrapper is used in the relevant places.
|
||||
|
||||
```tsx title="in packages/app/src/modules/nav/Sidebar.tsx"
|
||||
import { compatWrapper } from '@backstage/core-compat-api';
|
||||
import { NavContentBlueprint } from '@backstage/frontend-plugin-api';
|
||||
|
||||
export const SidebarContent = NavContentBlueprint.make({
|
||||
params: {
|
||||
component: ({ items }) =>
|
||||
compatWrapper(
|
||||
<Sidebar>
|
||||
<SidebarLogo />
|
||||
<SidebarGroup label="Search" icon={<SearchIcon />} to="/search">
|
||||
<SidebarSearchModal />
|
||||
</SidebarGroup>
|
||||
<SidebarDivider />
|
||||
<SidebarGroup label="Menu" icon={<MenuIcon />}>
|
||||
...
|
||||
</SidebarGroup>
|
||||
<SidebarGroup label="Plugins">
|
||||
<SidebarScrollWrapper>
|
||||
{/* Items in this group will be scrollable if they run out of space */}
|
||||
{items.map((item, index) => (
|
||||
<SidebarItem {...item} key={index} />
|
||||
))}
|
||||
</SidebarScrollWrapper>
|
||||
</SidebarGroup>
|
||||
</Sidebar>,
|
||||
),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The `items` property is a list of all extensions provided by the `NavItemBlueprint` that are currently installed in the App. If you don't want to auto populate this list you can simply remove the rendering of that `SidebarGroup`, but otherwise you can see from the above example how a `SidebarItem` element is rendered for each of the items in the list.
|
||||
|
||||
You might also notice that when you're rendering additional fixed icons for plugins that these might become duplicated as the plugin provides a `NavItem` extension and you're also rendering one in the `Sidebar` manually. In order to remove the item from the list of `items` which is passed through, we recommend that you disable that extension using config:
|
||||
|
||||
```yaml title="in app-config.yaml"
|
||||
app:
|
||||
extensions:
|
||||
- nav-item:search: false
|
||||
- nav-item:catalog: false
|
||||
```
|
||||
|
||||
You can also determine the order of the provided auto installed `NavItems` that you get from the system in config. The below example ensures that the `catalog` navigation item will proceed the `search` navigation item when being passed through as the `item` prop.
|
||||
|
||||
```yaml title="in app-config.yaml"
|
||||
app:
|
||||
extensions:
|
||||
- nav-item:catalog
|
||||
- nav-item:search
|
||||
```
|
||||
|
||||
#### App Root Routes
|
||||
|
||||
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:
|
||||
|
||||
@@ -569,19 +853,25 @@ const routes = (
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
### Entity Pages
|
||||
### Migrating core, internal and third-party plugins
|
||||
|
||||
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 `convertLegacyApp` 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 `convertLegacyApp`.
|
||||
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.
|
||||
|
||||
To start the gradual migration of entity pages, add your `entityPages` to the `convertLegacyApp` call:
|
||||
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 legacyFeatures = convertLegacyApp(routes);
|
||||
const convertedRootFeatures = convertLegacyAppRoot(routes);
|
||||
/* highlight-add-next-line */
|
||||
const legacyFeatures = convertLegacyApp(routes, { entityPage });
|
||||
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/>`.
|
||||
@@ -614,9 +904,9 @@ import { default as catalogPlugin } from '@backstage/plugin-catalog/alpha';
|
||||
|
||||
const app = createApp({
|
||||
/* highlight-remove-next-line */
|
||||
features: [optionsModule, ...legacyFeatures],
|
||||
features: [convertedOptionsModule, ...convertedRootFeatures],
|
||||
/* highlight-add-next-line */
|
||||
features: [catalogPlugin, optionsModule, ...legacyFeatures],
|
||||
features: [catalogPlugin, convertedOptionsModule, ...convertedRootFeatures],
|
||||
});
|
||||
```
|
||||
|
||||
@@ -642,9 +932,13 @@ const catalogPluginOverride = catalogPlugin.withOverrides({
|
||||
|
||||
const app = createApp({
|
||||
/* highlight-remove-next-line */
|
||||
features: [catalogPlugin, optionsModule, ...legacyFeatures],
|
||||
features: [catalogPlugin, convertedOptionsModule, ...convertedRootFeatures],
|
||||
/* highlight-add-next-line */
|
||||
features: [catalogPluginOverride, optionsModule, ...legacyFeatures],
|
||||
features: [
|
||||
catalogPluginOverride,
|
||||
convertedOptionsModule,
|
||||
...convertedRootFeatures,
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
@@ -652,96 +946,68 @@ At this point you should be able to run the app and see that you're not using th
|
||||
|
||||
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.
|
||||
|
||||
### Sidebar
|
||||
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.
|
||||
|
||||
New apps feature a built-in sidebar extension (`app/nav`) that will render all nav item extensions provided by plugins. This is a placeholder implementation and not intended as a long-term solution. In the future we will aim to provide a more flexible sidebar extension that allows for more customization out of the box.
|
||||
## Enable the new templates for `yarn new`
|
||||
|
||||
Because the built-in sidebar is quite limited you may want to override the sidebar with your own custom implementation. To do so, use `createExtension` directly and refer to the [original sidebar implementation](https://github.com/backstage/backstage/blob/master/plugins/app/src/extensions/AppNav.tsx). The following is an example of how to take your existing sidebar from the `Root` component that you typically find in `packages/app/src/components/Root.tsx`, and use it in an [extension override](../architecture/25-extension-overrides.md):
|
||||
It's encouraged that once you switch over to using the new frontend system, that new plugins that you create are using the new frontend system. This means that you're not instantly creating legacy plugins that will eventually need migration.
|
||||
|
||||
```tsx
|
||||
const nav = createExtension({
|
||||
namespace: 'app',
|
||||
name: 'nav',
|
||||
attachTo: { id: 'app/layout', input: 'nav' },
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory({ inputs }) {
|
||||
return [
|
||||
coreExtensionData.reactElement(
|
||||
<Sidebar>
|
||||
{/* Sidebar contents from packages/app/src/components/Root.tsx go here */}
|
||||
</Sidebar>,
|
||||
),
|
||||
];
|
||||
This practice is also pretty important early on, as it's going to help you get familiar with the practices of the new frontend system.
|
||||
|
||||
When creating a new Backstage app with `create-app` and using the `--next` flag you'll automatically get these choices in the `yarn new` command, but if you want to bring these templates to an older app, you can add the following to your root `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
...
|
||||
"scripts": {
|
||||
...
|
||||
"new": "backstage-cli new"
|
||||
},
|
||||
});
|
||||
"backstage": {
|
||||
"cli": {
|
||||
"new": {
|
||||
"globals": {
|
||||
"license": "UNLICENSED"
|
||||
},
|
||||
"templates": [
|
||||
"@backstage/cli/templates/new-frontend-plugin",
|
||||
"@backstage/cli/templates/new-frontend-plugin-module",
|
||||
"@backstage/cli/templates/backend-plugin",
|
||||
"@backstage/cli/templates/backend-plugin-module",
|
||||
"@backstage/cli/templates/plugin-web-library",
|
||||
"@backstage/cli/templates/plugin-node-library",
|
||||
"@backstage/cli/templates/plugin-common-library",
|
||||
"@backstage/cli/templates/web-library",
|
||||
"@backstage/cli/templates/node-library",
|
||||
"@backstage/cli/templates/scaffolder-backend-module"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### App Root Elements
|
||||
## Troubleshooting
|
||||
|
||||
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:
|
||||
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`.
|
||||
|
||||
```tsx
|
||||
export default app.createRoot(
|
||||
<>
|
||||
{/* highlight-next-line */}
|
||||
<AlertDisplay transientTimeoutMs={2500} />
|
||||
{/* highlight-next-line */}
|
||||
<OAuthRequestDialog />
|
||||
<AppRouter>
|
||||
{/* highlight-next-line */}
|
||||
<VisitListener />
|
||||
<Root>{routes}</Root>
|
||||
</AppRouter>
|
||||
</>,
|
||||
);
|
||||
```
|
||||
There is a `tree` mode that can be very helpful in understanding which plugins are being automatically detected and the extensions that they are providing to the system. You should also be able to see any legacy extensions which are being converted and added to the app.
|
||||
|
||||
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.
|
||||
This can be really useful output when raising any issue to the main repository too, so we can dig in to see what's happening with the system.
|
||||
|
||||
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.
|
||||
### I'm seeing duplicate cards for Entity Pages
|
||||
|
||||
### App Root Wrappers
|
||||
When using the `entityPage` option with `convertLegacyAppRoot`, you may notice duplicate cards appearing on your Entity Pages. This happens because the migration helper automatically extracts cards from your existing Entity Page component and adds them to the new system, while the new Entity Page system also automatically includes cards from any plugins installed in your `packages/app` package. This results in the same card appearing twice - once from your legacy component and once from the plugin.
|
||||
|
||||
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:
|
||||
To fix this, simply remove the card definitions from your old Entity Page component. The new system will automatically provide these cards through the installed plugins, so your manual definitions are no longer needed.
|
||||
|
||||
```tsx
|
||||
export default app.createRoot(
|
||||
<>
|
||||
<AlertDisplay transientTimeoutMs={2500} />
|
||||
<OAuthRequestDialog />
|
||||
<AppRouter>
|
||||
{/* highlight-next-line */}
|
||||
<CustomAppBarrier>
|
||||
<Root>{routes}</Root>
|
||||
{/* highlight-next-line */}
|
||||
</CustomAppBarrier>
|
||||
</AppRouter>
|
||||
</>,
|
||||
);
|
||||
```
|
||||
### `Error: Invalid element inside FlatRoutes, expected Route but found element of type ...`
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Here is an example converting the `CustomAppBarrier` into extension:
|
||||
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).
|
||||
|
||||
```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,
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
// ...
|
||||
});
|
||||
```
|
||||
## 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).
|
||||
|
||||
@@ -5,8 +5,6 @@ sidebar_label: Overview
|
||||
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/15-plugins.md) and
|
||||
[overrides](../architecture/25-extension-overrides.md). They are sometimes collectively referred to as
|
||||
frontend _features_, and what you install to build up a Backstage frontend [app](../architecture/10-app.md).
|
||||
|
||||
@@ -5,16 +5,8 @@ sidebar_label: Testing
|
||||
description: Testing plugins in the frontend system
|
||||
---
|
||||
|
||||
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
|
||||
|
||||
# Testing Frontend Plugins
|
||||
|
||||
:::note Note
|
||||
|
||||
The new frontend system is in alpha, and some plugins do not yet fully implement it.
|
||||
|
||||
:::
|
||||
|
||||
Utilities for testing frontend features and components are available in `@backstage/frontend-test-utils`.
|
||||
|
||||
## Testing React components
|
||||
|
||||
@@ -5,8 +5,6 @@ sidebar_label: Common Extension Blueprints
|
||||
description: Extension blueprints provided by the frontend system and core features
|
||||
---
|
||||
|
||||
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
|
||||
|
||||
This section covers many of the [extension blueprints](../architecture/23-extension-blueprints.md) available at your disposal when building Backstage frontend plugins.
|
||||
|
||||
## Extension blueprints in `@backstage/frontend-plugin-api`
|
||||
|
||||
@@ -5,8 +5,6 @@ sidebar_label: Built-in data refs
|
||||
description: Configuring or overriding built-in extension data references
|
||||
---
|
||||
|
||||
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
|
||||
|
||||
To have a better understanding of extension data references please read [the corresponding architecture section](../architecture/20-extensions.md#extension-data) first.
|
||||
|
||||
## Built-in extension data references
|
||||
|
||||
@@ -5,10 +5,8 @@ sidebar_label: Introduction
|
||||
description: The Frontend System
|
||||
---
|
||||
|
||||
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
|
||||
|
||||
## Status
|
||||
|
||||
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.
|
||||
We recommend migrating your frontend plugins to the new frontend system. If you do please do so under an `/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).
|
||||
|
||||
@@ -5,8 +5,6 @@ sidebar_label: Overview
|
||||
description: Working with Utility APIs in the New Frontend System
|
||||
---
|
||||
|
||||
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
|
||||
|
||||
As described [in the architecture section](../architecture/33-utility-apis.md), utility APIs are pieces of shared functionality - interfaces that can be requested by plugins to use. They are defined by a TypeScript interface as well as a reference (an "API ref") used to access its implementation. They can be provided both by plugins and the core framework, and are themselves [extensions](../architecture/20-extensions.md) that can accept inputs, be declaratively configured in your app-config, or transparently be replaced entirely with custom implementations that fulfill the same contract.
|
||||
|
||||
## Creating utility APIs
|
||||
|
||||
@@ -5,8 +5,6 @@ sidebar_label: Creating APIs
|
||||
description: Creating new utility APIs in your plugins and app
|
||||
---
|
||||
|
||||
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
|
||||
|
||||
This section describes how to make a Utility API from scratch, or to add configurability and inputs to an existing one. If you are instead interested in migrating an existing Utility API from the old frontend system, check out the [Migrating APIs section](../building-plugins/05-migrating.md#migrating-apis).
|
||||
|
||||
## Creating the Utility API contract
|
||||
|
||||
@@ -5,8 +5,6 @@ sidebar_label: Consuming APIs
|
||||
description: Consuming utility APIs
|
||||
---
|
||||
|
||||
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
|
||||
|
||||
All of the utility API extensions that were passed into your app through installed plugins, get instantiated and configured in the right order by the framework, and are then made available for consumption. You can interact with these instances in the following ways.
|
||||
|
||||
## Via React hooks
|
||||
|
||||
@@ -5,8 +5,6 @@ sidebar_label: Configuring
|
||||
description: Configuring, extending, and overriding utility APIs
|
||||
---
|
||||
|
||||
> **NOTE: The new frontend system is in alpha and is only supported by a small number of plugins.**
|
||||
|
||||
Utility APIs are extensions and can therefore optionally be amended with configurability, as well as inputs that other extensions attach themselves to. This section describes how to make use of that as a consumer of such utility APIs.
|
||||
|
||||
## Configuring
|
||||
|
||||
@@ -151,8 +151,11 @@ catalog:
|
||||
skipForkedRepos: false # Optional. If the project is a fork, skip repository
|
||||
includeArchivedRepos: false # Optional. If project is archived, include repository
|
||||
group: example-group # Optional. Group and subgroup (if needed) to look for repositories. If not present the whole instance will be scanned
|
||||
groupPattern: # Optional. Filters for groups based on a list of RegEx. Default, no filters.
|
||||
- '^somegroup$'
|
||||
- 'anothergroup'
|
||||
entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml`
|
||||
projectPattern: '[\s\S]*' # Optional. Filters found projects based on provided patter. Defaults to `[\s\S]*`, which means to not filter anything
|
||||
projectPattern: '[\s\S]*' # Optional. Filters found projects based on provided pattern. Defaults to `[\s\S]*`, which means to not filter anything
|
||||
excludeRepos: [] # Optional. A list of project paths that should be excluded from discovery, e.g. group/subgroup/repo. Should not start or end with a slash.
|
||||
schedule: # Same options as in SchedulerServiceTaskScheduleDefinition. Optional for the Legacy Backend System
|
||||
# supports cron, ISO duration, "human duration" as used in code
|
||||
|
||||
@@ -33,6 +33,12 @@ Example of use-cases:
|
||||
|
||||
## Installation
|
||||
|
||||
:::note
|
||||
|
||||
As of the `1.42.0` release of Backstage, Notifications and Signals are installed as part of the default `@backstage/create-app` instance which means you won't need to follow the installations steps outlined here. The only exception to this is adding the [Notifications tab to User Settings](#user-specific-notification-settings) to allow managing these settings.
|
||||
|
||||
:::
|
||||
|
||||
The following sections will walk you through the installation of the various parts of the Backstage Notification System.
|
||||
|
||||
### Add Notifications Backend
|
||||
@@ -173,6 +179,52 @@ notifications:
|
||||
|
||||
If the retention is set to false, notifications will not be automatically deleted.
|
||||
|
||||
## Scaffolder Action
|
||||
|
||||
:::note
|
||||
|
||||
As of the `1.42.0` release of Backstage, the Notifications Scaffolder action is installed as part of the default `@backstage/create-app` instance which means you won't need to follow the installations steps outlined here. Feel free to skip to the [Basic Example](#basic-example).
|
||||
|
||||
:::
|
||||
|
||||
There is also a Scaffolder action that you can use to send a notification as part of your Software Template.
|
||||
|
||||
First we need to add the backend package:
|
||||
|
||||
```bash title="From your Backstage root directory"
|
||||
yarn --cwd packages/backend add @backstage/plugin-scaffolder-backend-module-notifications
|
||||
```
|
||||
|
||||
Then we need to add it to our backend:
|
||||
|
||||
```ts title="packages/backend/src/index.ts"
|
||||
const backend = createBackend();
|
||||
// ...
|
||||
backend.add(
|
||||
import('@backstage/plugin-scaffolder-backend-module-notifications'),
|
||||
);
|
||||
```
|
||||
|
||||
### Basic Example
|
||||
|
||||
Here's an example of how you can use it in your Software Template, more details and examples can be found in the "Installed actions" screen in your Backstage instances:
|
||||
|
||||
```yaml title="template.yaml"
|
||||
steps:
|
||||
- id: notify
|
||||
name: Notify
|
||||
action: notification:send
|
||||
input:
|
||||
recipients: entity
|
||||
entityRefs:
|
||||
- user:default/guest
|
||||
title: 'Template executed'
|
||||
info: 'Your template has been executed'
|
||||
severity: 'normal'
|
||||
```
|
||||
|
||||
The example above would send a notification to the Guest user (`user:default/guest`)
|
||||
|
||||
## Additional info
|
||||
|
||||
An example of a backend plugin sending notifications can be found in the [`@backstage/plugin-scaffolder-backend-module-notifications` package](https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend-module-notifications).
|
||||
|
||||
@@ -210,3 +210,68 @@ notificationsApi.getNotification(yourId);
|
||||
// or with connection to signals:
|
||||
notificationsApi.getNotification(lastSignal.notification_id);
|
||||
```
|
||||
|
||||
## Metadata Field
|
||||
|
||||
The metadata field is a freeform object that is designed to be used by processors.
|
||||
|
||||
### Well-known Notification Metadata Fields
|
||||
|
||||
Below are metadata fields that will be commonly used between processors and have defined schematics.
|
||||
|
||||
#### backstage.io/body.markdown
|
||||
|
||||
```ts
|
||||
# Example:
|
||||
const payload = {
|
||||
title: 'Entities Require Attention',
|
||||
description: 'Entities: Service A, Service B'
|
||||
metadata: {
|
||||
'backstage.io/body.markdown': `
|
||||
# Entities
|
||||
- Service A
|
||||
- System B
|
||||
`
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This value of this metadata field should be the notification message in markdown format. This allows additional formatting options for processors that support markdown.
|
||||
|
||||
### Usage
|
||||
|
||||
Below is an example of using the `backstage.io/body.markdown` metadata field in a custom processor.
|
||||
|
||||
When sending a notification:
|
||||
|
||||
```ts
|
||||
notificationService.send({
|
||||
recipients: { type: 'entity', entityRef: 'group/default:team-a' },
|
||||
payload: {
|
||||
title: 'Notification',
|
||||
description: 'Description'
|
||||
metadata: {
|
||||
'backstage.io/body.markdown': `
|
||||
### Notification
|
||||
Description
|
||||
`,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
In the processor, you can then use the metadata field accordingly:
|
||||
|
||||
```ts
|
||||
async postProcess(notification: Notification): Promise<void> {
|
||||
// We suggest you parse the metadata field with a schema, i.e. Zod
|
||||
const parseResult = CustomProcessorMetadataSchema.safeParse(notification.payload.metadata ?? {});
|
||||
const metadata = parseResult.success ? parseResult.data : {};
|
||||
|
||||
customNotificationSender.send({
|
||||
to: getUsers(notification.recipients),
|
||||
subject: notification.payload.title,
|
||||
markdownText: metadata['backstage.io/body.markdown'] ?? notification.payload.description,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -284,8 +284,8 @@ stores as a means of improving performance or reliability. Similar to how
|
||||
databases are supported, plugins receive logically separated cache connections,
|
||||
which are powered by [Keyv](https://github.com/lukechilds/keyv) under the hood.
|
||||
|
||||
At this time of writing, Backstage can be configured to use one of three cache
|
||||
stores: memory, which is mainly used for local testing, memcache or Redis,
|
||||
At this time of writing, Backstage can be configured to use one of five cache
|
||||
stores: memory, which is mainly used for local testing, memcache, redis, valkey or infinispan,
|
||||
which are cache stores better suited for production deployment. The right cache
|
||||
store for your Backstage instance will depend on your own run-time constraints
|
||||
and those required of the plugins you're running.
|
||||
@@ -316,6 +316,40 @@ backend:
|
||||
connection: redis://user:pass@cache.example.com:6379
|
||||
```
|
||||
|
||||
### Use Infinispan for cache
|
||||
|
||||
#### Minimal configuration
|
||||
|
||||
- defaults, no `authentication`, expects cache named `cache` and host `127.0.0.1:11222`)
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
cache:
|
||||
store: infinispan
|
||||
```
|
||||
|
||||
#### Extended configuration
|
||||
|
||||
- Unlike Redis, Infinispan will **not** create the cache for you. It's expected you've configured the cache in your infinispan server prior to configuration here.
|
||||
- A full list of configuration items are available: https://docs.jboss.org/infinispan/hotrod-clients/javascript/1.0/apidocs/module-infinispan.html including support for backup clusters.
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
cache:
|
||||
store: infinispan
|
||||
infinispan:
|
||||
servers:
|
||||
- host: 127.0.0.1
|
||||
port: 11222
|
||||
cacheName: backstage-cache
|
||||
mediaType: application/json
|
||||
authentication:
|
||||
enabled: true
|
||||
userName: yourusername
|
||||
password: yourpassword
|
||||
saslMechanism: PLAIN
|
||||
```
|
||||
|
||||
Contributions supporting other cache stores are welcome!
|
||||
|
||||
## Containerization
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
---
|
||||
id: v1.42.0
|
||||
title: v1.42.0
|
||||
description: Backstage Release v1.42.0
|
||||
---
|
||||
|
||||
These are the release notes for the v1.42.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
|
||||
|
||||
### The new Frontend System is Adoption Ready! 🎉
|
||||
|
||||
We've been hard at work over the last few months stabilizing the APIs for the New Frontend System, and we're excited to announce that they're ready for prime time! With the release of version 1.42.0, we're officially encouraging all developers to start migrating their apps to the new system, along with all their plugins.
|
||||
|
||||
The New Frontend System is a major leap forward, offering a more powerful, flexible, and overall better development experience. By migrating, you’ll be able to take advantage of declarative integration of plugins and removing a large amount of code that’s used to configure your Backstage App. With plugins also migrating, there’s new extensibility patterns on the horizon that will allow you to customize Backstage even further than before.
|
||||
|
||||
We know that migrating can be a big undertaking, so we've made it as easy as possible. You can choose to migrate gradually, using a hybrid approach that combines the new and old systems, or you can start fresh with a brand new app using the `--next` flag.
|
||||
|
||||
Whatever path you choose, we've got you covered. Check out our comprehensive [migration guide](https://backstage.io/docs/frontend-system/building-apps/migrating) for step-by-step instructions, and don't hesitate to reach out to us on our repository or Discord if you need help.
|
||||
|
||||
### Default bundler switched to Rspack
|
||||
|
||||
The Backstage CLI has been updated to use [Rspack](https://rspack.rs/) as the bundler for frontend apps. It has been available behind the `EXPERIMENTAL_RSPACK` flag since the 1.32 release, but has now been switched to the default. The use of [WebPack](https://webpack.js.org/) can be restored using the `LEGACY_WEBPACK_BUILD` flag, along with installing the necessary dependencies. See the changelog for `@backstage/cli` for more information.
|
||||
|
||||
### Kubernetes Legacy Backend System Removal
|
||||
|
||||
We’ve removed support for the legacy backend system, as well all of the deprecated exports for the last plugin in the main repository. With this cleanup, we say goodbye to the last occurrence of the old `@backstage/backend-common` in the repository!
|
||||
|
||||
There’s extensive documentation in the [changelog](https://github.com/backstage/backstage/blob/master/plugins/kubernetes-backend/CHANGELOG.md#minor-changes) for migrating to the latest version.
|
||||
|
||||
Please reach out if you have any issues!
|
||||
|
||||
### Scaffolder is now powered by OpenAPI + more deprecations!
|
||||
|
||||
The Scaffolder router and client are now generated by the OpenAPI tooling. One of the oldest PR’s that we had in the repository has been merged after a lot of hard work. Thanks to [@solimant](https://github.com/solimant) and [@aramissennyeydd](https://github.com/aramissennyeydd) for their continued support throughout!
|
||||
|
||||
Also with this, there’s been a lot of types moved into `@backstage/plugin-scaffolder-common` which means that their original exports from `-react` have now been deprecated. Please check out the [CHANGELOG](https://github.com/backstage/backstage/blob/master/plugins/scaffolder-react/CHANGELOG.md) for more details on the affected types.
|
||||
|
||||
Contributed by [@solimant](https://github.com/solimant) in [#27771](https://github.com/backstage/backstage/pull/27771)
|
||||
|
||||
### Catalog performance improvements
|
||||
|
||||
The catalog sometimes did more work than necessary because it was not taking into account that in some places, order doesn’t matter. So that got fixed! The upshot is that you may see in your monitoring that it performs less useless stitching after the upgrade. To achieve this it will have to do a short period of _more_ work right after the upgrade before it converges. Just so you are aware.
|
||||
|
||||
Contributed by [@drodil](https://github.com/drodil) in [#30859](https://github.com/backstage/backstage/pull/30859)
|
||||
|
||||
## Security Fixes
|
||||
|
||||
This release also fixes the possibility of the scaffolder logging both redacted and plain text secrets when used as input to the `fetch:template` action.
|
||||
|
||||
## 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.42.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.
|
||||
@@ -0,0 +1,213 @@
|
||||
---
|
||||
id: integrating-event-driven-updates-with-entity-providers
|
||||
title: Integrating Event-Driven Updates with Entity Providers
|
||||
description: A guide on integrating the events system into an EntityProvider to enable immediate updates to the catalog.
|
||||
---
|
||||
|
||||
This guide walks you through setting up an HTTP endpoint to receive events and integrating the Events System into an EntityProvider to enable immediate updates to the catalog. While the Events system supports other use cases, this guide focuses specifically on using HTTP-based events in an EntityProvider.
|
||||
|
||||
In the example provided, we demonstrate how to ingest entities from an external fictional service called `Frobs`.
|
||||
|
||||
Basic flow:
|
||||
|
||||
- An external service (`Frobs` in this example) sends events to an HTTP endpoint exposed by the `@backstage/plugin-events-backend` plugin. This endpoint corresponds to a topic you define (e.g., `frobs`).
|
||||
|
||||
- A module to extend the `@backstage/plugin-events-backend` plugin that exposes a custom Router, processes incoming events on this generic topic and routes them to more specific sub-topics based on the contents of each event’s payload.
|
||||
|
||||
- An `EntityProvider` subscribes to these specific sub-topic events and, upon receiving them, takes an action to add/update/delete entities in the catalog accordingly.
|
||||
|
||||
## Receiving Events via HTTP Endpoints
|
||||
|
||||
The `@backstage/plugin-events-backend` plugin provides out-of-the-box support for receiving events via HTTP endpoints. These events are then published to the `EventsService`.
|
||||
|
||||
To create HTTP endpoints for specific topics, you need to configure them in your app-config.yaml:
|
||||
|
||||
```
|
||||
events:
|
||||
http:
|
||||
topics:
|
||||
- frobs
|
||||
```
|
||||
|
||||
Only topics explicitly listed in this configuration will result in available HTTP endpoints.
|
||||
|
||||
The example above would create the following endpoints:
|
||||
|
||||
`POST /api/events/http/frobs`
|
||||
|
||||
You can use this URL as the payload URL when setting up your webhooks from external services. When an event is sent to this endpoint, the events-backend will publish it to the Events Service, making it available to any subscribed entity providers.
|
||||
|
||||
## Routing General Topics to Specific Subtopics
|
||||
|
||||
To effectively manage events, you can extend the event system to route general topics to more specific subtopics by creating a module for the `events-backend` plugin.
|
||||
|
||||
For example, when setting up a webhook for the `Frobs` service using the `POST /api/events/http/frobs` endpoint, all incoming events are initially published under the generic `frobs` topic. However, by republishing these events under more specific subtopics based on their payload — such as the type field — you allow your EntityProvider to subscribe only to the relevant subtopics it needs, rather than processing every event under the broader `frobs` topic.
|
||||
|
||||
Here's an example of a `SubTopicEventRouter` that subscribes to a generic `frobs` topic and then publishes events under a more concrete sub-topic based on the `$.type` provided in the event payload:
|
||||
|
||||
```ts
|
||||
import {
|
||||
EventParams,
|
||||
EventsService,
|
||||
SubTopicEventRouter,
|
||||
} from '@backstage/plugin-events-node';
|
||||
|
||||
/**
|
||||
* Subscribes to the generic `frobs` topic
|
||||
* and publishes the events under the more concrete sub-topic
|
||||
* depending on the `$.type` provided in the event payload.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class FrobsEventRouter extends SubTopicEventRouter {
|
||||
constructor(options: { events: EventsService }) {
|
||||
super({
|
||||
events: options.events,
|
||||
topic: 'frobs',
|
||||
});
|
||||
}
|
||||
|
||||
protected getSubscriberId(): string {
|
||||
return 'FrobsEventRouter';
|
||||
}
|
||||
|
||||
protected determineSubTopic(params: EventParams): string | undefined {
|
||||
if ('type' in (params.eventPayload as object)) {
|
||||
const payload = params.eventPayload as { type: string };
|
||||
return payload.type;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Integrating Events into an Entity Provider
|
||||
|
||||
Your entity provider can subscribe to specific event topics and react to incoming events. This allows for immediate updates to your catalog based on external triggers.
|
||||
|
||||
Here is the basic structure of an EntityProvider that incorporates event subscription. Check out the numbered markings below for a breakdown of each step:
|
||||
|
||||
```ts title="plugins/catalog-backend-module-frobs/src/FrobsProvider.ts"
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
EntityProvider,
|
||||
EntityProviderConnection,
|
||||
} from '@backstage/plugin-catalog-node';
|
||||
import {
|
||||
SchedulerServiceTaskRunner,
|
||||
UrlReaderService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { EventsService, EventParams } from '@backstage/plugin-events-node';
|
||||
|
||||
/**
|
||||
* Provides entities from the fictional Frobs service.
|
||||
*/
|
||||
export class FrobsProvider implements EntityProvider {
|
||||
private readonly env: string;
|
||||
private readonly reader: UrlReaderService;
|
||||
private readonly taskRunner: SchedulerServiceTaskRunner;
|
||||
private readonly events?: EventsService;
|
||||
private connection?: EntityProviderConnection;
|
||||
|
||||
constructor(
|
||||
env: string,
|
||||
reader: UrlReaderService,
|
||||
taskRunner: SchedulerServiceTaskRunner,
|
||||
/** [1] */
|
||||
events?: EventsService,
|
||||
) {
|
||||
this.env = env;
|
||||
this.reader = reader;
|
||||
this.taskRunner = taskRunner;
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
getProviderName(): string {
|
||||
return `frobs-${this.env}`;
|
||||
}
|
||||
|
||||
async connect(connection: EntityProviderConnection): Promise<void> {
|
||||
this.connection = connection;
|
||||
|
||||
/** [2] */
|
||||
await this.events?.subscribe({
|
||||
id: this.getProviderName(),
|
||||
topics: ['frobs-add', 'frobs-delete', 'frobs-modify'],
|
||||
/** [3] */
|
||||
onEvent: async (params: EventParams) => {
|
||||
const id = params.eventPayload.id;
|
||||
const baseUrl = `https://frobs-${id}.example.com/data`;
|
||||
|
||||
const response = await this.reader.readUrl(baseUrl);
|
||||
const data = JSON.parse((await response.buffer()).toString());
|
||||
const entities: Entity[] = frobsToEntities(data);
|
||||
|
||||
if (params.topic === 'frobs-add') {
|
||||
await this.connection!.applyMutation({
|
||||
type: 'delta',
|
||||
added: entities,
|
||||
removed: [],
|
||||
});
|
||||
} else if (params.topic === 'frobs-delete') {
|
||||
await this.connection!.applyMutation({
|
||||
type: 'delta',
|
||||
added: [],
|
||||
removed: entities,
|
||||
});
|
||||
} else if (params.topic === 'frobs-modify') {
|
||||
const oldResponse = await this.reader.readUrl(
|
||||
`${baseUrl}/previous-state`,
|
||||
);
|
||||
const oldData = JSON.parse((await oldResponse.buffer()).toString());
|
||||
const oldEntities: Entity[] = frobsToEntities(oldData);
|
||||
|
||||
await this.connection!.applyMutation({
|
||||
type: 'delta',
|
||||
added: entities,
|
||||
removed: oldEntities,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await this.taskRunner.run({
|
||||
id: this.getProviderName(),
|
||||
fn: async () => {
|
||||
await this.run();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
if (!this.connection) {
|
||||
throw new Error('FrobsProvider not initialized');
|
||||
}
|
||||
|
||||
const response = await this.reader.readUrl(
|
||||
`https://frobs-${this.env}.example.com/data`,
|
||||
);
|
||||
const data = JSON.parse((await response.buffer()).toString());
|
||||
const entities: Entity[] = frobsToEntities(data);
|
||||
|
||||
await this.connection.applyMutation({
|
||||
type: 'full',
|
||||
entities: entities.map(entity => ({
|
||||
entity,
|
||||
locationKey: `frobs-provider:${this.env}`,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Let's break down the key parts of this integration:
|
||||
|
||||
1. **Add EventsService as a Dependency**: In the EntityProvider's constructor, include `EventsService` as an optional dependency. This allows your provider to interact with the event system.
|
||||
|
||||
2. **Subscribe to Topics in connect**: Within the connect function, subscribe to the specific event topics that your plugin needs to react to (e.g., 'frobs-add', 'frobs-delete', 'frobs-modify').
|
||||
|
||||
3. **Implement the `onEvent` Method**: The `onEvent` method is crucial. It will be invoked whenever your provider receives an event for a subscribed topic. Inside this method:
|
||||
|
||||
- Based on the event payload information (accessible through `params.eventPayload`), implement the logic to decide which entities should be added, deleted, or modified.
|
||||
- Use a delta mutation to explicitly upsert or delete entities. This approach is more efficient than updating the entire catalog from scratch. For more details on mutations, refer to the ["Provider Mutations" section](https://backstage.io/docs/features/software-catalog/external-integrations#provider-mutations).
|
||||
Reference in New Issue
Block a user