feat(scaffolder): implement custom field explorer

Signed-off-by: Phil Kuang <pkuang@factset.com>
This commit is contained in:
Phil Kuang
2022-10-05 13:58:43 -04:00
parent 905a7613bf
commit ddd1c3308d
29 changed files with 1026 additions and 158 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': minor
---
Implement Custom Field Explorer to view and play around with available installed custom field extensions
+270 -62
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<
@@ -57,6 +58,12 @@ export function createScaffolderLayout<TInputProps = unknown>(
options: LayoutOptions,
): Extension<LayoutComponent<TInputProps>>;
// @public
export type CustomFieldExtensionSchema = {
uiOptions?: JSONSchema7;
returnValue?: JSONSchema7;
};
// @public
export type CustomFieldValidator<TFieldReturnValue> = (
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<typeof EntityPickerUiOptionsSchema>;
// @public (undocumented)
export const EntityPickerUiOptionsSchema: z.ZodObject<
{
allowedKinds: z.ZodOptional<z.ZodArray<z.ZodString, 'many'>>;
defaultKind: z.ZodOptional<z.ZodString>;
allowArbitraryValues: z.ZodOptional<z.ZodBoolean>;
defaultNamespace: z.ZodOptional<
z.ZodUnion<[z.ZodString, z.ZodLiteral<false>]>
>;
},
'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<z.ZodArray<z.ZodString, 'many'>>;
showCounts: z.ZodOptional<z.ZodBoolean>;
helperText: z.ZodOptional<z.ZodString>;
},
'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<TFieldReturnValue, TInputProps>,
) => JSX.Element | null;
validation?: CustomFieldValidator<TFieldReturnValue>;
schema?: CustomFieldExtensionSchema;
};
// @public
@@ -199,6 +249,7 @@ export type NextFieldExtensionOptions<
props: NextFieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
) => JSX.Element | null;
validation?: NextCustomFieldValidator<TFieldReturnValue>;
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<z.ZodArray<z.ZodString, 'many'>>;
defaultKind: z.ZodOptional<z.ZodString>;
allowArbitraryValues: z.ZodOptional<z.ZodBoolean>;
defaultNamespace: z.ZodOptional<
z.ZodUnion<[z.ZodString, z.ZodLiteral<false>]>
>;
},
'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<typeof OwnerPickerUiOptionsSchema>;
// @public (undocumented)
export const OwnerPickerUiOptionsSchema: z.ZodObject<
{
allowedKinds: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString, 'many'>>>;
allowArbitraryValues: z.ZodOptional<z.ZodBoolean>;
defaultNamespace: z.ZodOptional<
z.ZodUnion<[z.ZodString, z.ZodLiteral<false>]>
>;
},
'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<z.ZodArray<z.ZodString, 'many'>>;
allowedOrganizations: z.ZodOptional<z.ZodArray<z.ZodString, 'many'>>;
allowedOwners: z.ZodOptional<z.ZodArray<z.ZodString, 'many'>>;
allowedRepos: z.ZodOptional<z.ZodArray<z.ZodString, 'many'>>;
requestUserCredentials: z.ZodOptional<
z.ZodObject<
{
secretsKey: z.ZodString;
additionalScopes: z.ZodOptional<
z.ZodObject<
{
gerrit: z.ZodOptional<z.ZodArray<z.ZodString, 'many'>>;
github: z.ZodOptional<z.ZodArray<z.ZodString, 'many'>>;
gitlab: z.ZodOptional<z.ZodArray<z.ZodString, 'many'>>;
bitbucket: z.ZodOptional<z.ZodArray<z.ZodString, 'many'>>;
azure: z.ZodOptional<z.ZodArray<z.ZodString, 'many'>>;
},
'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<undefined>;
+3 -1
View File
@@ -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",
@@ -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<any, any>[];
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 (
<main className={classes.root}>
<div className={classes.controls}>
<FormControl variant="outlined" size="small" fullWidth>
<InputLabel id="select-field-label">
Choose Custom Field Extension
</InputLabel>
<Select
value={selectedField}
label="Choose Custom Field Extension"
labelId="select-field-label"
onChange={e => handleSelectionChange(e.target.value)}
>
{fieldOptions.map((option, idx) => (
<MenuItem key={idx} value={option as any}>
{option.name}
</MenuItem>
))}
</Select>
</FormControl>
<IconButton size="medium" onClick={onClose}>
<CloseIcon />
</IconButton>
</div>
<div className={classes.fieldForm}>
<Card>
<CardHeader title="Field Options" />
<CardContent>
<Form
showErrorList={false}
fields={{ ...fieldOverrides, ...fieldComponents }}
noHtml5Validate
formData={fieldFormState}
formContext={{ fieldFormState }}
onSubmit={e => handleFieldConfigChange(e.formData)}
schema={selectedField.schema?.uiOptions || {}}
>
<Button
variant="contained"
color="primary"
type="submit"
disabled={!selectedField.schema?.uiOptions}
>
Apply
</Button>
</Form>
</CardContent>
</Card>
</div>
<div className={classes.preview}>
<Card>
<CardHeader title="Example Template Spec" />
<CardContent>
<CodeMirror
readOnly
theme="dark"
height="100%"
extensions={[StreamLanguage.define(yamlSupport)]}
value={sampleFieldTemplate}
/>
</CardContent>
</Card>
<TemplateEditorForm
key={refreshKey}
content={sampleFieldTemplate}
contentIsSpec
fieldExtensions={customFieldExtensions}
data={formState}
onUpdate={setFormState}
setErrorText={() => null}
/>
</div>
</main>
);
};
@@ -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) {
</Card>
);
const cardFieldExplorer = (
<Card className={classes.card} elevation={4}>
<CardActionArea onClick={() => props.onSelect?.('field-explorer')}>
<CardContent>
<Typography variant="h5" gutterBottom>
Custom Field Explorer
</Typography>
<Typography variant="body1">
View and play around with available installed custom field
extensions.
</Typography>
</CardContent>
</CardActionArea>
</Card>
);
return (
<div style={props.style}>
<Typography variant="h6" className={classes.introText}>
@@ -121,6 +137,7 @@ export function TemplateEditorIntro(props: EditorIntroProps) {
{supportsLoad && cardLoadLocal}
{cardFormEditor}
{!supportsLoad && cardLoadLocal}
{cardFieldExplorer}
</div>
</div>
);
@@ -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 = (
<CustomFieldExplorer
customFieldExtensions={props.customFieldExtensions}
onClose={() => setSelection(undefined)}
/>
);
} else {
content = (
<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' });
}
}}
/>
@@ -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<string>,
props: FieldExtensionComponentProps<EntityNamePickerReturnValue>,
) => {
const {
onChange,
@@ -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,
};
@@ -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<string, EntityPickerUiOptions>,
props: FieldExtensionComponentProps<
EntityPickerReturnValue,
EntityPickerUiOptions
>,
) => {
const {
onChange,
@@ -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';
@@ -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<typeof EntityPickerUiOptionsSchema>;
export type EntityPickerReturnValue = z.infer<
typeof EntityPickerReturnValueSchema
>;
export const EntityPickerSchema = {
uiOptions: zodToJsonSchema(EntityPickerUiOptionsSchema) as JSONSchema7,
returnValue: zodToJsonSchema(EntityPickerReturnValueSchema) as JSONSchema7,
};
@@ -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<string[], EntityTagsPickerUiOptions>,
props: FieldExtensionComponentProps<
EntityTagsPickerReturnValue,
EntityTagsPickerUiOptions
>,
) => {
const { formData, onChange, uiSchema } = props;
const catalogApi = useApi(catalogApiRef);
@@ -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';
@@ -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,
};
@@ -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<string, OwnedEntityPickerUiOptions>,
props: FieldExtensionComponentProps<
OwnedEntityPickerReturnValue,
OwnedEntityPickerUiOptions
>,
) => {
const {
onChange,
@@ -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';
@@ -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,
};
@@ -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<string, OwnerPickerUiOptions>,
props: FieldExtensionComponentProps<
OwnerPickerReturnValue,
OwnerPickerUiOptions
>,
) => {
const {
schema: { title = 'Owner', description = 'The owner of the component' },
@@ -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';
@@ -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<typeof OwnerPickerUiOptionsSchema>;
export type OwnerPickerReturnValue = z.infer<
typeof OwnerPickerReturnValueSchema
>;
export const OwnerPickerSchema = {
uiOptions: zodToJsonSchema(OwnerPickerUiOptionsSchema) as JSONSchema7,
returnValue: zodToJsonSchema(OwnerPickerReturnValueSchema) as JSONSchema7,
};
@@ -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<string, RepoUrlPickerUiOptions>,
props: FieldExtensionComponentProps<
RepoUrlPickerReturnValue,
RepoUrlPickerUiOptions
>,
) => {
const { uiSchema, onChange, rawErrors, formData } = props;
const [state, setState] = useState<RepoUrlPickerState>(
@@ -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';
@@ -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,
};
+30 -6
View File
@@ -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,
},
];
@@ -16,6 +16,7 @@
import React from 'react';
import {
CustomFieldExtensionSchema,
CustomFieldValidator,
FieldExtensionOptions,
FieldExtensionComponentProps,
@@ -103,6 +104,7 @@ attachComponentData(
);
export type {
CustomFieldExtensionSchema,
CustomFieldValidator,
FieldExtensionOptions,
FieldExtensionComponentProps,
+14 -2
View File
@@ -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<TFieldReturnValue> = (
context: { apiHolder: ApiHolder },
) => void | Promise<void>;
/**
* 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<TFieldReturnValue, TInputProps>,
) => JSX.Element | null;
validation?: CustomFieldValidator<TFieldReturnValue>;
schema?: CustomFieldExtensionSchema;
};
/**
@@ -107,4 +118,5 @@ export type NextFieldExtensionOptions<
props: NextFieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
) => JSX.Element | null;
validation?: NextCustomFieldValidator<TFieldReturnValue>;
schema?: CustomFieldExtensionSchema;
};
+1
View File
@@ -43,6 +43,7 @@ export {
ScaffolderFieldExtensions,
} from './extensions';
export type {
CustomFieldExtensionSchema,
CustomFieldValidator,
FieldExtensionOptions,
FieldExtensionComponentProps,
+30 -6
View File
@@ -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,
}),
);
+2
View File
@@ -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