diff --git a/.changeset/soft-nails-arrive.md b/.changeset/soft-nails-arrive.md new file mode 100644 index 0000000000..e9c0a40a05 --- /dev/null +++ b/.changeset/soft-nails-arrive.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Implement Custom Field Explorer to view and play around with available installed custom field extensions diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 158c75e5bd..69512e7af0 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -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< @@ -57,6 +58,12 @@ export function createScaffolderLayout( options: LayoutOptions, ): Extension>; +// @public +export type CustomFieldExtensionSchema = { + uiOptions?: JSONSchema7; + returnValue?: JSONSchema7; +}; + // @public export type CustomFieldValidator = ( data: TFieldReturnValue, @@ -75,36 +82,78 @@ export const EntityNamePickerFieldExtension: FieldExtensionComponent< // @public export const EntityPickerFieldExtension: FieldExtensionComponent< string, - EntityPickerUiOptions + { + defaultKind?: string | undefined; + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + } >; // @public -export interface EntityPickerUiOptions { - // (undocumented) - allowArbitraryValues?: boolean; - // (undocumented) - allowedKinds?: string[]; - // (undocumented) - defaultKind?: string; - // (undocumented) - defaultNamespace?: string | false; -} +export type EntityPickerUiOptions = z.infer; + +// @public (undocumented) +export const EntityPickerUiOptionsSchema: z.ZodObject< + { + allowedKinds: z.ZodOptional>; + defaultKind: z.ZodOptional; + allowArbitraryValues: z.ZodOptional; + defaultNamespace: z.ZodOptional< + z.ZodUnion<[z.ZodString, z.ZodLiteral]> + >; + }, + 'strip', + z.ZodTypeAny, + { + defaultKind?: string | undefined; + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + }, + { + defaultKind?: string | undefined; + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + } +>; // @public export const EntityTagsPickerFieldExtension: FieldExtensionComponent< string[], - EntityTagsPickerUiOptions + { + showCounts?: boolean | undefined; + kinds?: string[] | undefined; + helperText?: string | undefined; + } >; // @public -export interface EntityTagsPickerUiOptions { - // (undocumented) - helperText?: string; - // (undocumented) - kinds?: string[]; - // (undocumented) - showCounts?: boolean; -} +export type EntityTagsPickerUiOptions = z.infer< + typeof EntityTagsPickerUiOptionsSchema +>; + +// @public (undocumented) +export const EntityTagsPickerUiOptionsSchema: z.ZodObject< + { + kinds: z.ZodOptional>; + showCounts: z.ZodOptional; + helperText: z.ZodOptional; + }, + 'strip', + z.ZodTypeAny, + { + showCounts?: boolean | undefined; + kinds?: string[] | undefined; + helperText?: string | undefined; + }, + { + showCounts?: boolean | undefined; + kinds?: string[] | undefined; + helperText?: string | undefined; + } +>; // @public export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null; @@ -130,6 +179,7 @@ export type FieldExtensionOptions< props: FieldExtensionComponentProps, ) => JSX.Element | null; validation?: CustomFieldValidator; + schema?: CustomFieldExtensionSchema; }; // @public @@ -199,6 +249,7 @@ export type NextFieldExtensionOptions< props: NextFieldExtensionComponentProps, ) => JSX.Element | null; validation?: NextCustomFieldValidator; + schema?: CustomFieldExtensionSchema; }; // @alpha (undocumented) @@ -228,36 +279,80 @@ export const nextSelectedTemplateRouteRef: SubRouteRef< // @public export const OwnedEntityPickerFieldExtension: FieldExtensionComponent< string, - OwnedEntityPickerUiOptions + { + defaultKind?: string | undefined; + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + } >; // @public -export interface OwnedEntityPickerUiOptions { - // (undocumented) - allowArbitraryValues?: boolean; - // (undocumented) - allowedKinds?: string[]; - // (undocumented) - defaultKind?: string; - // (undocumented) - defaultNamespace?: string | false; -} +export type OwnedEntityPickerUiOptions = z.infer< + typeof OwnedEntityPickerUiOptionsSchema +>; + +// @public (undocumented) +export const OwnedEntityPickerUiOptionsSchema: z.ZodObject< + { + allowedKinds: z.ZodOptional>; + defaultKind: z.ZodOptional; + allowArbitraryValues: z.ZodOptional; + defaultNamespace: z.ZodOptional< + z.ZodUnion<[z.ZodString, z.ZodLiteral]> + >; + }, + 'strip', + z.ZodTypeAny, + { + defaultKind?: string | undefined; + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + }, + { + defaultKind?: string | undefined; + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + } +>; // @public export const OwnerPickerFieldExtension: FieldExtensionComponent< string, - OwnerPickerUiOptions + { + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + } >; // @public -export interface OwnerPickerUiOptions { - // (undocumented) - allowArbitraryValues?: boolean; - // (undocumented) - allowedKinds?: string[]; - // (undocumented) - defaultNamespace?: string | false; -} +export type OwnerPickerUiOptions = z.infer; + +// @public (undocumented) +export const OwnerPickerUiOptionsSchema: z.ZodObject< + { + allowedKinds: z.ZodOptional>>; + allowArbitraryValues: z.ZodOptional; + defaultNamespace: z.ZodOptional< + z.ZodUnion<[z.ZodString, z.ZodLiteral]> + >; + }, + 'strip', + z.ZodTypeAny, + { + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + }, + { + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + } +>; // @public export const repoPickerValidation: ( @@ -271,31 +366,144 @@ export const repoPickerValidation: ( // @public export const RepoUrlPickerFieldExtension: FieldExtensionComponent< string, - RepoUrlPickerUiOptions + { + allowedOwners?: string[] | undefined; + allowedOrganizations?: string[] | undefined; + allowedRepos?: string[] | undefined; + allowedHosts?: string[] | undefined; + requestUserCredentials?: + | { + additionalScopes?: + | { + azure?: string[] | undefined; + github?: string[] | undefined; + gitlab?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + } + | undefined; + secretsKey: string; + } + | undefined; + } >; // @public -export interface RepoUrlPickerUiOptions { - // (undocumented) - allowedHosts?: string[]; - // (undocumented) - allowedOrganizations?: string[]; - // (undocumented) - allowedOwners?: string[]; - // (undocumented) - allowedRepos?: string[]; - // (undocumented) - requestUserCredentials?: { - secretsKey: string; - additionalScopes?: { - gerrit?: string[]; - github?: string[]; - gitlab?: string[]; - bitbucket?: string[]; - azure?: string[]; - }; - }; -} +export type RepoUrlPickerUiOptions = z.infer< + typeof RepoUrlPickerUiOptionsSchema +>; + +// @public (undocumented) +export const RepoUrlPickerUiOptionsSchema: z.ZodObject< + { + allowedHosts: z.ZodOptional>; + allowedOrganizations: z.ZodOptional>; + allowedOwners: z.ZodOptional>; + allowedRepos: z.ZodOptional>; + requestUserCredentials: z.ZodOptional< + z.ZodObject< + { + secretsKey: z.ZodString; + additionalScopes: z.ZodOptional< + z.ZodObject< + { + gerrit: z.ZodOptional>; + github: z.ZodOptional>; + gitlab: z.ZodOptional>; + bitbucket: z.ZodOptional>; + azure: z.ZodOptional>; + }, + 'strip', + z.ZodTypeAny, + { + azure?: string[] | undefined; + github?: string[] | undefined; + gitlab?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + }, + { + azure?: string[] | undefined; + github?: string[] | undefined; + gitlab?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + } + > + >; + }, + 'strip', + z.ZodTypeAny, + { + additionalScopes?: + | { + azure?: string[] | undefined; + github?: string[] | undefined; + gitlab?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + } + | undefined; + secretsKey: string; + }, + { + additionalScopes?: + | { + azure?: string[] | undefined; + github?: string[] | undefined; + gitlab?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + } + | undefined; + secretsKey: string; + } + > + >; + }, + 'strip', + z.ZodTypeAny, + { + allowedOwners?: string[] | undefined; + allowedOrganizations?: string[] | undefined; + allowedRepos?: string[] | undefined; + allowedHosts?: string[] | undefined; + requestUserCredentials?: + | { + additionalScopes?: + | { + azure?: string[] | undefined; + github?: string[] | undefined; + gitlab?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + } + | undefined; + secretsKey: string; + } + | undefined; + }, + { + allowedOwners?: string[] | undefined; + allowedOrganizations?: string[] | undefined; + allowedRepos?: string[] | undefined; + allowedHosts?: string[] | undefined; + requestUserCredentials?: + | { + additionalScopes?: + | { + azure?: string[] | undefined; + github?: string[] | undefined; + gitlab?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + } + | undefined; + secretsKey: string; + } + | undefined; + } +>; // @public (undocumented) export const rootRouteRef: RouteRef; diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 214351e2b7..07ad33dd03 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -74,7 +74,9 @@ "react-use": "^17.2.4", "use-immer": "^0.7.0", "yaml": "^2.0.0", - "zen-observable": "^0.8.15" + "zen-observable": "^0.8.15", + "zod": "^3.11.6", + "zod-to-json-schema": "^3.18.1" }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/CustomFieldExplorer.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/CustomFieldExplorer.tsx new file mode 100644 index 0000000000..3510968acd --- /dev/null +++ b/plugins/scaffolder/src/components/TemplateEditorPage/CustomFieldExplorer.tsx @@ -0,0 +1,201 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { StreamLanguage } from '@codemirror/language'; +import { yaml as yamlSupport } from '@codemirror/legacy-modes/mode/yaml'; +import { + Button, + Card, + CardContent, + CardHeader, + FormControl, + IconButton, + InputLabel, + makeStyles, + MenuItem, + Select, +} from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; +import { withTheme } from '@rjsf/core'; +import { Theme as MuiTheme } from '@rjsf/material-ui'; +import CodeMirror from '@uiw/react-codemirror'; +import React, { useCallback, useMemo, useState } from 'react'; +import yaml from 'yaml'; +import { FieldExtensionOptions } from '../../extensions'; +import * as fieldOverrides from '../MultistepJsonForm/FieldOverrides'; +import { TemplateEditorForm } from './TemplateEditorForm'; + +const Form = withTheme(MuiTheme); + +const useStyles = makeStyles(theme => ({ + root: { + gridArea: 'pageContent', + display: 'grid', + gridTemplateAreas: ` + "controls controls" + "fieldForm preview" + `, + gridTemplateRows: 'auto 1fr', + gridTemplateColumns: '1fr 1fr', + }, + controls: { + gridArea: 'controls', + display: 'flex', + flexFlow: 'row nowrap', + alignItems: 'center', + margin: theme.spacing(1), + }, + fieldForm: { + gridArea: 'fieldForm', + }, + preview: { + gridArea: 'preview', + }, +})); + +export const CustomFieldExplorer = ({ + customFieldExtensions = [], + onClose, +}: { + customFieldExtensions?: FieldExtensionOptions[]; + onClose?: () => void; +}) => { + const classes = useStyles(); + const fieldOptions = customFieldExtensions.filter(field => !!field.schema); + const [selectedField, setSelectedField] = useState(fieldOptions[0]); + const [fieldFormState, setFieldFormState] = useState({}); + const [formState, setFormState] = useState({}); + const [refreshKey, setRefreshKey] = useState(Date.now()); + const sampleFieldTemplate = useMemo( + () => + yaml.stringify({ + parameters: [ + { + title: `${selectedField.name} Example`, + properties: { + [selectedField.name]: { + type: selectedField.schema?.returnValue?.type, + 'ui:field': selectedField.name, + 'ui:options': fieldFormState, + }, + }, + }, + ], + }), + [fieldFormState, selectedField], + ); + + const fieldComponents = useMemo(() => { + return Object.fromEntries( + customFieldExtensions.map(({ name, component }) => [name, component]), + ); + }, [customFieldExtensions]); + + const handleSelectionChange = useCallback( + selection => { + setSelectedField(selection); + setFieldFormState({}); + setFormState({}); + }, + [setFieldFormState, setFormState, setSelectedField], + ); + + const handleFieldConfigChange = useCallback( + state => { + setFieldFormState(state); + setFormState({}); + // Force TemplateEditorForm to re-render since some fields + // may not be responsive to ui:option changes + setRefreshKey(Date.now()); + }, + [setFieldFormState, setRefreshKey], + ); + + return ( +
+
+ + + Choose Custom Field Extension + + + + + + + +
+
+ + + +
handleFieldConfigChange(e.formData)} + schema={selectedField.schema?.uiOptions || {}} + > + +
+
+
+
+
+ + + + + + + null} + /> +
+
+ ); +}; diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorIntro.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorIntro.tsx index dc411ee965..61e6c46964 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorIntro.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorIntro.tsx @@ -44,7 +44,7 @@ const useStyles = makeStyles(theme => ({ interface EditorIntroProps { style?: JSX.IntrinsicElements['div']['style']; - onSelect?: (option: 'local' | 'form') => void; + onSelect?: (option: 'local' | 'form' | 'field-explorer') => void; } export function TemplateEditorIntro(props: EditorIntroProps) { @@ -104,6 +104,22 @@ export function TemplateEditorIntro(props: EditorIntroProps) { ); + const cardFieldExplorer = ( + + props.onSelect?.('field-explorer')}> + + + Custom Field Explorer + + + View and play around with available installed custom field + extensions. + + + + + ); + return (
@@ -121,6 +137,7 @@ export function TemplateEditorIntro(props: EditorIntroProps) { {supportsLoad && cardLoadLocal} {cardFormEditor} {!supportsLoad && cardLoadLocal} + {cardFieldExplorer}
); diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx index b539b6403b..8847b85373 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx @@ -19,6 +19,7 @@ import { TemplateDirectoryAccess, WebFileSystemAccess, } from '../../lib/filesystem'; +import { CustomFieldExplorer } from './CustomFieldExplorer'; import { TemplateEditorIntro } from './TemplateEditorIntro'; import { TemplateEditor } from './TemplateEditor'; import { TemplateFormPreviewer } from './TemplateFormPreviewer'; @@ -32,6 +33,9 @@ type Selection = } | { type: 'form'; + } + | { + type: 'field-explorer'; }; interface TemplateEditorPageProps { @@ -62,6 +66,13 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) { layouts={props.layouts} /> ); + } else if (selection?.type === 'field-explorer') { + content = ( + setSelection(undefined)} + /> + ); } else { content = ( @@ -73,6 +84,8 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) { .catch(() => {}); } else if (option === 'form') { setSelection({ type: 'form' }); + } else if (option === 'field-explorer') { + setSelection({ type: 'field-explorer' }); } }} /> diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/EntityNamePicker.tsx b/plugins/scaffolder/src/components/fields/EntityNamePicker/EntityNamePicker.tsx index 802135a863..eba05cfb92 100644 --- a/plugins/scaffolder/src/components/fields/EntityNamePicker/EntityNamePicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/EntityNamePicker.tsx @@ -15,13 +15,16 @@ */ import React from 'react'; import { FieldExtensionComponentProps } from '../../../extensions'; +import { EntityNamePickerReturnValue } from './schema'; import { TextField } from '@material-ui/core'; +export { EntityNamePickerSchema } from './schema'; + /** * EntityName Picker */ export const EntityNamePicker = ( - props: FieldExtensionComponentProps, + props: FieldExtensionComponentProps, ) => { const { onChange, diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts new file mode 100644 index 0000000000..45aa4502ce --- /dev/null +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { JSONSchema7 } from 'json-schema'; +import { z } from 'zod'; +import zodToJsonSchema from 'zod-to-json-schema'; + +const EntityNamePickerReturnValueSchema = z.string(); + +export type EntityNamePickerReturnValue = z.infer< + typeof EntityNamePickerReturnValueSchema +>; + +export const EntityNamePickerSchema = { + returnValue: zodToJsonSchema( + EntityNamePickerReturnValueSchema, + ) as JSONSchema7, +}; diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx index e7883a4721..8fcc412d0a 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx @@ -24,19 +24,9 @@ import Autocomplete from '@material-ui/lab/Autocomplete'; import React, { useCallback, useEffect } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { FieldExtensionComponentProps } from '../../../extensions'; +import { EntityPickerReturnValue, EntityPickerUiOptions } from './schema'; -/** - * The input props that can be specified under `ui:options` for the - * `EntityPicker` field extension. - * - * @public - */ -export interface EntityPickerUiOptions { - allowedKinds?: string[]; - defaultKind?: string; - allowArbitraryValues?: boolean; - defaultNamespace?: string | false; -} +export { EntityPickerSchema } from './schema'; /** * The underlying component that is rendered in the form for the `EntityPicker` @@ -45,7 +35,10 @@ export interface EntityPickerUiOptions { * @public */ export const EntityPicker = ( - props: FieldExtensionComponentProps, + props: FieldExtensionComponentProps< + EntityPickerReturnValue, + EntityPickerUiOptions + >, ) => { const { onChange, diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/index.ts b/plugins/scaffolder/src/components/fields/EntityPicker/index.ts index 891b5bef16..d1044e4e67 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityPicker/index.ts @@ -13,4 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export type { EntityPickerUiOptions } from './EntityPicker'; +export { + type EntityPickerUiOptions, + EntityPickerUiOptionsSchema, +} from './schema'; diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts new file mode 100644 index 0000000000..5f7d66fc4f --- /dev/null +++ b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { JSONSchema7 } from 'json-schema'; +import { z } from 'zod'; +import zodToJsonSchema from 'zod-to-json-schema'; + +/** + * @public + */ +export const EntityPickerUiOptionsSchema = z.object({ + allowedKinds: z + .array(z.string()) + .optional() + .describe('List of kinds of entities to derive options from'), + defaultKind: z + .string() + .optional() + .describe( + 'The default entity kind. Options of this kind will not be prefixed.', + ), + allowArbitraryValues: z + .boolean() + .optional() + .describe('Whether to allow arbitrary user input. Defaults to true'), + defaultNamespace: z + .union([z.string(), z.literal(false)]) + .optional() + .describe( + 'The default namespace. Options with this namespace will not be prefixed.', + ), +}); + +const EntityPickerReturnValueSchema = z.string(); + +/** + * The input props that can be specified under `ui:options` for the + * `EntityPicker` field extension. + * + * @public + */ +export type EntityPickerUiOptions = z.infer; + +export type EntityPickerReturnValue = z.infer< + typeof EntityPickerReturnValueSchema +>; + +export const EntityPickerSchema = { + uiOptions: zodToJsonSchema(EntityPickerUiOptionsSchema) as JSONSchema7, + returnValue: zodToJsonSchema(EntityPickerReturnValueSchema) as JSONSchema7, +}; diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx b/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx index c0e3aab436..6d5404cee1 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx @@ -23,18 +23,12 @@ import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { FormControl, TextField } from '@material-ui/core'; import { Autocomplete } from '@material-ui/lab'; import { FieldExtensionComponentProps } from '../../../extensions'; +import { + EntityTagsPickerReturnValue, + EntityTagsPickerUiOptions, +} from './schema'; -/** - * The input props that can be specified under `ui:options` for the - * `EntityTagsPicker` field extension. - * - * @public - */ -export interface EntityTagsPickerUiOptions { - kinds?: string[]; - showCounts?: boolean; - helperText?: string; -} +export { EntityTagsPickerSchema } from './schema'; /** * The underlying component that is rendered in the form for the `EntityTagsPicker` @@ -43,7 +37,10 @@ export interface EntityTagsPickerUiOptions { * @public */ export const EntityTagsPicker = ( - props: FieldExtensionComponentProps, + props: FieldExtensionComponentProps< + EntityTagsPickerReturnValue, + EntityTagsPickerUiOptions + >, ) => { const { formData, onChange, uiSchema } = props; const catalogApi = useApi(catalogApiRef); diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts b/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts index 9ff6e553a6..850fc17c8e 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts @@ -13,4 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export type { EntityTagsPickerUiOptions } from './EntityTagsPicker'; +export { + type EntityTagsPickerUiOptions, + EntityTagsPickerUiOptionsSchema, +} from './schema'; diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts new file mode 100644 index 0000000000..ff477903d7 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { JSONSchema7 } from 'json-schema'; +import { z } from 'zod'; +import zodToJsonSchema from 'zod-to-json-schema'; + +/** + * @public + */ +export const EntityTagsPickerUiOptionsSchema = z.object({ + kinds: z + .array(z.string()) + .optional() + .describe('List of kinds of entities to derive tags from'), + showCounts: z + .boolean() + .optional() + .describe('Whether to show usage counts per tag'), + helperText: z.string().optional().describe('Helper text to display'), +}); + +const EntityTagsPickerReturnValueSchema = z.array(z.string()); + +/** + * The input props that can be specified under `ui:options` for the + * `EntityTagsPicker` field extension. + * + * @public + */ +export type EntityTagsPickerUiOptions = z.infer< + typeof EntityTagsPickerUiOptionsSchema +>; + +export type EntityTagsPickerReturnValue = z.infer< + typeof EntityTagsPickerReturnValueSchema +>; + +export const EntityTagsPickerSchema = { + uiOptions: zodToJsonSchema(EntityTagsPickerUiOptionsSchema) as JSONSchema7, + returnValue: zodToJsonSchema( + EntityTagsPickerReturnValueSchema, + ) as JSONSchema7, +}; diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx index 2446883a86..47bcb6c9b8 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx @@ -27,20 +27,12 @@ import React, { useMemo } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { FieldExtensionComponentProps } from '../../../extensions'; +import { + OwnedEntityPickerReturnValue, + OwnedEntityPickerUiOptions, +} from './schema'; -/** - * The input props that can be specified under `ui:options` for the - * `OwnedEntityPicker` field extension. - * - * @public - */ -export interface OwnedEntityPickerUiOptions { - allowedKinds?: string[]; - defaultKind?: string; - allowArbitraryValues?: boolean; - defaultNamespace?: string | false; -} - +export { OwnedEntityPickerSchema } from './schema'; /** * The underlying component that is rendered in the form for the `OwnedEntityPicker` * field extension. @@ -48,7 +40,10 @@ export interface OwnedEntityPickerUiOptions { * @public */ export const OwnedEntityPicker = ( - props: FieldExtensionComponentProps, + props: FieldExtensionComponentProps< + OwnedEntityPickerReturnValue, + OwnedEntityPickerUiOptions + >, ) => { const { onChange, diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts index 2988ba8cdc..0101eb8845 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts @@ -13,4 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export type { OwnedEntityPickerUiOptions } from './OwnedEntityPicker'; +export { + type OwnedEntityPickerUiOptions, + OwnedEntityPickerUiOptionsSchema, +} from './schema'; diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts new file mode 100644 index 0000000000..c260fae3cd --- /dev/null +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { JSONSchema7 } from 'json-schema'; +import { z } from 'zod'; +import zodToJsonSchema from 'zod-to-json-schema'; + +/** + * @public + */ +export const OwnedEntityPickerUiOptionsSchema = z.object({ + allowedKinds: z + .array(z.string()) + .optional() + .describe('List of kinds of entities to derive options from'), + defaultKind: z + .string() + .optional() + .describe( + 'The default entity kind. Options of this kind will not be prefixed.', + ), + allowArbitraryValues: z + .boolean() + .optional() + .describe('Whether to allow arbitrary user input. Defaults to true'), + defaultNamespace: z + .union([z.string(), z.literal(false)]) + .optional() + .describe( + 'The default namespace. Options with this namespace will not be prefixed.', + ), +}); + +const OwnedEntityPickerReturnValueSchema = z.string(); + +/** + * The input props that can be specified under `ui:options` for the + * `OwnedEntityPicker` field extension. + * + * @public + */ +export type OwnedEntityPickerUiOptions = z.infer< + typeof OwnedEntityPickerUiOptionsSchema +>; + +export type OwnedEntityPickerReturnValue = z.infer< + typeof OwnedEntityPickerReturnValueSchema +>; + +export const OwnedEntityPickerSchema = { + uiOptions: zodToJsonSchema(OwnedEntityPickerUiOptionsSchema) as JSONSchema7, + returnValue: zodToJsonSchema( + OwnedEntityPickerReturnValueSchema, + ) as JSONSchema7, +}; diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx index 3de2b12bd0..10333684d0 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx @@ -16,18 +16,9 @@ import React from 'react'; import { EntityPicker } from '../EntityPicker/EntityPicker'; import { FieldExtensionComponentProps } from '../../../extensions'; +import { OwnerPickerReturnValue, OwnerPickerUiOptions } from './schema'; -/** - * The input props that can be specified under `ui:options` for the - * `OwnerPicker` field extension. - * - * @public - */ -export interface OwnerPickerUiOptions { - allowedKinds?: string[]; - allowArbitraryValues?: boolean; - defaultNamespace?: string | false; -} +export { OwnerPickerSchema } from './schema'; /** * The underlying component that is rendered in the form for the `OwnerPicker` @@ -36,7 +27,10 @@ export interface OwnerPickerUiOptions { * @public */ export const OwnerPicker = ( - props: FieldExtensionComponentProps, + props: FieldExtensionComponentProps< + OwnerPickerReturnValue, + OwnerPickerUiOptions + >, ) => { const { schema: { title = 'Owner', description = 'The owner of the component' }, diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts index aa26024b5b..5d436358b3 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts @@ -13,4 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export type { OwnerPickerUiOptions } from './OwnerPicker'; +export { + type OwnerPickerUiOptions, + OwnerPickerUiOptionsSchema, +} from './schema'; diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts b/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts new file mode 100644 index 0000000000..634a39ddee --- /dev/null +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts @@ -0,0 +1,60 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { JSONSchema7 } from 'json-schema'; +import { z } from 'zod'; +import zodToJsonSchema from 'zod-to-json-schema'; + +/** + * @public + */ +export const OwnerPickerUiOptionsSchema = z.object({ + allowedKinds: z + .array(z.string()) + .default(['Group', 'User']) + .optional() + .describe( + 'List of kinds of entities to derive options from. Defaults to Group and User', + ), + allowArbitraryValues: z + .boolean() + .optional() + .describe('Whether to allow arbitrary user input. Defaults to true'), + defaultNamespace: z + .union([z.string(), z.literal(false)]) + .optional() + .describe( + 'The default namespace. Options with this namespace will not be prefixed.', + ), +}); + +const OwnerPickerReturnValueSchema = z.string(); + +/** + * The input props that can be specified under `ui:options` for the + * `OwnerPicker` field extension. + * + * @public + */ +export type OwnerPickerUiOptions = z.infer; + +export type OwnerPickerReturnValue = z.infer< + typeof OwnerPickerReturnValueSchema +>; + +export const OwnerPickerSchema = { + uiOptions: zodToJsonSchema(OwnerPickerUiOptionsSchema) as JSONSchema7, + returnValue: zodToJsonSchema(OwnerPickerReturnValueSchema) as JSONSchema7, +}; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 451df857e5..8aca07e995 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -28,32 +28,12 @@ import { FieldExtensionComponentProps } from '../../../extensions'; import { RepoUrlPickerHost } from './RepoUrlPickerHost'; import { RepoUrlPickerRepoName } from './RepoUrlPickerRepoName'; import { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils'; +import { RepoUrlPickerReturnValue, RepoUrlPickerUiOptions } from './schema'; import { RepoUrlPickerState } from './types'; import useDebounce from 'react-use/lib/useDebounce'; import { useTemplateSecrets } from '../../secrets'; -/** - * The input props that can be specified under `ui:options` for the - * `RepoUrlPicker` field extension. - * - * @public - */ -export interface RepoUrlPickerUiOptions { - allowedHosts?: string[]; - allowedOrganizations?: string[]; - allowedOwners?: string[]; - allowedRepos?: string[]; - requestUserCredentials?: { - secretsKey: string; - additionalScopes?: { - gerrit?: string[]; - github?: string[]; - gitlab?: string[]; - bitbucket?: string[]; - azure?: string[]; - }; - }; -} +export { RepoUrlPickerSchema } from './schema'; /** * The underlying component that is rendered in the form for the `RepoUrlPicker` @@ -62,7 +42,10 @@ export interface RepoUrlPickerUiOptions { * @public */ export const RepoUrlPicker = ( - props: FieldExtensionComponentProps, + props: FieldExtensionComponentProps< + RepoUrlPickerReturnValue, + RepoUrlPickerUiOptions + >, ) => { const { uiSchema, onChange, rawErrors, formData } = props; const [state, setState] = useState( diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts index c5f596a786..34124d0ccd 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts @@ -13,5 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export type { RepoUrlPickerUiOptions } from './RepoUrlPicker'; +export { + type RepoUrlPickerUiOptions, + RepoUrlPickerUiOptionsSchema, +} from './schema'; export { repoPickerValidation } from './validation'; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts new file mode 100644 index 0000000000..b870747fd4 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts @@ -0,0 +1,101 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { JSONSchema7 } from 'json-schema'; +import { z } from 'zod'; +import zodToJsonSchema from 'zod-to-json-schema'; + +/** + * @public + */ +export const RepoUrlPickerUiOptionsSchema = z.object({ + allowedHosts: z + .array(z.string()) + .optional() + .describe('List of allowed SCM platform hosts'), + allowedOrganizations: z + .array(z.string()) + .optional() + .describe('List of allowed organizations in the given SCM platform'), + allowedOwners: z + .array(z.string()) + .optional() + .describe('List of allowed owners in the given SCM platform'), + allowedRepos: z + .array(z.string()) + .optional() + .describe('List of allowed repos in the given SCM platform'), + requestUserCredentials: z + .object({ + secretsKey: z + .string() + .describe( + 'Key used within the template secrets context to store the credential', + ), + additionalScopes: z + .object({ + gerrit: z + .array(z.string()) + .optional() + .describe('Additional Gerrit scopes to request'), + github: z + .array(z.string()) + .optional() + .describe('Additional GitHub scopes to request'), + gitlab: z + .array(z.string()) + .optional() + .describe('Additional GitLab scopes to request'), + bitbucket: z + .array(z.string()) + .optional() + .describe('Additional BitBucket scopes to request'), + azure: z + .array(z.string()) + .optional() + .describe('Additional Azure scopes to request'), + }) + .optional() + .describe('Additional permission scopes to request'), + }) + .optional() + .describe( + 'If defined will request user credentials to auth against the given SCM platform', + ), +}); + +const RepoUrlPickerReturnValueSchema = z.string(); + +/** + * The input props that can be specified under `ui:options` for the + * `RepoUrlPicker` field extension. + * + * @public + */ +export type RepoUrlPickerUiOptions = z.infer< + typeof RepoUrlPickerUiOptionsSchema +>; + +export type RepoUrlPickerReturnValue = z.infer< + typeof RepoUrlPickerReturnValueSchema +>; + +// 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: zodToJsonSchema(RepoUrlPickerUiOptionsSchema) as JSONSchema7, + returnValue: zodToJsonSchema(RepoUrlPickerReturnValueSchema) as JSONSchema7, +}; diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index 8081dcb0ca..c6ab5276d0 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -13,40 +13,64 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityPicker } from '../components/fields/EntityPicker/EntityPicker'; -import { EntityNamePicker } from '../components/fields/EntityNamePicker/EntityNamePicker'; +import { + EntityPicker, + EntityPickerSchema, +} from '../components/fields/EntityPicker/EntityPicker'; +import { + EntityNamePicker, + EntityNamePickerSchema, +} from '../components/fields/EntityNamePicker/EntityNamePicker'; import { entityNamePickerValidation } from '../components/fields/EntityNamePicker/validation'; -import { EntityTagsPicker } from '../components/fields/EntityTagsPicker/EntityTagsPicker'; -import { OwnerPicker } from '../components/fields/OwnerPicker/OwnerPicker'; -import { RepoUrlPicker } from '../components/fields/RepoUrlPicker/RepoUrlPicker'; +import { + EntityTagsPicker, + EntityTagsPickerSchema, +} from '../components/fields/EntityTagsPicker/EntityTagsPicker'; +import { + OwnerPicker, + OwnerPickerSchema, +} from '../components/fields/OwnerPicker/OwnerPicker'; +import { + RepoUrlPicker, + RepoUrlPickerSchema, +} from '../components/fields/RepoUrlPicker/RepoUrlPicker'; import { repoPickerValidation } from '../components/fields/RepoUrlPicker/validation'; -import { OwnedEntityPicker } from '../components/fields/OwnedEntityPicker/OwnedEntityPicker'; +import { + OwnedEntityPicker, + OwnedEntityPickerSchema, +} from '../components/fields/OwnedEntityPicker/OwnedEntityPicker'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { component: EntityPicker, name: 'EntityPicker', + schema: EntityPickerSchema, }, { component: EntityNamePicker, name: 'EntityNamePicker', validation: entityNamePickerValidation, + schema: EntityNamePickerSchema, }, { component: EntityTagsPicker, name: 'EntityTagsPicker', + schema: EntityTagsPickerSchema, }, { component: RepoUrlPicker, name: 'RepoUrlPicker', validation: repoPickerValidation, + schema: RepoUrlPickerSchema, }, { component: OwnerPicker, name: 'OwnerPicker', + schema: OwnerPickerSchema, }, { component: OwnedEntityPicker, name: 'OwnedEntityPicker', + schema: OwnedEntityPickerSchema, }, ]; diff --git a/plugins/scaffolder/src/extensions/index.tsx b/plugins/scaffolder/src/extensions/index.tsx index 08075b5b9d..72f17edc20 100644 --- a/plugins/scaffolder/src/extensions/index.tsx +++ b/plugins/scaffolder/src/extensions/index.tsx @@ -16,6 +16,7 @@ import React from 'react'; import { + CustomFieldExtensionSchema, CustomFieldValidator, FieldExtensionOptions, FieldExtensionComponentProps, @@ -103,6 +104,7 @@ attachComponentData( ); export type { + CustomFieldExtensionSchema, CustomFieldValidator, FieldExtensionOptions, FieldExtensionComponentProps, diff --git a/plugins/scaffolder/src/extensions/types.ts b/plugins/scaffolder/src/extensions/types.ts index 62f17dff15..b15de56fc1 100644 --- a/plugins/scaffolder/src/extensions/types.ts +++ b/plugins/scaffolder/src/extensions/types.ts @@ -15,14 +15,14 @@ */ import { ApiHolder } from '@backstage/core-plugin-api'; import { FieldValidation, FieldProps } from '@rjsf/core'; -import { PropsWithChildren } from 'react'; - import { UIOptionsType, FieldProps as FieldPropsV5, UiSchema as UiSchemaV5, FieldValidation as FieldValidationV5, } from '@rjsf/utils'; +import { PropsWithChildren } from 'react'; +import { JSONSchema7 } from 'json-schema'; /** * Field validation type for Custom Field Extensions. @@ -35,6 +35,16 @@ export type CustomFieldValidator = ( context: { apiHolder: ApiHolder }, ) => void | Promise; +/** + * Type for the Custom Field Extension schema. + * + * @public + */ +export type CustomFieldExtensionSchema = { + uiOptions?: JSONSchema7; + returnValue?: JSONSchema7; +}; + /** * Type for the Custom Field Extension with the * name and components and validation function. @@ -50,6 +60,7 @@ export type FieldExtensionOptions< props: FieldExtensionComponentProps, ) => JSX.Element | null; validation?: CustomFieldValidator; + schema?: CustomFieldExtensionSchema; }; /** @@ -107,4 +118,5 @@ export type NextFieldExtensionOptions< props: NextFieldExtensionComponentProps, ) => JSX.Element | null; validation?: NextCustomFieldValidator; + schema?: CustomFieldExtensionSchema; }; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 7853667101..0e5e2630ac 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -43,6 +43,7 @@ export { ScaffolderFieldExtensions, } from './extensions'; export type { + CustomFieldExtensionSchema, CustomFieldValidator, FieldExtensionOptions, FieldExtensionComponentProps, diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index f9b359d93a..a34105745f 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -16,12 +16,24 @@ import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { scaffolderApiRef, ScaffolderClient } from './api'; -import { EntityPicker } from './components/fields/EntityPicker/EntityPicker'; +import { + EntityPicker, + EntityPickerSchema, +} from './components/fields/EntityPicker/EntityPicker'; import { entityNamePickerValidation } from './components/fields/EntityNamePicker'; -import { EntityNamePicker } from './components/fields/EntityNamePicker/EntityNamePicker'; -import { OwnerPicker } from './components/fields/OwnerPicker/OwnerPicker'; +import { + EntityNamePicker, + EntityNamePickerSchema, +} from './components/fields/EntityNamePicker/EntityNamePicker'; +import { + OwnerPicker, + OwnerPickerSchema, +} from './components/fields/OwnerPicker/OwnerPicker'; import { repoPickerValidation } from './components/fields/RepoUrlPicker'; -import { RepoUrlPicker } from './components/fields/RepoUrlPicker/RepoUrlPicker'; +import { + RepoUrlPicker, + RepoUrlPickerSchema, +} from './components/fields/RepoUrlPicker/RepoUrlPicker'; import { createScaffolderFieldExtension } from './extensions'; import { nextRouteRef, @@ -37,8 +49,14 @@ import { fetchApiRef, identityApiRef, } from '@backstage/core-plugin-api'; -import { OwnedEntityPicker } from './components/fields/OwnedEntityPicker/OwnedEntityPicker'; -import { EntityTagsPicker } from './components/fields/EntityTagsPicker/EntityTagsPicker'; +import { + OwnedEntityPicker, + OwnedEntityPickerSchema, +} from './components/fields/OwnedEntityPicker/OwnedEntityPicker'; +import { + EntityTagsPicker, + EntityTagsPickerSchema, +} from './components/fields/EntityTagsPicker/EntityTagsPicker'; /** * The main plugin export for the scaffolder. @@ -82,6 +100,7 @@ export const EntityPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ component: EntityPicker, name: 'EntityPicker', + schema: EntityPickerSchema, }), ); @@ -95,6 +114,7 @@ export const EntityNamePickerFieldExtension = scaffolderPlugin.provide( component: EntityNamePicker, name: 'EntityNamePicker', validation: entityNamePickerValidation, + schema: EntityNamePickerSchema, }), ); @@ -109,6 +129,7 @@ export const RepoUrlPickerFieldExtension = scaffolderPlugin.provide( component: RepoUrlPicker, name: 'RepoUrlPicker', validation: repoPickerValidation, + schema: RepoUrlPickerSchema, }), ); @@ -121,6 +142,7 @@ export const OwnerPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ component: OwnerPicker, name: 'OwnerPicker', + schema: OwnerPickerSchema, }), ); @@ -146,6 +168,7 @@ export const OwnedEntityPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ component: OwnedEntityPicker, name: 'OwnedEntityPicker', + schema: OwnedEntityPickerSchema, }), ); @@ -157,6 +180,7 @@ export const EntityTagsPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ component: EntityTagsPicker, name: 'EntityTagsPicker', + schema: EntityTagsPickerSchema, }), ); diff --git a/yarn.lock b/yarn.lock index acc503697d..eb5d0ffe16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6998,6 +6998,8 @@ __metadata: use-immer: ^0.7.0 yaml: ^2.0.0 zen-observable: ^0.8.15 + zod: ^3.11.6 + zod-to-json-schema: ^3.18.1 peerDependencies: "@types/react": ^16.13.1 || ^17.0.0 react: ^16.13.1 || ^17.0.0