docs: migrate feature docs to new frontend system as primary content

Rewrite documentation for TechDocs, Software Templates, Software Catalog,
Search, and Kubernetes features to use the new frontend system as the
primary installation and configuration instructions. Old frontend system
instructions are moved to separate `--old` suffixed files for pages with
substantial legacy content, or updated inline for pages with minimal
old-system content.

Files migrated:
- techdocs/getting-started.md
- techdocs/how-to-guides.md
- software-templates/writing-custom-step-layouts.md
- software-templates/writing-custom-field-extensions.md
- software-templates/index.md
- software-catalog/catalog-customization.md
- search/getting-started.md
- search/how-to-guides.md
- kubernetes/installation.md

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-03-29 21:47:46 +02:00
parent 0336f92de8
commit 8ec254cd4b
18 changed files with 3269 additions and 1397 deletions
+2 -18
View File
@@ -85,25 +85,9 @@ Once the template has finished running, and from the screenshot above, when its
There could be situations where you would like to disable the
`Register Existing Component` button for your users.
To do so, you need to explicitly disable the default route binding from the `scaffolderPlugin.registerComponent` to the Catalog Import page.
To do so, you can disable the route binding in your `app-config.yaml`:
This can be done in `backstage/packages/app/src/App.tsx`:
```diff
const app = createApp({
apis,
bindRoutes({ bind }) {
bind(scaffolderPlugin.externalRoutes, {
+ registerComponent: false,
- registerComponent: catalogImportPlugin.routes.importPage,
viewTechDoc: techdocsPlugin.routes.docRoot,
});
})
```
OR in `app-config.yaml`:
```yaml
```yaml title="app-config.yaml"
app:
routes:
bindings:
@@ -0,0 +1,332 @@
---
id: writing-custom-field-extensions--old
title: Writing Custom Field Extensions (Old Frontend System)
description: How to write your own field extensions
---
::::info
This documentation is for Backstage apps that still use the old frontend
system. If your app uses the new frontend system, read the
[current guide](./writing-custom-field-extensions.md) instead.
::::
Collecting input from the user is a very large part of the scaffolding process
and Software Templates as a whole. Sometimes the built in components and fields
just aren't good enough, and sometimes you want to enrich the form that the
users sees with better inputs that fit better.
This is where `Custom Field Extensions` come in.
With them you can show your own `React` Components and use them to control the
state of the JSON schema, as well as provide your own validation functions to
validate the data too.
## Creating a Field Extension
Field extensions are a way to combine an ID, a `React` Component and a
`validation` function together in a modular way that you can then use to pass to
the `Scaffolder` frontend plugin in your own `App.tsx`.
You can create your own Field Extension by using the
[`createScaffolderFieldExtension`](https://backstage.io/api/stable/variables/_backstage_plugin-scaffolder.index.createScaffolderFieldExtension.html)
`API` like below.
As an example, we will create a component that validates whether a string is in the `Kebab-case` pattern:
```tsx
//packages/app/src/scaffolder/ValidateKebabCase/ValidateKebabCaseExtension.tsx
import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';
import type { FieldValidation } from '@rjsf/utils';
import FormControl from '@material-ui/core/FormControl';
import FormHelperText from '@material-ui/core/FormHelperText';
import Input from '@material-ui/core/Input';
import InputLabel from '@material-ui/core/InputLabel';
/*
This is the actual component that will get rendered in the form
*/
export const ValidateKebabCase = ({
onChange,
rawErrors,
required,
formData,
}: FieldExtensionComponentProps<string>) => {
return (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !formData}
>
<InputLabel htmlFor="validateName">Name</InputLabel>
<Input
id="validateName"
aria-describedby="entityName"
onChange={e => onChange(e.target?.value)}
/>
<FormHelperText id="entityName">
Use only letters, numbers, hyphens and underscores
</FormHelperText>
</FormControl>
);
};
/*
This is a validation function that will run when the form is submitted.
You will get the value from the `onChange` handler before as the value here to make sure that the types are aligned\
*/
export const validateKebabCaseValidation = (
value: string,
validation: FieldValidation,
) => {
const kebabCase = /^[a-z0-9-_]+$/g.test(value);
if (kebabCase === false) {
validation.addError(
`Only use letters, numbers, hyphen ("-") and underscore ("_").`,
);
}
};
```
```tsx
// packages/app/src/scaffolder/ValidateKebabCase/extensions.ts
/*
This is where the magic happens and creates the custom field extension.
Note that if you're writing extensions part of a separate plugin,
then please use `scaffolderPlugin.provide` from there instead and export it part of your `plugin.ts` rather than re-using the `scaffolder.plugin`.
*/
import { scaffolderPlugin } from '@backstage/plugin-scaffolder';
import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder-react';
import {
ValidateKebabCase,
validateKebabCaseValidation,
} from './ValidateKebabCaseExtension';
export const ValidateKebabCaseFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: 'ValidateKebabCase',
component: ValidateKebabCase,
validation: validateKebabCaseValidation,
}),
);
```
```tsx
// packages/app/src/scaffolder/ValidateKebabCase/index.ts
export { ValidateKebabCaseFieldExtension } from './extensions';
```
Once all these files are in place, you then need to provide your custom
extension to the `scaffolder` plugin.
You do this in `packages/app/src/App.tsx`. You need to provide the
`customFieldExtensions` as children to the `ScaffolderPage`.
```tsx
const routes = (
<FlatRoutes>
...
<Route path="/create" element={<ScaffolderPage />} />
...
</FlatRoutes>
);
```
Should look something like this instead:
```tsx
import { ValidateKebabCaseFieldExtension } from './scaffolder/ValidateKebabCase';
import { ScaffolderFieldExtensions } from '@backstage/plugin-scaffolder-react';
const routes = (
<FlatRoutes>
...
<Route path="/create" element={<ScaffolderPage />}>
<ScaffolderFieldExtensions>
<ValidateKebabCaseFieldExtension />
</ScaffolderFieldExtensions>
</Route>
...
</FlatRoutes>
);
```
### Async Validation Function
A validation function can be asynchronous and use [Utility APIs](https://backstage.io/docs/api/utility-apis/) via the `ApiHolder` in the [field validation context](https://backstage.io/api/stable/types/_backstage_plugin-scaffolder-react.index.CustomFieldValidator.html). The example below uses the `catalogApiRef` to check if the submitted value (in this scenario an entity ref) exists in the catalog.
```tsx
import { FieldValidation } from '@rjsf/utils';
import { ApiHolder } from '@backstage/core-plugin-api';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
/*
This validation function checks if the submitted entity ref value is present in the catalog.
*/
export const customFieldExtensionValidator = async (
value: string,
validation: FieldValidation,
context: { apiHolder: ApiHolder },
) => {
const catalogApi = context.apiHolder.get(catalogApiRef);
if ((await catalogApi?.getEntityByRef(value)) === undefined) {
validation.addError('Entity not found');
}
};
```
## Using the Custom Field Extension
Once it's been passed to the `ScaffolderPage` you should now be able to use the
`ui:field` property in your templates to point it to the name of the
`customFieldExtension` that you registered.
Something like this:
```yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: Test template
title: Test template with custom extension
description: Test template
spec:
parameters:
- title: Fill in some steps
required:
- name
properties:
name:
title: Name
type: string
description: My custom name for the component
ui:field: ValidateKebabCase
steps:
[...]
```
## Access Data from other Fields
Custom fields extensions can read data from other fields in the form via the form context. This
is something that we discourage due to the coupling that it creates, but is sometimes still
the most sensible solution.
```tsx
const CustomFieldExtensionComponent = (props: FieldExtensionComponentProps<string[]>) => {
const { formData } = props.formContext;
...
};
const CustomFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: ...,
component: CustomFieldExtensionComponent,
validation: ...
})
);
```
## Previewing Custom Field Extensions
You can preview custom field extensions you write in the Backstage UI using the Custom Field Explorer
(accessible via the `/create/edit` route by default):
![Custom Field Explorer](../../assets/software-templates/custom-field-explorer.png)
In order to make your new custom field extension available in the explorer you will have to define a
JSON schema that describes the input/output types on your field like in the following example:
```tsx
//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
export const MyCustomExtensionWithOptionsSchema = {
uiOptions: {
type: 'object',
properties: {
focused: {
description: 'Whether to focus this field',
type: 'boolean',
},
},
},
returnValue: { type: 'string' },
};
export const MyCustomExtensionWithOptions = ({
onChange,
rawErrors,
required,
formData,
}: FieldExtensionComponentProps<string, { focused?: boolean }>) => {
return (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !formData}
onChange={onChange}
focused={focused}
/>
);
};
```
```tsx
// packages/app/src/scaffolder/MyCustomExtensionWithOptions/extensions.ts
...
import { MyCustomExtensionWithOptions, MyCustomExtensionWithOptionsSchema } from './MyCustomExtensionWithOptions';
export const MyCustomFieldWithOptionsExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: 'MyCustomExtensionWithOptions',
component: MyCustomExtensionWithOptions,
schema: MyCustomExtensionWithOptionsSchema,
}),
);
```
We recommend using a library like [zod](https://github.com/colinhacks/zod) to define your schema
and the provided `makeFieldSchemaFromZod` helper utility function to generate both the JSON schema
and type for your field props to preventing having to duplicate the definitions:
```tsx
//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
...
import { z } from 'zod/v3';
import { makeFieldSchemaFromZod } from '@backstage/plugin-scaffolder';
const MyCustomExtensionWithOptionsFieldSchema = makeFieldSchemaFromZod(
z.string(),
z.object({
focused: z
.boolean()
.optional()
.describe('Whether to focus this field'),
}),
);
export const MyCustomExtensionWithOptionsSchema = MyCustomExtensionWithOptionsFieldSchema.schema;
type MyCustomExtensionWithOptionsProps = typeof MyCustomExtensionWithOptionsFieldSchema.type;
export const MyCustomExtensionWithOptions = ({
onChange,
rawErrors,
required,
formData,
}: MyCustomExtensionWithOptionsProps) => {
return (
<FormControl
margin="normal"
required={required}
error={rawErrors?.length > 0 && !formData}
onChange={onChange}
focused={focused}
/>
);
};
```
@@ -4,6 +4,13 @@ title: Writing Custom Field Extensions
description: How to write your own field extensions
---
::::info
This documentation is written for the new frontend system, which is the default
in new Backstage apps. If your Backstage app still uses the old frontend system,
read the [old frontend system version of this guide](./writing-custom-field-extensions--old.md)
instead.
::::
Collecting input from the user is a very large part of the scaffolding process
and Software Templates as a whole. Sometimes the built in components and fields
just aren't good enough, and sometimes you want to enrich the form that the
@@ -18,26 +25,26 @@ validate the data too.
## Creating a Field Extension
Field extensions are a way to combine an ID, a `React` Component and a
`validation` function together in a modular way that you can then use to pass to
the `Scaffolder` frontend plugin in your own `App.tsx`.
`validation` function together in a modular way that you can then register as
an extension in your Backstage app.
You can create your own Field Extension by using the
[`createScaffolderFieldExtension`](https://backstage.io/api/stable/variables/_backstage_plugin-scaffolder.index.createScaffolderFieldExtension.html)
`API` like below.
You can create your own field extension by using the `FormFieldBlueprint` from
`@backstage/plugin-scaffolder-react/alpha` together with `createFormField`, which
types the component, validation, and optional schema.
As an example, we will create a component that validates whether a string is in the `Kebab-case` pattern:
As an example, we will create a component that validates whether a string is in the `Kebab-case` pattern.
First, create the component and validation function:
```tsx
//packages/app/src/scaffolder/ValidateKebabCase/ValidateKebabCaseExtension.tsx
// packages/app/src/scaffolder/ValidateKebabCase/ValidateKebabCaseExtension.tsx
import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';
import type { FieldValidation } from '@rjsf/utils';
import FormControl from '@material-ui/core/FormControl';
import FormHelperText from '@material-ui/core/FormHelperText';
import Input from '@material-ui/core/Input';
import InputLabel from '@material-ui/core/InputLabel';
/*
This is the actual component that will get rendered in the form
*/
export const ValidateKebabCase = ({
onChange,
rawErrors,
@@ -63,11 +70,6 @@ export const ValidateKebabCase = ({
);
};
/*
This is a validation function that will run when the form is submitted.
You will get the value from the `onChange` handler before as the value here to make sure that the types are aligned\
*/
export const validateKebabCaseValidation = (
value: string,
validation: FieldValidation,
@@ -82,71 +84,50 @@ export const validateKebabCaseValidation = (
};
```
Then, create the extension using `FormFieldBlueprint` and `createFormField`:
```tsx
// packages/app/src/scaffolder/ValidateKebabCase/extensions.ts
/*
This is where the magic happens and creates the custom field extension.
Note that if you're writing extensions part of a separate plugin,
then please use `scaffolderPlugin.provide` from there instead and export it part of your `plugin.ts` rather than re-using the `scaffolder.plugin`.
*/
import { scaffolderPlugin } from '@backstage/plugin-scaffolder';
import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder-react';
import {
FormFieldBlueprint,
createFormField,
} from '@backstage/plugin-scaffolder-react/alpha';
import {
ValidateKebabCase,
validateKebabCaseValidation,
} from './ValidateKebabCaseExtension';
export const ValidateKebabCaseFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: 'ValidateKebabCase',
component: ValidateKebabCase,
validation: validateKebabCaseValidation,
}),
);
export const ValidateKebabCaseFieldExtension = FormFieldBlueprint.make({
name: 'validate-kebab-case',
params: {
field: () =>
Promise.resolve(
createFormField({
name: 'ValidateKebabCase',
component: ValidateKebabCase,
validation: validateKebabCaseValidation,
}),
),
},
});
```
```tsx
// packages/app/src/scaffolder/ValidateKebabCase/index.ts
export { ValidateKebabCaseFieldExtension } from './extensions';
```
Once all these files are in place, you then need to provide your custom
extension to the `scaffolder` plugin.
Once the extension is created, install it in your app by passing it to `createApp`:
You do this in `packages/app/src/App.tsx`. You need to provide the
`customFieldExtensions` as children to the `ScaffolderPage`.
```tsx
const routes = (
<FlatRoutes>
...
<Route path="/create" element={<ScaffolderPage />} />
...
</FlatRoutes>
);
```
Should look something like this instead:
```tsx
```tsx title="packages/app/src/App.tsx"
import { createApp } from '@backstage/frontend-defaults';
import { ValidateKebabCaseFieldExtension } from './scaffolder/ValidateKebabCase';
import { ScaffolderFieldExtensions } from '@backstage/plugin-scaffolder-react';
const routes = (
<FlatRoutes>
...
<Route path="/create" element={<ScaffolderPage />}>
<ScaffolderFieldExtensions>
<ValidateKebabCaseFieldExtension />
</ScaffolderFieldExtensions>
</Route>
...
</FlatRoutes>
);
const app = createApp({
features: [ValidateKebabCaseFieldExtension],
});
export default app.createRoot();
```
### Async Validation Function
@@ -158,10 +139,6 @@ import { FieldValidation } from '@rjsf/utils';
import { ApiHolder } from '@backstage/core-plugin-api';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
/*
This validation function checks if the submitted entity ref value is present in the catalog.
*/
export const customFieldExtensionValidator = async (
value: string,
validation: FieldValidation,
@@ -177,11 +154,8 @@ export const customFieldExtensionValidator = async (
## Using the Custom Field Extension
Once it's been passed to the `ScaffolderPage` you should now be able to use the
`ui:field` property in your templates to point it to the name of the
`customFieldExtension` that you registered.
Something like this:
Once registered, you can use the `ui:field` property in your templates to
reference the name of the custom field extension:
```yaml
apiVersion: scaffolder.backstage.io/v1beta3
@@ -212,18 +186,30 @@ is something that we discourage due to the coupling that it creates, but is some
the most sensible solution.
```tsx
import {
FormFieldBlueprint,
createFormField,
} from '@backstage/plugin-scaffolder-react/alpha';
import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';
const CustomFieldExtensionComponent = (props: FieldExtensionComponentProps<string[]>) => {
const { formData } = props.formContext;
...
};
const CustomFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: ...,
component: CustomFieldExtensionComponent,
validation: ...
})
);
const CustomFieldExtension = FormFieldBlueprint.make({
name: 'custom-field',
params: {
field: () =>
Promise.resolve(
createFormField({
name: 'custom-field',
component: CustomFieldExtensionComponent,
validation: ...,
}),
),
},
});
```
## Previewing Custom Field Extensions
@@ -237,7 +223,10 @@ In order to make your new custom field extension available in the explorer you w
JSON schema that describes the input/output types on your field like in the following example:
```tsx
//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
// packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
import FormControl from '@material-ui/core/FormControl';
import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';
export const MyCustomExtensionWithOptionsSchema = {
uiOptions: {
type: 'object',
@@ -256,7 +245,10 @@ export const MyCustomExtensionWithOptions = ({
rawErrors,
required,
formData,
uiSchema,
}: FieldExtensionComponentProps<string, { focused?: boolean }>) => {
const focused = uiSchema['ui:options']?.focused;
return (
<FormControl
margin="normal"
@@ -271,16 +263,28 @@ export const MyCustomExtensionWithOptions = ({
```tsx
// packages/app/src/scaffolder/MyCustomExtensionWithOptions/extensions.ts
...
import { MyCustomExtensionWithOptions, MyCustomExtensionWithOptionsSchema } from './MyCustomExtensionWithOptions';
import {
FormFieldBlueprint,
createFormField,
} from '@backstage/plugin-scaffolder-react/alpha';
import {
MyCustomExtensionWithOptions,
MyCustomExtensionWithOptionsSchema,
} from './MyCustomExtensionWithOptions';
export const MyCustomFieldWithOptionsExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: 'MyCustomExtensionWithOptions',
component: MyCustomExtensionWithOptions,
schema: MyCustomExtensionWithOptionsSchema,
}),
);
export const MyCustomFieldWithOptionsExtension = FormFieldBlueprint.make({
name: 'MyCustomExtensionWithOptions',
params: {
field: () =>
Promise.resolve(
createFormField({
name: 'MyCustomExtensionWithOptions',
component: MyCustomExtensionWithOptions,
schema: MyCustomExtensionWithOptionsSchema,
}),
),
},
});
```
We recommend using a library like [zod](https://github.com/colinhacks/zod) to define your schema
@@ -288,31 +292,33 @@ and the provided `makeFieldSchemaFromZod` helper utility function to generate bo
and type for your field props to preventing having to duplicate the definitions:
```tsx
//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
...
// packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
import FormControl from '@material-ui/core/FormControl';
import { z } from 'zod/v3';
import { makeFieldSchemaFromZod } from '@backstage/plugin-scaffolder';
const MyCustomExtensionWithOptionsFieldSchema = makeFieldSchemaFromZod(
z.string(),
z.object({
focused: z
.boolean()
.optional()
.describe('Whether to focus this field'),
focused: z.boolean().optional().describe('Whether to focus this field'),
}),
);
export const MyCustomExtensionWithOptionsSchema = MyCustomExtensionWithOptionsFieldSchema.schema;
export const MyCustomExtensionWithOptionsSchema =
MyCustomExtensionWithOptionsFieldSchema.schema;
type MyCustomExtensionWithOptionsProps = typeof MyCustomExtensionWithOptionsFieldSchema.type;
type MyCustomExtensionWithOptionsProps =
typeof MyCustomExtensionWithOptionsFieldSchema.type;
export const MyCustomExtensionWithOptions = ({
onChange,
rawErrors,
required,
formData,
uiSchema,
}: MyCustomExtensionWithOptionsProps) => {
const focused = uiSchema['ui:options']?.focused;
return (
<FormControl
margin="normal"
@@ -0,0 +1,94 @@
---
id: writing-custom-step-layouts--old
title: Writing custom step layouts (Old Frontend System)
description: How to override the default step form layout
---
::::info
This documentation is for Backstage apps that still use the old frontend
system. If your app uses the new frontend system, read the
[current guide](./writing-custom-step-layouts.md) instead.
::::
Every form in each step rendered in the frontend uses the default form layout from [react-jsonschema-form](https://rjsf-team.github.io/react-jsonschema-form/docs/). It is possible to override this behaviour by supplying a `ui:ObjectFieldTemplate` property for a particular step:
```yaml
parameters:
- title: Fill in some steps
ui:ObjectFieldTemplate: TwoColumn
```
This is the same [field](https://rjsf-team.github.io/react-jsonschema-form/docs/advanced-customization/custom-templates#objectfieldtemplate) used by [react-jsonschema-form](https://rjsf-team.github.io/react-jsonschema-form/docs/) but we need to add a couple of steps to ensure that the string value of `TwoColumn` above is resolved to a react component.
## Registering a React component as a custom step layout
The [createScaffolderLayout](https://backstage.io/api/stable/functions/_backstage_plugin-scaffolder-react.index.createScaffolderLayout.html) function is used to mark a component as a custom step layout:
```tsx
import { scaffolderPlugin } from '@backstage/plugin-scaffolder';
import {
createScaffolderLayout,
LayoutTemplate,
} from '@backstage/plugin-scaffolder-react';
import { Grid } from '@material-ui/core';
const TwoColumn: LayoutTemplate = ({ properties, description, title }) => {
const mid = Math.ceil(properties.length / 2);
return (
<>
<h1>{title}</h1>
<h2>In two column layout!!</h2>
<Grid container justifyContent="flex-end">
{properties.slice(0, mid).map(prop => (
<Grid item xs={6} key={prop.content.key}>
{prop.content}
</Grid>
))}
{properties.slice(mid).map(prop => (
<Grid item xs={6} key={prop.content.key}>
{prop.content}
</Grid>
))}
</Grid>
{description}
</>
);
};
export const TwoColumnLayout = scaffolderPlugin.provide(
createScaffolderLayout({
name: 'TwoColumn',
component: TwoColumn,
}),
);
```
After you have registered your component as a custom layout then you need to provide the `layouts` to the `ScaffolderPage`:
```tsx
import { MyCustomFieldExtension } from './scaffolder/MyCustomExtension';
import { TwoColumnLayout } from './components/scaffolder/customScaffolderLayouts';
const routes = (
<FlatRoutes>
...
<Route path="/create" element={<ScaffolderPage />}>
<ScaffolderLayouts>
<TwoColumnLayout />
</ScaffolderLayouts>
</Route>
...
</FlatRoutes>
);
```
## Using the custom step layout
Any component that has been passed to the `ScaffolderPage` as children of the `ScaffolderLayouts` component can be used as a `ui:ObjectFieldTemplate` in your template file:
```yaml
parameters:
- title: Fill in some steps
ui:ObjectFieldTemplate: TwoColumn
```
@@ -4,6 +4,13 @@ title: Writing custom step layouts
description: How to override the default step form layout
---
::::info
This documentation is written for the new frontend system, which is the default
in new Backstage apps. If your Backstage app still uses the old frontend system,
read the [old frontend system version of this guide](./writing-custom-step-layouts--old.md)
instead.
::::
Every form in each step rendered in the frontend uses the default form layout from [react-jsonschema-form](https://rjsf-team.github.io/react-jsonschema-form/docs/). It is possible to override this behaviour by supplying a `ui:ObjectFieldTemplate` property for a particular step:
```yaml
@@ -16,14 +23,12 @@ This is the same [field](https://rjsf-team.github.io/react-jsonschema-form/docs/
## Registering a React component as a custom step layout
The [createScaffolderLayout](https://backstage.io/api/stable/functions/_backstage_plugin-scaffolder-react.index.createScaffolderLayout.html) function is used to mark a component as a custom step layout:
In the new frontend system, custom step layouts can be registered by creating a scaffolder module plugin that provides the layout through an extension override. Create a new plugin module:
```tsx
import { scaffolderPlugin } from '@backstage/plugin-scaffolder';
import {
createScaffolderLayout,
LayoutTemplate,
} from '@backstage/plugin-scaffolder-react';
```tsx title="packages/app/src/scaffolder/customLayouts.tsx"
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import scaffolderPlugin from '@backstage/plugin-scaffolder/alpha';
import { LayoutTemplate } from '@backstage/plugin-scaffolder-react';
import { Grid } from '@material-ui/core';
const TwoColumn: LayoutTemplate = ({ properties, description, title }) => {
@@ -49,37 +54,15 @@ const TwoColumn: LayoutTemplate = ({ properties, description, title }) => {
</>
);
};
export const TwoColumnLayout = scaffolderPlugin.provide(
createScaffolderLayout({
name: 'TwoColumn',
component: TwoColumn,
}),
);
```
After you have registered your component as a custom layout then you need to provide the `layouts` to the `ScaffolderPage`:
Use `createScaffolderLayout` from `@backstage/plugin-scaffolder-react` and `scaffolderPlugin.provide` from `@backstage/plugin-scaffolder` to register the layout under the name `TwoColumn`, then install it through a frontend module using `createFrontendModule` together with `scaffolderPlugin.withOverrides` from `@backstage/plugin-scaffolder/alpha`, following the patterns described in the extension overrides guide.
```tsx
import { MyCustomFieldExtension } from './scaffolder/MyCustomExtension';
import { TwoColumnLayout } from './components/scaffolder/customScaffolderLayouts';
const routes = (
<FlatRoutes>
...
<Route path="/create" element={<ScaffolderPage />}>
<ScaffolderLayouts>
<TwoColumnLayout />
</ScaffolderLayouts>
</Route>
...
</FlatRoutes>
);
```
For details on how to override and extend extensions in the new frontend system, see the [extension overrides](../../frontend-system/architecture/25-extension-overrides.md) documentation.
## Using the custom step layout
Any component that has been passed to the `ScaffolderPage` as children of the `ScaffolderLayouts` component can be used as a `ui:ObjectFieldTemplate` in your template file:
Once the layout is registered, it can be used as a `ui:ObjectFieldTemplate` in your template file:
```yaml
parameters: