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:
@@ -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):
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user