feat(scaffolder): config-driven template groups and swappable TemplateCard (#34147)

* feat(scaffolder): config-driven template groups and swappable TemplateCard

Signed-off-by: benjdlambert <ben@blam.sh>

* refactor(scaffolder): keep createGroupsWithOther internal

Signed-off-by: benjdlambert <ben@blam.sh>

* docs(scaffolder): fix sub-page extension ID in changeset

Signed-off-by: benjdlambert <ben@blam.sh>

* address PR review feedback

Signed-off-by: benjdlambert <ben@blam.sh>

* split TemplateCard swappable contract from legacy props

Signed-off-by: benjdlambert <ben@blam.sh>

* address review feedback: dedupe tags, defensive groups copy, doc clarifications

Signed-off-by: benjdlambert <ben@blam.sh>

* regenerate api reports

Signed-off-by: benjdlambert <ben@blam.sh>

* align docs and changeset with actual default group titles

Signed-off-by: benjdlambert <ben@blam.sh>

* regen api reports after rebase

Signed-off-by: benjdlambert <ben@blam.sh>

---------

Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
Ben Lambert
2026-05-12 12:29:44 +02:00
committed by GitHub
parent 92dfe61e79
commit d09c21cb84
23 changed files with 656 additions and 135 deletions
@@ -109,6 +109,10 @@ Default secrets are resolved from environment variables and accessible via `${{
## Customizing the ScaffolderPage with Grouping and Filtering
The sections below cover the legacy (JSX) frontend system. For the new
frontend system, see [Customizing the templates page in the new frontend system](#customizing-the-templates-page-in-the-new-frontend-system)
below.
Once you have more than a few software templates you may want to customize your
`ScaffolderPage` by grouping and surfacing certain templates together. You can
accomplish this by creating `groups` and passing them to your `ScaffolderPage`
@@ -149,3 +153,74 @@ You can have several use cases for that:
}
/>
```
## Customizing the templates page in the new frontend system
In the new frontend system the templates page is built from extensions, so
customisations are configured rather than passed as JSX props.
### Defining template groups in `app-config.yaml`
The `sub-page:scaffolder/templates` extension accepts a `groups` config field.
Each group has a `title` and a `filter` predicate (using
[entity predicate queries](https://backstage.io/docs/features/software-catalog/catalog-customization#entity-predicate-queries)).
Templates not matched by any group fall into an automatically appended
"Other Templates" group. With no groups configured the page renders a single
"Templates" group.
```yaml
app:
extensions:
- sub-page:scaffolder/templates:
config:
groups:
- title: Recommended Services
filter:
spec.type: service
- title: Documentation
filter:
spec.type: documentation
```
Predicate values are matched case-insensitively. The matchers `$exists`,
`$in`, `$contains`, `$hasPrefix` and the logical operators `$all`, `$any`, `$not`
are also supported — see the
[entity predicate queries reference](https://backstage.io/docs/features/software-catalog/catalog-customization#entity-predicate-queries)
for the full grammar.
### Replacing the default `TemplateCard`
The `TemplateCard` exported from `@backstage/plugin-scaffolder-react/alpha`
is a swappable component. Apps can replace it by registering a
`SwappableComponentBlueprint` extension that targets `TemplateCard`:
```tsx
// packages/app/src/modules/appModuleScaffolder.tsx
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { SwappableComponentBlueprint } from '@backstage/plugin-app-react';
import { TemplateCard } from '@backstage/plugin-scaffolder-react/alpha';
export const appModuleScaffolder = createFrontendModule({
pluginId: 'app',
extensions: [
SwappableComponentBlueprint.make({
name: 'scaffolder-template-card',
params: defineParams =>
defineParams({
component: TemplateCard,
loader: () => import('./MyTemplateCard').then(m => m.MyTemplateCard),
}),
}),
],
});
```
Wire the module into your app by adding `appModuleScaffolder` to the
`features` array of `createApp` in `packages/app/src/App.tsx`.
`MyTemplateCard` receives `TemplateCardComponentProps`
(`{ template, additionalLinks?, onSelected? }`). The list takes care of
binding the template to `onSelected`, so the card just calls
`props.onSelected?.()` to choose itself. The example app under
`packages/app/src/modules/BuiTemplateCard.tsx` shows a Backstage UI (BUI)
implementation you can use as a starting point.