From c371516b1b2c131e65e9580b34a9f6e634781e15 Mon Sep 17 00:00:00 2001 From: Stan Lewis Date: Wed, 24 Jun 2026 08:55:19 -0400 Subject: [PATCH] chore(docs): add frontend migration guides This change adds a couple migration guides to help plugin developers and RHDH operators better understand the changes required to adopt the new frontend system once it's made the default in RHDH. Assisted-By: Cursor Desktop --- .../dynamic-plugins/frontend-plugin-wiring.md | 5 + docs/dynamic-plugins/index.md | 4 + ...migrating-config-to-new-frontend-system.md | 722 +++++++++++ ...igrating-plugins-to-new-frontend-system.md | 1077 +++++++++++++++++ 4 files changed, 1808 insertions(+) create mode 100644 docs/dynamic-plugins/migrating-config-to-new-frontend-system.md create mode 100644 docs/dynamic-plugins/migrating-plugins-to-new-frontend-system.md diff --git a/docs/dynamic-plugins/frontend-plugin-wiring.md b/docs/dynamic-plugins/frontend-plugin-wiring.md index b5b6d15eee..138f1cce19 100644 --- a/docs/dynamic-plugins/frontend-plugin-wiring.md +++ b/docs/dynamic-plugins/frontend-plugin-wiring.md @@ -1,5 +1,10 @@ # Frontend Plugin Wiring +> **Legacy configuration reference.** This document describes RHDH dynamic plugin frontend wiring for the **current** app shell. If you are moving to the Backstage new frontend system, use: +> +> - **Operators / platform admins** (YAML customization only) → [Migrating Config to the New Frontend System](migrating-config-to-new-frontend-system.md) +> - **Plugin authors** (plugin code and `/alpha` exports) → [Migrating Plugins to the New Frontend System](migrating-plugins-to-new-frontend-system.md) + Compared to the backend plugins, where mount points are defined in code and consumed by the backend plugin manager, frontend plugins require additional configuration in the `app-config.yaml`. A plugin missing this configuration will not be loaded into the application and will not be displayed. Similarly to traditional Backstage instances, there are various kinds of functionality a dynamic frontend plugin can offer: diff --git a/docs/dynamic-plugins/index.md b/docs/dynamic-plugins/index.md index 0a4b1a97a6..0aa4ce56ca 100644 --- a/docs/dynamic-plugins/index.md +++ b/docs/dynamic-plugins/index.md @@ -21,6 +21,10 @@ More details about publishing dynamic plugins is in the [Packaging Dynamic Plugi [Frontend Plugin Wiring](frontend-plugin-wiring.md) +[Migrating Plugins to the New Frontend System](migrating-plugins-to-new-frontend-system.md) + +[Migrating Config to the New Frontend System](migrating-config-to-new-frontend-system.md) + [Local configuration (for development only)](local.md) [Version Compatibility Matrix](versions.md) diff --git a/docs/dynamic-plugins/migrating-config-to-new-frontend-system.md b/docs/dynamic-plugins/migrating-config-to-new-frontend-system.md new file mode 100644 index 0000000000..bc9dae172a --- /dev/null +++ b/docs/dynamic-plugins/migrating-config-to-new-frontend-system.md @@ -0,0 +1,722 @@ +# Migrating RHDH Frontend Configuration to the Backstage New Frontend System + +This guide helps **operators and platform administrators** customize Red Hat Developer Hub (RHDH) after it moves to the **Backstage new frontend system**. It explains how to translate legacy RHDH frontend configuration (`dynamicPlugins.frontend` in `dynamic-plugins.yaml` and related `app-config`) into the upstream `app.*` configuration model. + +> **Operators / platform admins** → this document. +> +> **Plugin authors** → [Migrating Plugins to the New Frontend System](migrating-plugins-to-new-frontend-system.md). + +## Transition: the new frontend system is not the default yet + +RHDH still ships the legacy `app` frontend package by default. The new frontend system lives in the `app-next` package and will become the default after the app-shell switch. Until then, enable **both** of the following on your RHDH **backend** deployment (OpenShift, Helm, Operator, [rhdh-local](https://github.com/redhat-developer/rhdh-local), or any environment where the backend runs as a container): + +| Setting | How to apply | Purpose | +| --- | --- | --- | +| `app.packageName: app-next` | Environment variable `APP_CONFIG_app_packageName=app-next`, **or** in `app-config.yaml` under `app.packageName` | Tells the app backend to serve the `app-next` frontend (new frontend system) instead of `app`. | +| `ENABLE_STANDARD_MODULE_FEDERATION=true` | Environment variable on the backend container only | Enables the backend to serve standard Module Federation assets for dynamic frontend plugins. Without this, RHDH disables that service because the legacy frontend does not use it. | + +Example environment variables for the RHDH backend pod or deployment: + +```bash +APP_CONFIG_app_packageName=app-next +ENABLE_STANDARD_MODULE_FEDERATION=true +``` + +Equivalent `app-config` fragment (you still need `ENABLE_STANDARD_MODULE_FEDERATION` in the environment): + +```yaml +app: + packageName: app-next +``` + +These requirements are temporary. Once RHDH completes the switch to `app-next`, they will become the default and this transition note can be removed. + +## Who should read this + +- RHDH administrators who edit `dynamic-plugins.yaml`, Helm values, or Operator configuration. +- Customer solution architects who customized entity tabs, mount points, routes, or navigation through YAML. +- Anyone familiar with [Frontend Plugin Wiring](frontend-plugin-wiring.md) who needs the equivalent settings in the new frontend system. + +## Prerequisites + +- RHDH is running with the new frontend system enabled — see [Transition: the new frontend system is not the default yet](#transition-the-new-frontend-system-is-not-the-default-yet) above. +- You understand where your deployment stores `dynamic-plugins.yaml` and `app-config` — see [Installing Plugins](installing-plugins.md) and the [Red Hat product documentation](https://docs.redhat.com/en/documentation/red_hat_developer_hub/) for Helm and Operator paths. +- Installed plugins support the new frontend system. Configuration alone cannot add UI that a plugin does not register as an extension. + +## The mindset shift + +| Aspect | Legacy RHDH (`dynamicPlugins.frontend`) | New frontend system (`app.*`) | +| --- | --- | --- | +| Who declares UI placement | **You** in YAML (`mountPoints`, `dynamicRoutes`, `importName`) | **Plugins** declare defaults; you **override** via config | +| Plugin registration | Per-plugin block under `dynamicPlugins.frontend` | Plugin installed + auto-discovered; `app.packages` controls discovery | +| Entity cards | `mountPoints` on `entity.page.*` | `entity-card:*` extensions on overview | +| Entity tabs | `entityTabs` + mount point names | `entity-content:*` extensions + `page:catalog/entity` groups | +| Cross-plugin links | `routeBindings` | `app.routes.bindings` | +| Disable a feature | Remove YAML or set `enabled: false` | `app.extensions: [: false]` | +| Ordering | List order in `mountPoints` / `entityTabs` | List order in `app.extensions` | + +**Key idea:** In the legacy model, you told RHDH *which exported component goes to which mount point*. In the new frontend system, plugins describe their own extensions; you enable, disable, reorder, and tune them through `app.extensions`, `app.routes.bindings`, and related `app.*` keys. + +## Who does what + +| Responsibility | Operator (this guide) | Plugin vendor | +| --- | --- | --- | +| Install / enable a plugin | `dynamic-plugins.yaml` `enabled: true` | Publish OCI or npm package | +| Add a new entity tab | Configure an existing `entity-content:*` extension | Plugin update (see [plugins migration guide](migrating-plugins-to-new-frontend-system.md)) | +| Add a card to entity overview | Enable/configure `entity-card:*` | Plugin update (see [plugins migration guide](migrating-plugins-to-new-frontend-system.md)) | +| Hide a default card or page | `app.extensions: [: false]` | — | +| Change tab title or group | `entity-content:*` or `page:catalog/entity` config | Sensible defaults in plugin | +| Replace entire settings page | `page:user-settings` override (limited today) | Full page extension | + +If a customization required `mountPoints` + `importName` before, the plugin must now expose that UI as an extension. Contact the plugin maintainer if the extension does not appear after install. + +## Where configuration lives in RHDH + +RHDH still uses two related configuration surfaces: + +### `dynamic-plugins.yaml` — plugin installation + +Defines **which** dynamic plugins are installed and whether they are enabled. Each entry can include `pluginConfig`, which is merged into `app-config.dynamic-plugins.yaml` at startup. + +```yaml +plugins: + - enabled: true + package: oci://example.com/my-plugin:1.0 + pluginConfig: + # Fragment merged into app-config +``` + +See [Installing Plugins](installing-plugins.md) for Helm, Operator, and catalog index image details. + +### `app-config` — application behavior + +Frontend customization for the new frontend system lives under top-level `app` keys in `app-config.yaml` (or fragments merged into it): + +| Key | Purpose | +| --- | --- | +| `app.packages` | Control automatic discovery of frontend plugins from dependencies | +| `app.routes.bindings` | Bind external route references between plugins | +| `app.extensions` | Enable, disable, reorder, and configure individual extensions | +| `app.pluginOverrides` | Override resolved plugin metadata (owner, description, etc.) | + +Default plugin configuration may also come from the catalog index image (`dynamic-plugins.default.yaml`). You can override defaults in your own `dynamic-plugins.yaml` `pluginConfig` or in `app-config`. + +### Config file merging + +Backstage merges configuration files by **replacing entire arrays** when the same key appears in multiple files. If `app.extensions` is defined in both `app-config.yaml` and `app-config.production.yaml`, the higher-priority file's array **replaces** the lower-priority one — entries are not merged entry-by-entry. + +Because unlisted extensions are still **auto-discovered**, a local override file can contain only the extensions you want to change: + +```yaml +# app-config.local.yaml — only overrides what you need +app: + extensions: + - entity-card:catalog/about: + config: + type: info +``` + +Individual extension `config` objects are also replaced wholesale when overridden. See [Writing Configuration](https://backstage.io/docs/conf/writing/) and [Configuring Extensions](https://backstage.io/docs/frontend-system/building-apps/configuring-extensions/). + +--- + +## Configuration surface overview + +### `app.packages` + +Controls which frontend plugin packages are auto-discovered at build time: + +```yaml +app: + packages: all +``` + +Or restrict explicitly: + +```yaml +app: + packages: + include: + - '@backstage/plugin-catalog' + - '@backstage/plugin-techdocs' + exclude: [] +``` + +Dynamic plugins loaded at runtime through the frontend feature loader are discovered separately from this setting; consult your RHDH release notes for how installed OCI plugins participate in extension discovery. + +### `app.routes.bindings` + +Replaces `routeBindings` in `dynamicPlugins.frontend`: + +```yaml +# Legacy RHDH +dynamicPlugins: + frontend: + backstage.plugin-techdocs: + routeBindings: + targets: + - importName: techdocsPlugin + bindings: + - bindTarget: catalogPlugin.externalRoutes + bindMap: + viewTechDoc: techdocsPlugin.routes.docRoot +``` + +```yaml +# New frontend system +app: + routes: + bindings: + catalog.viewTechDoc: techdocs.docRoot + catalog.createComponent: scaffolder.index + scaffolder.registerComponent: false # explicitly disable a binding +``` + +Binding values use `pluginId.routeName` syntax. See [Frontend Routes](https://backstage.io/docs/frontend-system/architecture/routes/#binding-external-route-references). + +### `app.extensions` + +List of extension IDs with optional `attachTo`, `disabled`, and `config`: + +```yaml +app: + extensions: + # Shorthand: enable with plugin defaults + - entity-card:catalog/about + + # Shorthand: disable + - page:catalog-unprocessed-entities: false + + # Full form + - entity-card:catalog/links: + config: + filter: + kind: component + metadata.links: + $exists: true + type: info +``` + +Extension IDs follow `[:][/]` — for example `page:catalog`, `entity-card:catalog/about`, `entity-content:techdocs`. + +**Resolution rules:** + +1. All extensions from installed plugins are **auto-discovered** and loaded by default. +2. Entries in `app.extensions` **override** matching extensions by ID. +3. Extensions **listed** in `app.extensions` are **reordered** to appear first, in list order. Unlisted extensions keep their default order afterward. + +You typically list only extensions you want to customize, disable, or reorder — not the full inventory. + +--- + +## Migrating configuration by feature + +The following sections map each major area of [Frontend Plugin Wiring](frontend-plugin-wiring.md) to new-frontend-system `app-config` only. + +### Dynamic routes and sidebar navigation + +**Legacy:** + +```yaml +dynamicPlugins: + frontend: + backstage.plugin-my-plugin: + dynamicRoutes: + - path: /my-plugin + importName: MyPluginPage + menuItem: + icon: my-icon + text: My Plugin + priority: 100 +``` + +**New:** Routes come from `page:*` extensions declared by the plugin. Customize title, path, or visibility via `app.extensions`: + +```yaml +app: + extensions: + - page:my-plugin: + config: + title: My Plugin + path: /my-plugin + - page:my-plugin: false # disable the page entirely +``` + +Sidebar navigation is derived from each page's `title` and `icon`. To enforce nav order, list page extensions in the desired order in `app.extensions`. + +Nested sidebar menu groups (`menuItems.parent`) from RHDH dynamic plugins do not have a direct upstream equivalent — see [RHDH-specific gaps](#rhdh-specific-gaps) below. + +### Entity page cards (mount points) + +**Legacy:** Cards target `entity.page.*/cards` mount points with `importName` and optional `config.if` / `config.layout`. + +```yaml +mountPoints: + - mountPoint: entity.page.overview/cards + importName: MyOverviewCard + config: + if: + allOf: + - isKind: component + layout: + gridColumn: "1 / span 6" +``` + +**New:** Cards are `entity-card:*` extensions. Most attach to `entity-content:catalog/overview` automatically. Override filters, card type, or disable: + +```yaml +app: + extensions: + - entity-card:catalog/about: + config: + type: info + - entity-card:my-plugin/overview-card: + config: + filter: + kind: component + - entity-card:catalog/labels: false # hide default labels card +``` + +| Legacy mount point | Typical NFS extension | +| --- | --- | +| `entity.page.overview/cards` | `entity-card:*` → overview `cards` input | +| `entity.page.*/cards` (other tabs) | Plugin's `entity-content:*` or cards within that content | +| `entity.context.menu` | `entity-context-menu-item:*` | +| `search.page.results` | `search-result-list-item:*` | +| `search.page.filters` | `search-filter:*` | +| `search.page.types` | `search-filter-result-type:*` | + +`config.layout` grid positioning from RHDH mount points is **not** available in `app.extensions`. Layout is determined by the overview layout (`info` vs `content` card types) or by the plugin component. + +### Entity page tabs (`entityTabs`) + +**Legacy:** + +```yaml +entityTabs: + - path: /my-tab + title: My Tab + mountPoint: entity.page.my-tab + priority: 10 + - path: / + title: General + mountPoint: entity.page.overview + priority: -6 # hide default overview tab +``` + +**New:** Tabs come from `entity-content:*` extensions attached to `page:catalog/entity`. Control groups and titles at two levels: + +**1. Tab groups** on the entity page: + +```yaml +app: + extensions: + - page:catalog/entity: + config: + showNavItemIcons: true + groups: + - overview: + title: Overview + - documentation: + title: Documentation + - development: + title: Development + - deployment: + title: Deployment +``` + +Default groups: `overview`, `documentation`, `development`, `deployment`, `operation`, `observability`. Set `groups: []` to disable all default groups. + +**2. Individual tab content:** + +```yaml +app: + extensions: + - entity-content:techdocs: + config: + title: Docs + icon: techdocs + group: documentation + - entity-content:api-docs/apis: + config: + title: APIs + group: development + - entity-content:kubernetes/kubernetes: + config: + group: deployment + - entity-content:techdocs: false # hide TechDocs tab +``` + +To show a content extension as a **standalone tab** (not grouped), set `group: false` in config. + +Adding a **brand-new** tab requires a plugin that exports an `entity-content:*` extension — you cannot create one from YAML alone. + +#### Default RHDH entity tab routes (legacy reference) + +| Route | Title | Legacy mount point | +| --- | --- | --- | +| `/` | Overview | `entity.page.overview` | +| `/topology` | Topology | `entity.page.topology` | +| `/issues` | Issues | `entity.page.issues` | +| `/pr` | Pull/Merge Requests | `entity.page.pull-requests` | +| `/ci` | CI | `entity.page.ci` | +| `/cd` | CD | `entity.page.cd` | +| `/kubernetes` | Kubernetes | `entity.page.kubernetes` | +| `/api` | Api | `entity.page.api` | +| `/dependencies` | Dependencies | `entity.page.dependencies` | +| `/docs` | Docs | `entity.page.docs` | +| `/definition` | Definition | `entity.page.definition` | +| `/system` | Diagram | `entity.page.diagram` | + +On the new frontend system, plugin tabs appear when the plugin exports a matching `entity-content:*` extension and it is not disabled in config. There is no `entityTabs` registry in app config. + +### Search page + +**Legacy:** + +```yaml +mountPoints: + - mountPoint: search.page.results + importName: MySearchResultItem + - mountPoint: search.page.filters + importName: MySearchFilter +``` + +**New:** + +```yaml +app: + extensions: + - search-result-list-item:my-plugin/custom-result + - search-filter:my-plugin/custom-filter + - search-filter-result-type:my-plugin/custom-type: false +``` + +### Application icons + +**Legacy:** `appIcons` in `dynamicPlugins.frontend` registered components in the app icon catalog. + +**New:** Page icons come from each `page:*` extension's `icon` (or `config.icon` as a string icon ID when icon bundles are installed). See [Icons](https://backstage.io/docs/conf/user-interface/icons/). + +For most operators, icon customization is limited to `config.icon` string IDs on page and entity-content extensions. + +### API factories + +**Legacy:** + +```yaml +apiFactories: + - importName: myApiFactory +``` + +**New:** APIs are `api:*` extensions auto-discovered with the plugin. Adopters rarely configure them directly. To override behavior when supported: + +```yaml +app: + extensions: + - api:my-plugin.config: + config: + goSlow: false +``` + +If a plugin still relies on legacy `apiFactories` YAML only, it needs a plugin update — see [Migrating Plugins to the New Frontend System](migrating-plugins-to-new-frontend-system.md). + +### Translation resources + +**Legacy:** + +```yaml +translationResources: + - importName: myPluginTranslations +``` + +**New:** Translations are `translation:*` extensions, usually auto-discovered. Override messages via extension config where the plugin supports it. See the plugin's documentation for supported override keys. + +### Themes + +RHDH theme customization continues to interact with Backstage theming. Dynamic plugins can supply `theme:*` extensions. Operator overrides: + +```yaml +app: + extensions: + - theme:my-plugin/light: + config: + title: Corporate Light +``` + +See also [RHDH customization documentation](../customization.md). + +### Scaffolder field extensions + +**Legacy:** `scaffolderFieldExtensions` with `importName` entries. + +**New:** Custom scaffolder fields are registered by the plugin automatically on the new frontend system. No YAML registration is required. Template grouping on the scaffolder sub-page: + +```yaml +app: + extensions: + - sub-page:scaffolder/templates: + config: + groups: + - title: Recommended Services + filter: + spec.type: service +``` + +--- + +## Catalog entity page changes + +When RHDH moves to the upstream new frontend system entity page, several RHDH-specific tabs and layouts change. These are **not renames** — they reflect the upstream composition model introduced with the new frontend system (Backstage 1.49+ made NFS the default app template; entity cards and content are extension-driven). + +### Dependencies tab → overview cards + +**Legacy RHDH:** A dedicated `/dependencies` tab (`entity.page.dependencies`) with dependency cards and often a large relation graph. + +**New frontend system:** There is no `/dependencies` entity-content tab in upstream. The same cards are available as overview extensions: + +| Card | Extension ID | +| --- | --- | +| Depends on components | `entity-card:catalog/depends-on-components` | +| Depends on resources | `entity-card:catalog/depends-on-resources` | +| Has subcomponents | `entity-card:catalog/has-subcomponents` | +| Provided APIs | `entity-card:api-docs/provided-apis` | +| Consumed APIs | `entity-card:api-docs/consumed-apis` | +| Relation graph (smaller) | `entity-card:catalog-graph/relations` | + +Enable and tune the graph card: + +```yaml +app: + extensions: + - entity-card:catalog-graph/relations: + config: + height: 400 + direction: TOP_BOTTOM +``` + +To restore a dedicated Dependencies tab, a plugin must contribute a custom `entity-content:*` extension — this is not provided out of the box upstream. + +### System diagram tab → catalog graph + +**Legacy RHDH:** `/system` tab (`entity.page.diagram`) with a full-width `EntityCatalogGraphCard` and extended relation set. + +**New frontend system:** + +- Overview includes `entity-card:catalog-graph/relations` for a compact graph. +- The **View Graph** action on that card opens the standalone `page:catalog-graph` page. + +There is no default system-only diagram tab. Configure the relations card for richer graphs: + +```yaml +app: + extensions: + - entity-card:catalog-graph/relations: + config: + title: System Diagram + height: 700 + direction: TOP_BOTTOM + unidirectional: false + relations: + - partOf + - hasPart + - apiConsumedBy + - apiProvidedBy + - consumesApi + - providesApi + - dependencyOf + - dependsOn +``` + +### API tab path change + +**Legacy RHDH:** `/api` tab with provided/consumed API cards for service components. + +**New frontend system:** `entity-content:api-docs/apis` at path `/apis` (note the **s**). Update bookmarks and documentation links accordingly. + +### Overview layout + +**Legacy RHDH:** Per-entity-kind MUI grid layout in app shell code (`OverviewTabContent.tsx`). + +**New frontend system:** `DefaultEntityContentLayout` with `type: info` cards in a sticky sidebar and `type: content` cards in the main area. Warnings (orphan, relation, processing errors) are built into the layout — no separate mount point configuration. + +```yaml +app: + extensions: + - entity-card:catalog/about: + config: + type: info + - entity-card:catalog/links: + config: + type: info +``` + +### Built-in entity extensions reference + +**Overview content:** + +- `entity-content:catalog/overview` + +**Common overview cards** (enable or configure as needed): + +- `entity-card:catalog/about` +- `entity-card:catalog/labels` +- `entity-card:catalog/links` +- `entity-card:catalog-graph/relations` +- `entity-card:catalog/depends-on-components` +- `entity-card:catalog/depends-on-resources` +- `entity-card:catalog/has-subcomponents` +- `entity-card:catalog/has-components` +- `entity-card:catalog/has-resources` +- `entity-card:catalog/has-systems` +- `entity-card:api-docs/has-apis` +- `entity-card:api-docs/consumed-apis` +- `entity-card:api-docs/provided-apis` +- `entity-card:api-docs/providing-components` +- `entity-card:api-docs/consuming-components` +- `entity-card:org/group-profile` +- `entity-card:org/members-list` +- `entity-card:org/ownership` +- `entity-card:org/user-profile` + +**Entity content tabs** (attach to `page:catalog/entity`): + +- `entity-content:catalog/overview` +- `entity-content:api-docs/definition` +- `entity-content:api-docs/apis` +- `entity-content:techdocs` +- `entity-content:kubernetes/kubernetes` + +Additional tabs (CI, CD, Argo CD, etc.) appear when the corresponding dynamic plugin exports an `entity-content:*` extension. + +### Example: typical `app.extensions` starter for entity pages + +Based on the [Backstage example `app-config.yaml`](https://github.com/backstage/backstage/blob/master/app-config.yaml): + +```yaml +app: + extensions: + # Entity page cards + - entity-card:catalog/about: + config: + type: info + - entity-card:catalog/links: + config: + type: info + - entity-card:catalog-graph/relations: + config: + height: 300 + - entity-card:api-docs/consumed-apis + - entity-card:api-docs/provided-apis + - entity-card:org/group-profile + - entity-card:org/members-list + - entity-card:org/ownership + - entity-card:org/user-profile + + # Entity page contents (tabs) + - entity-content:catalog/overview + - entity-content:api-docs/definition + - entity-content:api-docs/apis + - entity-content:techdocs + - entity-content:kubernetes/kubernetes +``` + +--- + +## User settings page + +RHDH today hardcodes settings UI extensions in the app package. The upstream `user-settings` plugin exports `page:user-settings` and sub-pages (`general`, `auth-providers`, `feature-flags`). + +| Customization goal | New frontend system support today | +| --- | --- | +| Replace the entire settings page | Override `page:user-settings` via `app.extensions` (interim escape hatch) | +| Add cards to General settings | **Not yet** — `sub-page:user-settings/general` has no card/content input in upstream | +| Hide a default settings card | **Not yet** — default cards are not individual extensions | + +Extensible user settings is tracked as product work. Until upstream adds extension inputs on the General sub-page, partners may need to replace the full settings page or wait for plugin updates. + +--- + +## Operator cheat sheet + +| Task | Legacy RHDH | New frontend system | +| --- | --- | --- | +| Install a plugin | `dynamic-plugins.yaml` entry | Same — `dynamic-plugins.yaml` `enabled: true` | +| Disable a plugin page | Remove route or `menuItem.enabled: false` | `app.extensions: ['page:my-plugin': false]` | +| Rename sidebar item | `menuItem.text` | `page:my-plugin` → `config.title` | +| Reorder sidebar | `menuItems.*.priority` | Order in `app.extensions` | +| Hide entity overview card | Remove mount point entry | `entity-card:*: false` | +| Change card visibility filter | `mountPoints[].config.if` | `entity-card:*` → `config.filter` | +| Rename entity tab | `entityTabs[].title` | `entity-content:*` → `config.title` | +| Reorder / group entity tabs | `entityTabs` + `priority` | `page:catalog/entity` → `config.groups` | +| Hide entity tab | Negative `entityTabs` priority | `entity-content:*: false` | +| Bind catalog → TechDocs | `routeBindings` | `app.routes.bindings` | +| Disable route binding | Omit binding | `app.routes.bindings.: false` | + +--- + +## What you cannot do from configuration alone + +- **Attach arbitrary exported components** to mount points without a matching NFS extension from the plugin. +- **Replicate `mountPoints[].config.layout`** grid column positioning — use card `type: info|content` or ask the plugin vendor to adjust the component layout. +- **Add a new entity tab** without a plugin that exports `entity-content:*`. +- **Add cards to General settings** until upstream exposes extension inputs on `sub-page:user-settings/general`. +- **Use RHDH-only mount points** (application drawers, some global header slots) until equivalent NFS extensions exist. + +## RHDH-specific gaps + +| Feature | Status on new frontend system | +| --- | --- | +| Nested sidebar menu groups (`menuItems.parent`) | No direct equivalent — flat nav from pages | +| Application drawer mount points | RHDH-specific — pending NFS design | +| `global.header/help` and similar header slots | Migrating in RHDH global-header plugins | +| `mountPoints[].config.layout` (MUI grid) | Not configurable via YAML | +| Legacy `staticJSXContent` pattern | Requires a plugin update | + +Plugin-side changes are covered in [Migrating Plugins to the New Frontend System](migrating-plugins-to-new-frontend-system.md). + +--- + +## Troubleshooting + +### An extension does not appear + +1. Confirm the plugin is **enabled** in `dynamic-plugins.yaml`. +2. Confirm the plugin supports the **new frontend system** — plugins that only support legacy frontend wiring do not register extensions. +3. Check whether the extension is **disabled** in `app.extensions`. +4. Check **filter** config — entity cards and content hide themselves when the filter does not match the current entity. +5. Use the Backstage **app visualizer** plugin during migration to inspect the extension tree ([migrating apps guide](https://backstage.io/docs/frontend-system/building-apps/migrating/#using-the-app-visualizer-plugin)). + +### My `app.extensions` override has no effect + +- A higher-priority config file may **replace** the entire `app.extensions` array — verify merge order. +- Extension `config` is replaced wholesale; ensure you include all keys you need in the override file. +- Verify the extension ID spelling matches the plugin's registered ID (`entity-card:namespace/name`). + +### Entity tab order looks wrong + +- Configure `page:catalog/entity` → `config.groups` explicitly. +- List `entity-content:*` extensions in `app.extensions` in the desired order within their groups. + +### Catalog page missing Dependencies or Diagram tab + +- Expected on the new frontend system — see [Catalog entity page changes](#catalog-entity-page-changes). Cards moved to Overview; system diagram uses the catalog-graph card and page. + +--- + +## Further reading + +### RHDH documentation + +- [Frontend Plugin Wiring](frontend-plugin-wiring.md) — legacy configuration reference +- [Migrating Plugins to the New Frontend System](migrating-plugins-to-new-frontend-system.md) — plugin author guide +- [Installing Plugins](installing-plugins.md) +- [Version Compatibility Matrix](versions.md) + +### Backstage new frontend system + +- [Frontend System Introduction](https://backstage.io/docs/frontend-system/) +- [Configuring Extensions](https://backstage.io/docs/frontend-system/building-apps/configuring-extensions/) +- [Built-in Extensions](https://backstage.io/docs/frontend-system/building-apps/built-in-extensions/) +- [Frontend Routes](https://backstage.io/docs/frontend-system/architecture/routes/) +- [Example `app-config.yaml`](https://github.com/backstage/backstage/blob/master/app-config.yaml) diff --git a/docs/dynamic-plugins/migrating-plugins-to-new-frontend-system.md b/docs/dynamic-plugins/migrating-plugins-to-new-frontend-system.md new file mode 100644 index 0000000000..ab5e79b8d7 --- /dev/null +++ b/docs/dynamic-plugins/migrating-plugins-to-new-frontend-system.md @@ -0,0 +1,1077 @@ +# Migrating RHDH Frontend Plugins to the Backstage New Frontend System + +This guide helps **plugin authors** who built frontend plugins for Red Hat Developer Hub (RHDH) using the **legacy Backstage frontend system** and **RHDH dynamic plugin wiring** (`dynamicPlugins.frontend` in `app-config.yaml`). It explains how to migrate plugin **code** to the **Backstage new frontend system** — the extension-based architecture documented in the [Frontend System](https://backstage.io/docs/frontend-system/). + +> **Plugin authors** → this document. +> +> **Operators and platform admins** customizing RHDH through YAML only (no plugin source changes) → [Migrating Config to the New Frontend System](migrating-config-to-new-frontend-system.md). + +> **Scope:** This guide covers every configuration case documented in [Frontend Plugin Wiring](frontend-plugin-wiring.md) from a **plugin implementation** perspective. RHDH itself is transitioning to the new frontend system; some RHDH-specific mount points may remain available only in the current dynamic-plugin host until equivalent extensions exist upstream or in RHDH. + +## Who should read this + +- Plugin authors who exported dynamic frontend plugins with `@red-hat-developer-hub/cli plugin export` and wired them via `pluginConfig.dynamicPlugins.frontend`. +- Teams that used `createPlugin`, `createRoutableExtension`, `createComponentExtension`, and similar legacy APIs. +- Overlay maintainers adding or updating `/alpha` exports for dynamic plugin OCI images. + +If you only need to translate existing `dynamic-plugins.yaml` / `app-config` customization into the new frontend system — without changing plugin packages — use [Migrating Config to the New Frontend System](migrating-config-to-new-frontend-system.md) instead. + +## What changes + +| Aspect | RHDH dynamic plugin wiring | New frontend system | +| --- | --- | --- | +| Plugin definition | `createPlugin` + extension helpers in `src/index.ts` | `createFrontendPlugin` in `src/alpha.tsx` | +| Package export | `scalprum.exposedModules` in `package.json` (default `PluginRoot` → `src/index.ts`) | `./alpha` entry point with `createFrontendPlugin` default export | +| Dynamic plugin identity in YAML | `scalprum.name` keys the `dynamicPlugins.frontend` block | Installed package; operators use the package as published | +| Pages / routes | `dynamicRoutes` + optional `menuItem` in config | `PageBlueprint` in plugin code; nav from page `title`/`icon` | +| Entity UI | `mountPoints` + `entityTabs` string IDs | `EntityCardBlueprint`, `EntityContentBlueprint`, etc. | +| Cross-plugin links | `routeBindings` in plugin config | `externalRoutes` in plugin + `app.routes.bindings` | +| APIs | `apiFactories` or `createPlugin({ apis })` | `ApiBlueprint` extensions (auto-discovered) | +| Adopter customization | Per-plugin `dynamicPlugins.frontend.` block | `app.extensions`, `app.routes.bindings`, `app.packages` | +| Wiring location | Mostly **configuration** (YAML) | Mostly **plugin code** (extensions), with **optional** `app.extensions` overrides | + +The biggest mindset shift: in RHDH dynamic wiring, adopters declare *what component goes where* in YAML. In the new frontend system, plugins declare *where they attach* in code (via extension blueprints), and adopters override *behavior* (titles, filters, ordering, disable) through `app.extensions`. + +## Before you begin + +1. **Keep legacy exports during migration.** Unless your plugin is private to a single app, add the new frontend system under `src/alpha.tsx` and export it from `./alpha` in `package.json`. Keep the existing default export for backward compatibility until RHDH and your consumers no longer need it. + +2. **Read the upstream migration guides:** + - [Migrating Plugins](https://backstage.io/docs/frontend-system/building-plugins/migrating/) + - [Migrating Apps](https://backstage.io/docs/frontend-system/building-apps/migrating/) + - [Configuring Extensions](https://backstage.io/docs/frontend-system/building-apps/configuring-extensions/) + +3. **Study a migrated plugin.** Good references in the Backstage repo: `@backstage/plugin-catalog/alpha`, `@backstage/plugin-techdocs/alpha`, `@backstage/plugin-scaffolder/alpha`. + +4. **Check the [version matrix](versions.md)** when re-exporting dynamic plugin packages with the RHDH CLI. + +## Migration strategy + +We recommend a three-phase approach: + +### Phase 1 — Add `/alpha` without changing RHDH wiring + +1. Create `src/alpha.tsx` with `createFrontendPlugin` and extension blueprints. +2. Export `./alpha` from `package.json`. +3. Verify the plugin builds (`yarn tsc`) and unit tests pass. + +The legacy `PluginRoot` export can remain the entry point for the current RHDH dynamic plugin host. + +### Phase 2 — Move wiring from YAML into plugin extensions + +For each item in your `dynamicPlugins.frontend.` config, implement the equivalent extension in `src/alpha.tsx` so the plugin is self-describing. Remove redundant YAML once the new path is validated. + +### Phase 3 — Validate in a new-frontend-system app + +1. Add the plugin as an app dependency (or install manually via `createApp({ features: [...] })`). +2. Set `app.packages: all` (or include your package explicitly). +3. Use `app.extensions` only for adopter-specific overrides (titles, filters, ordering). +4. Run the app and verify routes, entity tabs, APIs, and cross-plugin links. + +--- + +## Plugin code migration primer + +### Legacy plugin + +```typescript +// src/plugin.ts +import { createPlugin, createRoutableExtension } from '@backstage/core-plugin-api'; + +export const myPlugin = createPlugin({ + id: 'my-plugin', + apis: [myApiFactory], + routes: { root: rootRouteRef }, + externalRoutes: { entityPage: entityPageExternalRouteRef }, +}); + +export const MyPage = myPlugin.provide( + createRoutableExtension({ + name: 'MyPage', + component: () => import('./components/MyPage').then(m => m.MyPage), + mountPoint: rootRouteRef, + }), +); +``` + +### New frontend system plugin + +```tsx +// src/alpha.tsx +import { + createFrontendPlugin, + PageBlueprint, + ApiBlueprint, +} from '@backstage/frontend-plugin-api'; +import { rootRouteRef, entityPageExternalRouteRef } from './routes'; + +const myApi = ApiBlueprint.make({ + params: defineParams => + defineParams({ + api: myApiRef, + deps: { /* ... */ }, + factory: ({ /* ... */ }) => new MyApiImpl(), + }), +}); + +const myPage = PageBlueprint.make({ + params: { + path: '/my-plugin', + routeRef: rootRouteRef, + title: 'My Plugin', + icon: , + loader: () => import('./components/MyPage').then(m => ), + }, +}); + +export default createFrontendPlugin({ + pluginId: 'my-plugin', + extensions: [myApi, myPage], + routes: { root: rootRouteRef }, + externalRoutes: { entityPage: entityPageExternalRouteRef }, +}); +``` + +```json +// package.json exports +{ + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.tsx", + "./package.json": "./package.json" + } +} +``` + +--- + +## Configuration migration reference + +Each subsection maps one topic from [Frontend Plugin Wiring](frontend-plugin-wiring.md) to the new frontend system. Sections follow the same order as that document. + +### Extend internal library of available icons (`appIcons`) + +**RHDH wiring:** + +```yaml +dynamicPlugins: + frontend: + my.package: + appIcons: + - name: fooIcon + importName: FooIcon +``` + +**New approach — plugin code:** + +Register icons with `IconBundleBlueprint` from `@backstage/plugin-app-react`: + +```tsx +import { IconBundleBlueprint } from '@backstage/plugin-app-react'; + +const myIcons = IconBundleBlueprint.make({ + params: { + icons: { + fooIcon: , + }, + }, +}); + +// Add myIcons to createFrontendPlugin({ extensions: [...] }) +``` + +**Adopter configuration:** Usually none. Icons are discovered with the plugin. Pages and entity tabs can reference icon IDs as strings in `app.extensions` config when icon bundles are installed. + +**Notes:** + +- RHDH `appIcons` registered icons globally for `menuItem.icon` string references. In the new system, prefer `PageBlueprint` `icon` params (component) or registered icon bundle IDs. +- See [Icons](https://backstage.io/docs/conf/user-interface/icons/) and [IconBundleBlueprint](https://backstage.io/docs/frontend-system/building-plugins/common-extension-blueprints/). + +--- + +### Dynamic routes (`dynamicRoutes`) + +**RHDH wiring:** + +```yaml +dynamicPlugins: + frontend: + my.package: + dynamicRoutes: + - path: /my-plugin + importName: MyPage + menuItem: + icon: docs + text: My Plugin +``` + +**New approach — plugin code:** + +```tsx +const myPage = PageBlueprint.make({ + params: { + path: '/my-plugin', + routeRef: rootRouteRef, + title: 'My Plugin', + icon: , + loader: () => import('./components/MyPage').then(m => ), + }, +}); +``` + +**Adopter configuration:** + +```yaml +app: + extensions: + - page:my-plugin: + config: + title: Custom Title + path: /custom-path # when blueprint supports path override +``` + +**Notes:** + +- You no longer declare `importName` in YAML — the `loader` in the blueprint points at your component. +- Sidebar entries are inferred from page extensions. `NavItemBlueprint` was removed; do not create separate nav-item extensions. +- Custom `SidebarItem` components from RHDH `menuItem.importName` have no direct equivalent — use `NavContentBlueprint` to replace the entire sidebar, or keep the default nav item styling. +- Sub-routes within a page use `SubPageBlueprint` (see scaffolder templates as an example). + +--- + +### Menu items (`menuItems`) + +**RHDH wiring:** + +```yaml +dynamicPlugins: + frontend: + my.package: + menuItems: + my-plugin: + priority: 10 + parent: favorites + favorites: + title: Favorites + priority: 100 +``` + +**New approach:** + +| RHDH feature | New frontend system equivalent | +| --- | --- | +| `menuItem.text` / `title` | `PageBlueprint` `title` param or `app.extensions` `config.title` | +| `menuItem.icon` | `PageBlueprint` `icon` param or `config` icon ID | +| `priority` | Order entries in `app.extensions` (listed extensions render first, in list order) | +| Nested `parent` menus | No built-in equivalent yet — use `NavContentBlueprint` for custom sidebar layout | + +**Adopter configuration:** + +```yaml +app: + extensions: + - page:catalog + - page:my-plugin + - page:scaffolder + # Order above controls sidebar order for listed pages +``` + +**Notes:** + +- Nested sidebar groups (up to 3 levels) are an RHDH dynamic-plugin feature. Standard Backstage new-frontend-system apps use a flat nav derived from pages unless you customize `app/nav` via `NavContentBlueprint`. + +--- + +### Bind to existing plugins (`routeBindings`) + +**RHDH wiring:** + +```yaml +dynamicPlugins: + frontend: + my.package: + routeBindings: + targets: + - importName: barPlugin + bindings: + - bindTarget: barPlugin.externalRoutes + bindMap: + headerLink: fooPlugin.routes.root +``` + +**New approach — plugin code:** + +Declare `externalRoutes` on the frontend plugin: + +```tsx +export default createFrontendPlugin({ + pluginId: 'bar', + externalRoutes: { + headerLink: headerLinkExternalRouteRef, + }, + routes: { + root: rootRouteRef, + }, + // ... +}); +``` + +**Adopter configuration:** + +```yaml +app: + routes: + bindings: + bar.headerLink: foo.root + scaffolder.registerComponent: false +``` + +Or programmatically in `createApp({ bindRoutes({ bind }) { ... } })`. + +**Notes:** + +- Binding syntax uses `pluginId.routeName` (for example `catalog.viewTechDoc: techdocs.docRoot`). +- External route refs can declare `defaultTarget` in plugin code to reduce required app config. +- See [Frontend Routes](https://backstage.io/docs/frontend-system/architecture/routes/). + +--- + +### Using mount points — entity page cards (`entity.page.*/cards`) + +**RHDH wiring:** + +```yaml +mountPoints: + - mountPoint: entity.page.overview/cards + importName: MyOverviewCard + config: + layout: + gridColumn: "1 / -1" + if: + allOf: + - isKind: component +``` + +**New approach — plugin code:** + +```tsx +import { EntityCardBlueprint } from '@backstage/plugin-catalog-react/alpha'; + +const myOverviewCard = EntityCardBlueprint.make({ + name: 'my-overview', + params: { + filter: { kind: 'component' }, + loader: async () => { + const { MyOverviewCard } = await import('./components/MyOverviewCard'); + return ; + }, + }, +}); +``` + +Default attachment: `entity-content:catalog/overview` input `cards`. + +**Adopter configuration:** + +```yaml +app: + extensions: + - entity-card:my-plugin/my-overview: + config: + filter: + kind: component + type: info +``` + +**Notes:** + +- `EntityCardBlueprint` replaces `createEntityCardExtension` / mount-point card wiring. +- Layout grid positioning from RHDH `config.layout` is not a standard blueprint config — implement layout inside your card component or use extension overrides for advanced cases. +- Filter predicates use the [filter predicate](https://backstage.io/docs/reference/filter-predicates) schema in config (not the RHDH `isKind`/`isType` shorthand, though similar concepts apply). + +--- + +### Using mount points — entity page tab content (`entity.page.*` without `/cards`) + +**RHDH wiring:** + +```yaml +mountPoints: + - mountPoint: entity.page.docs/cards + importName: EntityTechdocsContent +``` + +For full tabs, RHDH often combines `entityTabs` + mount points (see below). + +**New approach — plugin code:** + +```tsx +import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha'; + +const myEntityContent = EntityContentBlueprint.make({ + name: 'my-tab', + params: { + path: 'my-tab', + title: 'My Tab', + group: 'development', + routeRef: myEntityRouteRef, // optional, for routable content + loader: () => import('./MyTab').then(m => ), + }, +}); +``` + +Attaches to `page:catalog/entity` input `contents`. + +**Adopter configuration:** + +```yaml +app: + extensions: + - entity-content:my-plugin/my-tab: + config: + title: Renamed Tab + group: deployment + icon: kubernetes +``` + +--- + +### Using mount points — entity context menu (`entity.context.menu`) + +**RHDH wiring:** + +```yaml +mountPoints: + - mountPoint: entity.context.menu + importName: SimpleDialog + config: + props: + title: Open Simple Dialog + icon: dialogIcon +``` + +**New approach — plugin code:** + +```tsx +import { EntityContextMenuItemBlueprint } from '@backstage/plugin-catalog-react/alpha'; + +const myMenuItem = EntityContextMenuItemBlueprint.make({ + name: 'my-action', + params: { + icon: , + title: 'Open Simple Dialog', + onClick: ({ entity, dialogApi }) => { /* ... */ }, + // or href: ... + filter: entity => entity.kind === 'Component', + }, +}); +``` + +**Adopter configuration:** Typically none — menu items are defined in the plugin. + +**Notes:** + +- The new system uses data-driven menu item extensions, not dialog wrapper components with `open`/`onClose` props. Refactor dialog lifecycle into your blueprint's `onClick` handler. + +--- + +### Using mount points — search page (`search.page.*`) + +**RHDH wiring:** + +| Mount point | Purpose | +| --- | --- | +| `search.page.types` | Search result type tabs | +| `search.page.filters` | Filter controls | +| `search.page.results` | Result list item renderers | + +**New approach — plugin code:** + +| Mount point | Blueprint | Attaches to | +| --- | --- | --- | +| Result items | `SearchResultListItemBlueprint` | `page:search` input `items` | +| Filters | `SearchFilterBlueprint` | `page:search` input `filters` | +| Result types | `SearchFilterResultTypeBlueprint` | `page:search` input `types` | + +Example: + +```tsx +import { SearchResultListItemBlueprint } from '@backstage/plugin-search-react/alpha'; + +const mySearchItem = SearchResultListItemBlueprint.make({ + params: { + predicate: result => result.type === 'my-type', + component: async () => { + const { MyResultItem } = await import('./MyResultItem'); + return props => ; + }, + }, +}); +``` + +**Adopter configuration:** + +```yaml +app: + extensions: + - search-result-list-item:my-plugin: + config: + title: Custom Label +``` + +--- + +### Adding application header (`application/header`) + +**RHDH wiring:** + +```yaml +mountPoints: + - mountPoint: application/header + importName: GlobalHeader + config: + position: above-main-content +``` + +**New approach:** + +Use `AppRootElementBlueprint` or `AppRootWrapperBlueprint` from `@backstage/plugin-app-react`, attached to `app/root`: + +```tsx +import { AppRootElementBlueprint } from '@backstage/plugin-app-react'; + +const myHeader = AppRootElementBlueprint.make({ + name: 'my-header', + params: { + loader: async () => { + const { GlobalHeader } = await import('./GlobalHeader'); + return ; + }, + }, +}); +``` + +**Notes:** + +- RHDH's global header plugin (`red-hat-developer-hub.backstage-plugin-global-header`) is being migrated to extension blueprints in `rhdh-plugins`. The `position: above-main-content` concept is app-layout-specific — verify layout behavior when migrating. +- Multiple headers may require coordination via extension ordering in `app.extensions`. + +--- + +### Adding application listeners (`application/listener`) + +**RHDH wiring:** + +```yaml +mountPoints: + - mountPoint: application/listener + importName: MyListener +``` + +**New approach:** + +Use `AppRootElementBlueprint` (renders outside the main layout, alongside alert display and OAuth dialog): + +```tsx +const myListener = AppRootElementBlueprint.make({ + name: 'my-listener', + params: { + loader: async () => { + const { MyListener } = await import('./MyListener'); + return ; + }, + }, +}); +``` + +--- + +### Adding application providers (`application/provider`) + +**RHDH wiring:** + +```yaml +mountPoints: + - mountPoint: application/provider + importName: MyProvider +``` + +**New approach:** + +Use `AppRootWrapperBlueprint` or `PluginWrapperBlueprint`: + +```tsx +import { AppRootWrapperBlueprint } from '@backstage/plugin-app-react'; + +const myProvider = AppRootWrapperBlueprint.make({ + params: { + component: MyProvider, // wraps the app root + }, +}); +``` + +For plugin-scoped providers (only wrap that plugin's UI): + +```tsx +import { PluginWrapperBlueprint } from '@backstage/frontend-plugin-api/alpha'; + +const myPluginWrapper = PluginWrapperBlueprint.make({ + params: { component: MyPluginProvider }, +}); +``` + +--- + +### Adding application drawers (`application/internal/drawer-*`) + +**RHDH wiring:** Uses `application/provider`, `application/internal/drawer-state`, and `application/internal/drawer-content` mount points with RHDH-coordinated drawer management. + +**Status:** This is an **RHDH-specific** integration pattern. There is no direct equivalent in the upstream new frontend system today. Options: + +1. Keep using RHDH dynamic plugin wiring for drawer plugins until RHDH provides a new-frontend-system drawer blueprint. +2. Reimplement drawer UX with `AppRootElementBlueprint` / layout overrides if targeting a standard Backstage app. + +See the note in [Frontend Plugin Wiring — Adding application drawers](frontend-plugin-wiring.md#adding-application-drawers). + +--- + +### Customizing and adding entity tabs (`entityTabs`) + +**RHDH wiring:** + +```yaml +entityTabs: + - path: /new-path + title: My New Tab + mountPoint: entity.page.my-new-tab + - path: / + title: General + mountPoint: entity.page.overview + priority: -6 +``` + +**New approach — plugin code (new tab):** + +Define an `EntityContentBlueprint` (see above). The tab appears when the extension is installed and its entity filter matches. + +**New approach — adopter config (rename/reorder/hide groups):** + +```yaml +app: + extensions: + - page:catalog/entity: + config: + showNavItemIcons: true + groups: + - overview: + title: General + - documentation: + title: Docs + - development: false # hide a default group + - custom: + title: My New Tab + - entity-content:my-plugin/my-tab: + config: + title: My New Tab + group: custom +``` + +Default groups: `overview`, `documentation`, `development`, `deployment`, `operation`, `observability`. + +**Mapping RHDH mount points to entity content groups:** + +| RHDH mount point prefix | Typical `group` value | +| --- | --- | +| `entity.page.overview` | `overview` | +| `entity.page.docs` | `documentation` | +| `entity.page.ci`, `entity.page.cd`, `entity.page.kubernetes`, etc. | `development` or `deployment` | +| Custom `entity.page.my-new-tab` | Custom group id in `page:catalog/entity` `groups` config | + +**Notes:** + +- RHDH `entityTabs` created mount point namespaces dynamically. In the new system, you define content extensions and assign them to groups explicitly. +- Negative `priority` to hide tabs maps to `group: false` on content extensions or disabling groups in `page:catalog/entity` config. + +--- + +### Translation resources (`translationResources`) + +**RHDH wiring:** + +```yaml +translationResources: + - importName: myTranslationRef +``` + +**New approach — plugin code:** + +```tsx +import { TranslationBlueprint } from '@backstage/plugin-app-react'; +import { myTranslationRef } from './translation'; + +const myTranslations = TranslationBlueprint.make({ + params: { + resource: myTranslationRef, + }, +}); +``` + +**Adopter configuration:** Override messages via additional `TranslationBlueprint` extensions or JSON translation resources attached to the app. + +See [Migrating Apps — Translations](https://backstage.io/docs/frontend-system/building-apps/migrating/#translations). + +--- + +### Provide additional Utility APIs (`apiFactories`) + +**RHDH wiring (explicit):** + +```yaml +apiFactories: + - importName: customScmAuthApiFactory +``` + +**RHDH wiring (implicit):** Export `createPlugin` from `PluginRoot` — APIs auto-register. + +**New approach — plugin code:** + +```tsx +const customScmAuthApi = ApiBlueprint.make({ + params: defineParams => + defineParams({ + api: scmAuthApiRef, + deps: { githubAuthApi: githubAuthApiRef }, + factory: ({ githubAuthApi }) => ScmAuth.forGithub(githubAuthApi), + }), +}); +``` + +APIs attach to the `app` extension `apis` input and are auto-discovered when the plugin is installed. + +**Adopter configuration (override API behavior):** + +```yaml +app: + extensions: + - api:core.auth.github: + config: + # extension-specific config when supported +``` + +To replace an API implementation entirely, use [extension overrides](https://backstage.io/docs/frontend-system/architecture/extension-overrides/) in a frontend module. + +**Notes:** + +- Empty `dynamicPlugins.frontend.my-package: {}` is no longer needed — installing the plugin dependency is sufficient. +- API ref IDs map to extension IDs: `api:` (see [Configuring Utility APIs](https://backstage.io/docs/frontend-system/utility-apis/configuring/)). + +--- + +### Adding custom authentication provider settings (`providerSettings`) + +**RHDH wiring:** + +```yaml +providerSettings: + - title: My Custom Auth Provider + description: Sign in using My Custom Auth Provider + provider: core.auth.my-custom-auth-provider +``` + +**New approach — plugin code:** + +Attach a provider settings UI element to the user-settings auth sub-page: + +```tsx +import { createExtension } from '@backstage/frontend-plugin-api'; +import { coreExtensionData } from '@backstage/frontend-plugin-api'; +import { ProviderSettingsItem } from '@backstage/plugin-user-settings'; + +const myProviderSettings = createExtension({ + kind: 'auth-provider-settings', + name: 'my-custom', + attachTo: { id: 'sub-page:user-settings/auth-providers', input: 'providerSettings' }, + output: [coreExtensionData.reactElement], + factory: () => [ + coreExtensionData.reactElement( + , + ), + ], +}); +``` + +Also provide `ApiBlueprint` for the auth API and `SignInPageBlueprint` if needed. + +--- + +### Use a custom SignInPage component (`signInPage`) + +**RHDH wiring:** + +```yaml +signInPage: + importName: CustomSignInPage +``` + +**New approach — plugin code:** + +```tsx +import { SignInPageBlueprint } from '@backstage/plugin-app-react'; + +const customSignInPage = SignInPageBlueprint.make({ + params: { + loader: async () => { + const { CustomSignInPage } = await import('./CustomSignInPage'); + return CustomSignInPage; + }, + }, +}); +``` + +**Notes:** + +- Only one sign-in page extension can be active. Installing multiple replaces based on extension override rules. +- See [Migrating Apps — Sign-in page](https://backstage.io/docs/frontend-system/building-apps/migrating/). + +--- + +### Provide custom Scaffolder field extensions (`scaffolderFieldExtensions`) + +**RHDH wiring:** + +```yaml +scaffolderFieldExtensions: + - importName: MyNewFieldExtension +``` + +**New approach — plugin code:** + +```tsx +import { FormFieldBlueprint } from '@backstage/plugin-scaffolder-react/alpha'; + +export const myField = FormFieldBlueprint.make({ + name: 'MyCustomField', + params: { + schema: { /* JSON schema fragment */ }, + loader: async () => { + const { MyCustomField } = await import('./MyCustomField'); + return MyCustomField; + }, + }, +}); +``` + +Fields are auto-discovered via `formFieldsApiRef` when the plugin is installed. No YAML registration required. + +**Adopter configuration:** None for registration. Template authors use the field name in `template.yaml` as before. + +--- + +### Provide custom TechDocs addons (`techdocsAddons`) + +**RHDH wiring:** + +```yaml +techdocsAddons: + - importName: ExampleAddon + config: + props: ... +``` + +**New approach — plugin code:** + +```tsx +import { AddonBlueprint } from '@backstage/plugin-techdocs-react/alpha'; + +const exampleAddon = AddonBlueprint.make({ + name: 'example', + params: { + location: TechDocsAddonLocations.Content, + component: ExampleTestAddon, + }, +}); +``` + +Addons are collected via `techdocsAddonsApiRef` and merged into TechDocs reader and entity content extensions automatically. + +**Notes:** + +- The older pattern of injecting addons through `staticJSXContent` in dynamic plugin exports (see [Export Derived Package](export-derived-package.md)) is specific to the legacy dynamic plugin host. Prefer `AddonBlueprint` for new development. + +--- + +### Add a custom Backstage theme or replace the provided theme (`themes`) + +**RHDH wiring:** + +```yaml +themes: + - id: light + title: Light + variant: light + icon: someIconReference + importName: lightThemeProvider +``` + +**New approach — plugin code:** + +```tsx +import { ThemeBlueprint } from '@backstage/plugin-app-react'; +import { lightTheme } from './lightTheme'; + +const customLightTheme = ThemeBlueprint.make({ + name: 'light', + params: { + theme: lightTheme, + title: 'Light', + variant: 'light', + icon: , + }, +}); +``` + +Use `name: 'light'` or `name: 'dark'` to override the built-in themes. + +**Adopter configuration:** + +```yaml +app: + extensions: + - theme:my-plugin/light: + config: + title: Corporate Light +``` + +--- + +## Operator configuration + +YAML-only customization (disable a card, reorder tabs, rename nav items, and so on) is documented in [Migrating Config to the New Frontend System](migrating-config-to-new-frontend-system.md). Plugin authors should still understand that adopters will use `app.extensions` for overrides once plugins export `/alpha` extensions. + +--- + +## Dynamic plugin export considerations + +When continuing to ship OCI dynamic plugin images during migration: + +### Legacy `scalprum` configuration in `package.json` + +Legacy RHDH frontend dynamic plugins are built as a Webpack module-federation container. The RHDH CLI reads a `scalprum` section in the derived package's `package.json` to control the container name and which source files are exposed as entrypoints. See [Export Derived Dynamic Plugin Package](export-derived-package.md) for the full export workflow. + +Default configuration (often generated by the CLI when none is present): + +```json +{ + "scalprum": { + "name": "", + "exposedModules": { + "PluginRoot": "./src/index.ts" + } + } +} +``` + +| Field | Purpose | +| --- | --- | +| `scalprum.name` | Webpack container name. This is also the key used under `dynamicPlugins.frontend` in operator configuration — it may differ from the npm package name if you customize it. | +| `scalprum.exposedModules` | Maps **module names** to source entrypoints. Each key becomes a loadable entrypoint in the dynamic plugin bundle. | + +Legacy [Frontend Plugin Wiring](frontend-plugin-wiring.md) references these modules: + +- `module` — optional; selects which `exposedModules` key to load (defaults to `PluginRoot`). +- `importName` — optional; which named export to render from that module (defaults to the module's default export). + +Example with multiple exposed modules: + +```json +{ + "scalprum": { + "name": "custom-package-name", + "exposedModules": { + "PluginRoot": "./src/index.ts", + "FooModuleName": "./src/foo.ts" + } + } +} +``` + +Corresponding legacy wiring: + +```yaml +dynamicPlugins: + frontend: + custom-package-name: + mountPoints: + - mountPoint: entity.page.overview/cards + module: FooModuleName + importName: MyCard +``` + +**During migration:** keep `scalprum` configuration working for any deployment still on the legacy frontend host. Add a `./alpha` export in `package.json` for the new frontend system. The new path does not use `importName` / `module` in operator YAML — extensions are registered by the `createFrontendPlugin` default export from `./alpha`. + +You can customize `scalprum` in `package.json` directly or via the CLI `--scalprum-config` option (see [export-derived-package.md](export-derived-package.md)). + +### Export checklist + +1. **Dual exports:** Keep legacy `scalprum.exposedModules` (typically `PluginRoot` → `src/index.ts`) during transition; add `./alpha` for the new frontend system. +2. **Import names in YAML:** Legacy wiring uses `importName` and optional `module` to resolve exports from `exposedModules`. New frontend system plugins are self-describing — the `./alpha` export registers extensions without per-feature YAML wiring. +3. **Reduce YAML surface:** As you migrate each feature to extensions in plugin code, delete the corresponding `dynamicPlugins.frontend` keys from `pluginConfig` in `dynamic-plugins.yaml`. Fewer moving parts means fewer export-time surprises. +4. Re-export with a CLI version from the [version matrix](versions.md). + +--- + +## Verification checklist + +Use this checklist before declaring migration complete: + +- [ ] Legacy `scalprum.exposedModules` still resolves all `importName` / `module` references used in existing operator config (during dual-export period) +- [ ] `src/alpha.tsx` exports `createFrontendPlugin` as default from `./alpha` +- [ ] All former `dynamicRoutes` are `PageBlueprint` / `SubPageBlueprint` extensions +- [ ] All former `mountPoints` map to the correct blueprint (`EntityCard`, `EntityContent`, `SearchResultListItem`, etc.) +- [ ] `externalRoutes` declared on plugin; app-level bindings documented for adopters +- [ ] APIs migrated to `ApiBlueprint` (no `apiFactories` YAML needed) +- [ ] Scaffolder fields use `FormFieldBlueprint` (if applicable) +- [ ] TechDocs addons use `AddonBlueprint` (if applicable) +- [ ] Themes / translations use `ThemeBlueprint` / `TranslationBlueprint` (if applicable) +- [ ] Sign-in and provider settings use `SignInPageBlueprint` / auth settings extensions (if applicable) +- [ ] Plugin works when added as dependency to `packages/app` with `app.packages: all` +- [ ] Adopter overrides tested via `app.extensions` (title, filter, disable) +- [ ] Dynamic plugin OCI image rebuilt and smoke-tested in RHDH (if still distributing as dynamic plugin) + +--- + +## RHDH-specific features without upstream equivalents (yet) + +| Feature | Status | +| --- | --- | +| Nested sidebar menu groups (`menuItems.parent`) | RHDH dynamic plugins only — use `NavContentBlueprint` for custom nav upstream | +| Application drawer mount points | RHDH-specific — pending new frontend system design | +| `global.header/help` and similar RHDH header slots | Being migrated in `rhdh-plugins` global-header workspace | +| RHDH `mountPoints[].config.layout` grid SX | Implement in component CSS or card wrapper | +| `staticJSXContent` dynamic plugin pattern | Legacy dynamic host — replace with extension inputs / Utility APIs | + +--- + +## Further reading + +### RHDH documentation + +- [Migrating Config to the New Frontend System](migrating-config-to-new-frontend-system.md) — operator guide for YAML customization +- [Frontend Plugin Wiring](frontend-plugin-wiring.md) — legacy RHDH dynamic plugin configuration reference +- [Export Derived Dynamic Plugin Package](export-derived-package.md) +- [Installing Plugins](installing-plugins.md) +- [Version Compatibility Matrix](versions.md) + +### Backstage new frontend system + +- [Frontend System Introduction](https://backstage.io/docs/frontend-system/) +- [Migrating Plugins](https://backstage.io/docs/frontend-system/building-plugins/migrating/) +- [Migrating Apps](https://backstage.io/docs/frontend-system/building-apps/migrating/) +- [Configuring Extensions](https://backstage.io/docs/frontend-system/building-apps/configuring-extensions/) +- [Common Extension Blueprints](https://backstage.io/docs/frontend-system/building-plugins/common-extension-blueprints/) +- [Example `app-config.yaml`](https://github.com/backstage/backstage/blob/master/app-config.yaml)