docs(scaffolder): add info about custom field explorer
Signed-off-by: Phil Kuang <pkuang@factset.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 148 KiB |
@@ -181,3 +181,109 @@ const CustomFieldExtension = scaffolderPlugin.provide(
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
## 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,
|
||||
}: FieldProps<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 `makeJsonSchemaFromZod` helper utility function to generate both the JSON schema
|
||||
and types for your field input/output to preventing having to duplicate the definitions:
|
||||
|
||||
```tsx
|
||||
//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
|
||||
...
|
||||
import { z } from 'zod';
|
||||
import { makeJsonSchemaFromZod } from '@backstage/plugin-scaffolder';
|
||||
|
||||
const MyCustomExtensionWithOptionsUiOptionsSchema = makeJsonSchemaFromZod(
|
||||
z.object({
|
||||
focused: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether to focus this field'),
|
||||
}),
|
||||
);
|
||||
|
||||
const MyCustomExtensionWithOptionsReturnValueSchema = makeJsonSchemaFromZod(
|
||||
z.string(),
|
||||
);
|
||||
|
||||
export const MyCustomExtensionWithOptionsSchema = {
|
||||
uiOptions: MyCustomExtensionWithOptionsUiOptionsSchema.schema,
|
||||
returnValue: MyCustomExtensionWithOptionsReturnValueSchema.schema,
|
||||
};
|
||||
|
||||
export const MyCustomExtensionWithOptions = ({
|
||||
onChange,
|
||||
rawErrors,
|
||||
required,
|
||||
formData,
|
||||
}: FieldProps<
|
||||
typeof MyCustomExtensionWithOptionsReturnValueSchema.type,
|
||||
typeof MyCustomExtensionWithOptionsUiOptionsSchema.type
|
||||
>) => {
|
||||
return (
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required={required}
|
||||
error={rawErrors?.length > 0 && !formData}
|
||||
onChange={onChange}
|
||||
focused={focused}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
@@ -35,6 +35,7 @@ import { TaskStep } from '@backstage/plugin-scaffolder-common';
|
||||
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
|
||||
import { UIOptionsType } from '@rjsf/utils';
|
||||
import { UiSchema } from '@rjsf/utils';
|
||||
import { z } from 'zod';
|
||||
|
||||
// @alpha
|
||||
export function createNextScaffolderFieldExtension<
|
||||
@@ -90,13 +91,12 @@ export const EntityPickerFieldExtension: FieldExtensionComponent<
|
||||
>;
|
||||
|
||||
// @public
|
||||
export type EntityPickerUiOptions =
|
||||
typeof EntityPickerUiOptionsSchema.schemaType;
|
||||
export type EntityPickerUiOptions = typeof EntityPickerUiOptionsSchema.type;
|
||||
|
||||
// @public (undocumented)
|
||||
export const EntityPickerUiOptionsSchema: {
|
||||
jsonSchema: JSONSchema7;
|
||||
schemaType: {
|
||||
schema: JSONSchema7;
|
||||
type: {
|
||||
defaultKind?: string | undefined;
|
||||
defaultNamespace?: string | false | undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
@@ -116,12 +116,12 @@ export const EntityTagsPickerFieldExtension: FieldExtensionComponent<
|
||||
|
||||
// @public
|
||||
export type EntityTagsPickerUiOptions =
|
||||
typeof EntityTagsPickerUiOptionsSchema.schemaType;
|
||||
typeof EntityTagsPickerUiOptionsSchema.type;
|
||||
|
||||
// @public (undocumented)
|
||||
export const EntityTagsPickerUiOptionsSchema: {
|
||||
jsonSchema: JSONSchema7;
|
||||
schemaType: {
|
||||
schema: JSONSchema7;
|
||||
type: {
|
||||
showCounts?: boolean | undefined;
|
||||
kinds?: string[] | undefined;
|
||||
helperText?: string | undefined;
|
||||
@@ -192,6 +192,14 @@ export type LogEvent = {
|
||||
taskId: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function makeJsonSchemaFromZod<T extends z.ZodType>(
|
||||
schema: T,
|
||||
): {
|
||||
schema: JSONSchema7;
|
||||
type: T extends z.ZodType<any, any, infer I> ? I : never;
|
||||
};
|
||||
|
||||
// @alpha
|
||||
export type NextCustomFieldValidator<TFieldReturnValue> = (
|
||||
data: TFieldReturnValue,
|
||||
@@ -262,12 +270,12 @@ export const OwnedEntityPickerFieldExtension: FieldExtensionComponent<
|
||||
|
||||
// @public
|
||||
export type OwnedEntityPickerUiOptions =
|
||||
typeof OwnedEntityPickerUiOptionsSchema.schemaType;
|
||||
typeof OwnedEntityPickerUiOptionsSchema.type;
|
||||
|
||||
// @public (undocumented)
|
||||
export const OwnedEntityPickerUiOptionsSchema: {
|
||||
jsonSchema: JSONSchema7;
|
||||
schemaType: {
|
||||
schema: JSONSchema7;
|
||||
type: {
|
||||
defaultKind?: string | undefined;
|
||||
defaultNamespace?: string | false | undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
@@ -286,12 +294,12 @@ export const OwnerPickerFieldExtension: FieldExtensionComponent<
|
||||
>;
|
||||
|
||||
// @public
|
||||
export type OwnerPickerUiOptions = typeof OwnerPickerUiOptionsSchema.schemaType;
|
||||
export type OwnerPickerUiOptions = typeof OwnerPickerUiOptionsSchema.type;
|
||||
|
||||
// @public (undocumented)
|
||||
export const OwnerPickerUiOptionsSchema: {
|
||||
jsonSchema: JSONSchema7;
|
||||
schemaType: {
|
||||
schema: JSONSchema7;
|
||||
type: {
|
||||
defaultNamespace?: string | false | undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
@@ -333,13 +341,12 @@ export const RepoUrlPickerFieldExtension: FieldExtensionComponent<
|
||||
>;
|
||||
|
||||
// @public
|
||||
export type RepoUrlPickerUiOptions =
|
||||
typeof RepoUrlPickerUiOptionsSchema.schemaType;
|
||||
export type RepoUrlPickerUiOptions = typeof RepoUrlPickerUiOptionsSchema.type;
|
||||
|
||||
// @public (undocumented)
|
||||
export const RepoUrlPickerUiOptionsSchema: {
|
||||
jsonSchema: JSONSchema7;
|
||||
schemaType: {
|
||||
schema: JSONSchema7;
|
||||
type: {
|
||||
allowedOwners?: string[] | undefined;
|
||||
allowedOrganizations?: string[] | undefined;
|
||||
allowedRepos?: string[] | undefined;
|
||||
|
||||
@@ -19,8 +19,8 @@ import { makeJsonSchemaFromZod } from '../utils';
|
||||
const EntityNamePickerReturnValueSchema = makeJsonSchemaFromZod(z.string());
|
||||
|
||||
export type EntityNamePickerReturnValue =
|
||||
typeof EntityNamePickerReturnValueSchema.schemaType;
|
||||
typeof EntityNamePickerReturnValueSchema.type;
|
||||
|
||||
export const EntityNamePickerSchema = {
|
||||
returnValue: EntityNamePickerReturnValueSchema.jsonSchema,
|
||||
returnValue: EntityNamePickerReturnValueSchema.schema,
|
||||
};
|
||||
|
||||
@@ -52,13 +52,11 @@ const EntityPickerReturnValueSchema = makeJsonSchemaFromZod(z.string());
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type EntityPickerUiOptions =
|
||||
typeof EntityPickerUiOptionsSchema.schemaType;
|
||||
export type EntityPickerUiOptions = typeof EntityPickerUiOptionsSchema.type;
|
||||
|
||||
export type EntityPickerReturnValue =
|
||||
typeof EntityPickerReturnValueSchema.schemaType;
|
||||
export type EntityPickerReturnValue = typeof EntityPickerReturnValueSchema.type;
|
||||
|
||||
export const EntityPickerSchema = {
|
||||
uiOptions: EntityPickerUiOptionsSchema.jsonSchema,
|
||||
returnValue: EntityPickerReturnValueSchema.jsonSchema,
|
||||
uiOptions: EntityPickerUiOptionsSchema.schema,
|
||||
returnValue: EntityPickerReturnValueSchema.schema,
|
||||
};
|
||||
|
||||
@@ -44,12 +44,12 @@ const EntityTagsPickerReturnValueSchema = makeJsonSchemaFromZod(
|
||||
* @public
|
||||
*/
|
||||
export type EntityTagsPickerUiOptions =
|
||||
typeof EntityTagsPickerUiOptionsSchema.schemaType;
|
||||
typeof EntityTagsPickerUiOptionsSchema.type;
|
||||
|
||||
export type EntityTagsPickerReturnValue =
|
||||
typeof EntityTagsPickerReturnValueSchema.schemaType;
|
||||
typeof EntityTagsPickerReturnValueSchema.type;
|
||||
|
||||
export const EntityTagsPickerSchema = {
|
||||
uiOptions: EntityTagsPickerUiOptionsSchema.jsonSchema,
|
||||
returnValue: EntityTagsPickerReturnValueSchema.jsonSchema,
|
||||
uiOptions: EntityTagsPickerUiOptionsSchema.schema,
|
||||
returnValue: EntityTagsPickerReturnValueSchema.schema,
|
||||
};
|
||||
|
||||
@@ -53,12 +53,12 @@ const OwnedEntityPickerReturnValueSchema = makeJsonSchemaFromZod(z.string());
|
||||
* @public
|
||||
*/
|
||||
export type OwnedEntityPickerUiOptions =
|
||||
typeof OwnedEntityPickerUiOptionsSchema.schemaType;
|
||||
typeof OwnedEntityPickerUiOptionsSchema.type;
|
||||
|
||||
export type OwnedEntityPickerReturnValue =
|
||||
typeof OwnedEntityPickerReturnValueSchema.schemaType;
|
||||
typeof OwnedEntityPickerReturnValueSchema.type;
|
||||
|
||||
export const OwnedEntityPickerSchema = {
|
||||
uiOptions: OwnedEntityPickerUiOptionsSchema.jsonSchema,
|
||||
returnValue: OwnedEntityPickerReturnValueSchema.jsonSchema,
|
||||
uiOptions: OwnedEntityPickerUiOptionsSchema.schema,
|
||||
returnValue: OwnedEntityPickerReturnValueSchema.schema,
|
||||
};
|
||||
|
||||
@@ -49,12 +49,11 @@ const OwnerPickerReturnValueSchema = makeJsonSchemaFromZod(z.string());
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type OwnerPickerUiOptions = typeof OwnerPickerUiOptionsSchema.schemaType;
|
||||
export type OwnerPickerUiOptions = typeof OwnerPickerUiOptionsSchema.type;
|
||||
|
||||
export type OwnerPickerReturnValue =
|
||||
typeof OwnerPickerReturnValueSchema.schemaType;
|
||||
export type OwnerPickerReturnValue = typeof OwnerPickerReturnValueSchema.type;
|
||||
|
||||
export const OwnerPickerSchema = {
|
||||
uiOptions: OwnerPickerUiOptionsSchema.jsonSchema,
|
||||
returnValue: OwnerPickerReturnValueSchema.jsonSchema,
|
||||
uiOptions: OwnerPickerUiOptionsSchema.schema,
|
||||
returnValue: OwnerPickerReturnValueSchema.schema,
|
||||
};
|
||||
|
||||
@@ -85,16 +85,15 @@ const RepoUrlPickerReturnValueSchema = makeJsonSchemaFromZod(z.string());
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type RepoUrlPickerUiOptions =
|
||||
typeof RepoUrlPickerUiOptionsSchema.schemaType;
|
||||
export type RepoUrlPickerUiOptions = typeof RepoUrlPickerUiOptionsSchema.type;
|
||||
|
||||
export type RepoUrlPickerReturnValue =
|
||||
typeof RepoUrlPickerReturnValueSchema.schemaType;
|
||||
typeof RepoUrlPickerReturnValueSchema.type;
|
||||
|
||||
// NOTE: There is a bug with this failing validation in the custom field explorer due
|
||||
// to https://github.com/rjsf-team/react-jsonschema-form/issues/675 even if
|
||||
// requestUserCredentials is not defined
|
||||
export const RepoUrlPickerSchema = {
|
||||
uiOptions: RepoUrlPickerUiOptionsSchema.jsonSchema,
|
||||
returnValue: RepoUrlPickerReturnValueSchema.jsonSchema,
|
||||
uiOptions: RepoUrlPickerUiOptionsSchema.schema,
|
||||
returnValue: RepoUrlPickerReturnValueSchema.schema,
|
||||
};
|
||||
|
||||
@@ -18,3 +18,4 @@ export * from './OwnerPicker';
|
||||
export * from './RepoUrlPicker';
|
||||
export * from './OwnedEntityPicker';
|
||||
export * from './EntityTagsPicker';
|
||||
export { makeJsonSchemaFromZod } from './utils';
|
||||
|
||||
@@ -18,17 +18,18 @@ import { z } from 'zod';
|
||||
import zodToJsonSchema from 'zod-to-json-schema';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Utility function to convert zod schemas to JSON schemas with
|
||||
* type inference extraction that abstracts away zod typings
|
||||
*/
|
||||
export function makeJsonSchemaFromZod<T extends z.ZodType>(
|
||||
schema: T,
|
||||
): {
|
||||
jsonSchema: JSONSchema7;
|
||||
schemaType: T extends z.ZodType<any, any, infer I> ? I : never;
|
||||
schema: JSONSchema7;
|
||||
type: T extends z.ZodType<any, any, infer I> ? I : never;
|
||||
} {
|
||||
return {
|
||||
jsonSchema: zodToJsonSchema(schema) as JSONSchema7,
|
||||
schemaType: null as any,
|
||||
schema: zodToJsonSchema(schema) as JSONSchema7,
|
||||
type: null as any,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user