Merge pull request #33667 from backstage/docs-features-new-frontend-migration
docs: migrate feature documentation to new frontend system as primary content
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,58 @@ 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: async () =>
|
||||
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 wrapping it in a frontend module and 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 title="packages/app/src/scaffolder/scaffolderModule.ts"
|
||||
import { createFrontendModule } from '@backstage/frontend-plugin-api';
|
||||
import { ValidateKebabCaseFieldExtension } from './ValidateKebabCase';
|
||||
|
||||
```tsx
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
...
|
||||
<Route path="/create" element={<ScaffolderPage />} />
|
||||
...
|
||||
</FlatRoutes>
|
||||
);
|
||||
export const scaffolderCustomizations = createFrontendModule({
|
||||
pluginId: 'scaffolder',
|
||||
extensions: [ValidateKebabCaseFieldExtension],
|
||||
});
|
||||
```
|
||||
|
||||
Should look something like this instead:
|
||||
```tsx title="packages/app/src/App.tsx"
|
||||
import { createApp } from '@backstage/frontend-defaults';
|
||||
import { scaffolderCustomizations } from './scaffolder/scaffolderModule';
|
||||
|
||||
```tsx
|
||||
import { ValidateKebabCaseFieldExtension } from './scaffolder/ValidateKebabCase';
|
||||
import { ScaffolderFieldExtensions } from '@backstage/plugin-scaffolder-react';
|
||||
const app = createApp({
|
||||
features: [scaffolderCustomizations],
|
||||
});
|
||||
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
...
|
||||
<Route path="/create" element={<ScaffolderPage />}>
|
||||
<ScaffolderFieldExtensions>
|
||||
<ValidateKebabCaseFieldExtension />
|
||||
</ScaffolderFieldExtensions>
|
||||
</Route>
|
||||
...
|
||||
</FlatRoutes>
|
||||
);
|
||||
export default app.createRoot();
|
||||
```
|
||||
|
||||
### Async Validation Function
|
||||
@@ -158,10 +147,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 +162,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 +194,28 @@ 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: async () =>
|
||||
createFormField({
|
||||
name: 'custom-field',
|
||||
component: CustomFieldExtensionComponent,
|
||||
validation: ...,
|
||||
}),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Previewing Custom Field Extensions
|
||||
@@ -237,7 +229,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 +251,10 @@ export const MyCustomExtensionWithOptions = ({
|
||||
rawErrors,
|
||||
required,
|
||||
formData,
|
||||
uiSchema,
|
||||
}: FieldExtensionComponentProps<string, { focused?: boolean }>) => {
|
||||
const focused = uiSchema['ui:options']?.focused;
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
margin="normal"
|
||||
@@ -271,48 +269,60 @@ 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: async () =>
|
||||
createFormField({
|
||||
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:
|
||||
and type for your field props to prevent 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,85 +4,19 @@ title: Writing custom step layouts
|
||||
description: How to override the default step form layout
|
||||
---
|
||||
|
||||
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:
|
||||
::::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.
|
||||
::::
|
||||
|
||||
```yaml
|
||||
parameters:
|
||||
- title: Fill in some steps
|
||||
ui:ObjectFieldTemplate: TwoColumn
|
||||
```
|
||||
:::caution
|
||||
Custom step layouts are not yet supported in the new frontend system. The
|
||||
scaffolder plugin does not provide an extension blueprint or input for
|
||||
registering custom layouts in the new system.
|
||||
|
||||
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
|
||||
```
|
||||
If you need custom step layouts, you can continue using the
|
||||
[old frontend system](./writing-custom-step-layouts--old.md) approach with
|
||||
`createScaffolderLayout` and the `ScaffolderLayouts` component.
|
||||
:::
|
||||
|
||||
Reference in New Issue
Block a user