docs(scaffolder): add info about custom field explorer

Signed-off-by: Phil Kuang <pkuang@factset.com>
This commit is contained in:
Phil Kuang
2022-11-10 13:11:03 -05:00
parent aa4438e341
commit 3e4a2ee779
11 changed files with 158 additions and 47 deletions
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):
![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,
}: 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}
/>
);
};
```
+24 -17
View File
@@ -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,
};
}