Merge pull request #14008 from kuangp/feat/fieldExplorer
[PRFC] Custom Field Extension Explorer
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': minor
|
||||
---
|
||||
|
||||
Implement Custom Field Explorer to view and play around with available installed custom field extensions
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 148 KiB |
@@ -181,3 +181,102 @@ const CustomFieldExtension = scaffolderPlugin.provide(
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
## Previewing Custom Field Extensions
|
||||
|
||||
You can preview custom field extensions you write in the Backstage UI using the Custom Field Explorer
|
||||
(accessible via the `/create/edit` route by default):
|
||||
|
||||

|
||||
|
||||
In order to make your new custom field extension available in the explorer you will have to define a
|
||||
JSON schema that describes the input/output types on your field like in the following example:
|
||||
|
||||
```tsx
|
||||
//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
|
||||
export const MyCustomExtensionWithOptionsSchema = {
|
||||
uiOptions: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
focused: {
|
||||
description: 'Whether to focus this field',
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
returnValue: { type: 'string' },
|
||||
};
|
||||
|
||||
export const MyCustomExtensionWithOptions = ({
|
||||
onChange,
|
||||
rawErrors,
|
||||
required,
|
||||
formData,
|
||||
}: FieldExtensionComponentProps<string, { focused?: boolean }>) => {
|
||||
return (
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required={required}
|
||||
error={rawErrors?.length > 0 && !formData}
|
||||
onChange={onChange}
|
||||
focused={focused}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
```tsx
|
||||
// packages/app/src/scaffolder/MyCustomExtensionWithOptions/extensions.ts
|
||||
...
|
||||
import { MyCustomExtensionWithOptions, MyCustomExtensionWithOptionsSchema } from './MyCustomExtensionWithOptions';
|
||||
|
||||
export const MyCustomFieldWithOptionsExtension = scaffolderPlugin.provide(
|
||||
createScaffolderFieldExtension({
|
||||
name: 'MyCustomExtensionWithOptions',
|
||||
component: MyCustomExtensionWithOptions,
|
||||
schema: MyCustomExtensionWithOptionsSchema,
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
We recommend using a library like [zod](https://github.com/colinhacks/zod) to define your schema
|
||||
and the provided `makeFieldSchemaFromZod` helper utility function to generate both the JSON schema
|
||||
and type for your field props to preventing having to duplicate the definitions:
|
||||
|
||||
```tsx
|
||||
//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
|
||||
...
|
||||
import { z } from 'zod';
|
||||
import { makeFieldSchemaFromZod } from '@backstage/plugin-scaffolder';
|
||||
|
||||
const MyCustomExtensionWithOptionsFieldSchema = makeFieldSchemaFromZod(
|
||||
z.string(),
|
||||
z.object({
|
||||
focused: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether to focus this field'),
|
||||
}),
|
||||
);
|
||||
|
||||
export const MyCustomExtensionWithOptionsSchema = MyCustomExtensionWithOptionsFieldSchema.schema;
|
||||
|
||||
type MyCustomExtensionWithOptionsProps = typeof MyCustomExtensionWithOptionsFieldSchema.type;
|
||||
|
||||
export const MyCustomExtensionWithOptions = ({
|
||||
onChange,
|
||||
rawErrors,
|
||||
required,
|
||||
formData,
|
||||
}: MyCustomExtensionWithOptionsProps) => {
|
||||
return (
|
||||
<FormControl
|
||||
margin="normal"
|
||||
required={required}
|
||||
error={rawErrors?.length > 0 && !formData}
|
||||
onChange={onChange}
|
||||
focused={focused}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
@@ -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 = {
|
||||
returnValue: JSONSchema7;
|
||||
uiOptions?: JSONSchema7;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type CustomFieldValidator<TFieldReturnValue> = (
|
||||
data: TFieldReturnValue,
|
||||
@@ -75,36 +82,52 @@ 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 (undocumented)
|
||||
export const EntityPickerFieldSchema: FieldSchema<
|
||||
string,
|
||||
{
|
||||
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 =
|
||||
typeof EntityPickerFieldSchema.uiOptionsType;
|
||||
|
||||
// @public
|
||||
export const EntityTagsPickerFieldExtension: FieldExtensionComponent<
|
||||
string[],
|
||||
EntityTagsPickerUiOptions
|
||||
{
|
||||
showCounts?: boolean | undefined;
|
||||
kinds?: string[] | undefined;
|
||||
helperText?: string | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const EntityTagsPickerFieldSchema: FieldSchema<
|
||||
string[],
|
||||
{
|
||||
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 =
|
||||
typeof EntityTagsPickerFieldSchema.uiOptionsType;
|
||||
|
||||
// @public
|
||||
export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null;
|
||||
@@ -130,8 +153,19 @@ export type FieldExtensionOptions<
|
||||
props: FieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
|
||||
) => JSX.Element | null;
|
||||
validation?: CustomFieldValidator<TFieldReturnValue>;
|
||||
schema?: CustomFieldExtensionSchema;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface FieldSchema<TReturn, TUiOptions> {
|
||||
// (undocumented)
|
||||
readonly schema: CustomFieldExtensionSchema;
|
||||
// (undocumented)
|
||||
readonly type: FieldExtensionComponentProps<TReturn, TUiOptions>;
|
||||
// (undocumented)
|
||||
readonly uiOptionsType: TUiOptions;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type LayoutComponent<_TInputProps> = () => null;
|
||||
|
||||
@@ -169,6 +203,20 @@ export type LogEvent = {
|
||||
taskId: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function makeFieldSchemaFromZod<
|
||||
TReturnSchema extends z.ZodType,
|
||||
TUiOptionsSchema extends z.ZodType = z.ZodType<any, any, {}>,
|
||||
>(
|
||||
returnSchema: TReturnSchema,
|
||||
uiOptionsSchema?: TUiOptionsSchema,
|
||||
): FieldSchema<
|
||||
TReturnSchema extends z.ZodType<any, any, infer IReturn> ? IReturn : never,
|
||||
TUiOptionsSchema extends z.ZodType<any, any, infer IUiOptions>
|
||||
? IUiOptions
|
||||
: never
|
||||
>;
|
||||
|
||||
// @alpha
|
||||
export type NextCustomFieldValidator<TFieldReturnValue> = (
|
||||
data: TFieldReturnValue,
|
||||
@@ -199,6 +247,7 @@ export type NextFieldExtensionOptions<
|
||||
props: NextFieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
|
||||
) => JSX.Element | null;
|
||||
validation?: NextCustomFieldValidator<TFieldReturnValue>;
|
||||
schema?: CustomFieldExtensionSchema;
|
||||
};
|
||||
|
||||
// @alpha (undocumented)
|
||||
@@ -228,36 +277,51 @@ 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 (undocumented)
|
||||
export const OwnedEntityPickerFieldSchema: FieldSchema<
|
||||
string,
|
||||
{
|
||||
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 =
|
||||
typeof OwnedEntityPickerFieldSchema.uiOptionsType;
|
||||
|
||||
// @public
|
||||
export const OwnerPickerFieldExtension: FieldExtensionComponent<
|
||||
string,
|
||||
OwnerPickerUiOptions
|
||||
{
|
||||
defaultNamespace?: string | false | undefined;
|
||||
allowedKinds?: string[] | undefined;
|
||||
allowArbitraryValues?: boolean | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export const OwnerPickerFieldSchema: FieldSchema<
|
||||
string,
|
||||
{
|
||||
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 = typeof OwnerPickerFieldSchema.uiOptionsType;
|
||||
|
||||
// @public
|
||||
export const repoPickerValidation: (
|
||||
@@ -271,31 +335,56 @@ 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 (undocumented)
|
||||
export const RepoUrlPickerFieldSchema: FieldSchema<
|
||||
string,
|
||||
{
|
||||
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 =
|
||||
typeof RepoUrlPickerFieldSchema.uiOptionsType;
|
||||
|
||||
// @public (undocumented)
|
||||
export const rootRouteRef: RouteRef<undefined>;
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -14,15 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { FieldExtensionComponentProps } from '../../../extensions';
|
||||
import { EntityNamePickerProps } from './schema';
|
||||
import { TextField } from '@material-ui/core';
|
||||
|
||||
export { EntityNamePickerSchema } from './schema';
|
||||
|
||||
/**
|
||||
* EntityName Picker
|
||||
*/
|
||||
export const EntityNamePicker = (
|
||||
props: FieldExtensionComponentProps<string>,
|
||||
) => {
|
||||
export const EntityNamePicker = (props: EntityNamePickerProps) => {
|
||||
const {
|
||||
onChange,
|
||||
required,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 { z } from 'zod';
|
||||
import { makeFieldSchemaFromZod } from '../utils';
|
||||
|
||||
const EntityNamePickerFieldSchema = makeFieldSchemaFromZod(z.string());
|
||||
|
||||
export const EntityNamePickerSchema = EntityNamePickerFieldSchema.schema;
|
||||
|
||||
export type EntityNamePickerProps = typeof EntityNamePickerFieldSchema.type;
|
||||
@@ -23,20 +23,9 @@ import FormControl from '@material-ui/core/FormControl';
|
||||
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 { EntityPickerProps } 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`
|
||||
@@ -44,9 +33,7 @@ export interface EntityPickerUiOptions {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const EntityPicker = (
|
||||
props: FieldExtensionComponentProps<string, EntityPickerUiOptions>,
|
||||
) => {
|
||||
export const EntityPicker = (props: EntityPickerProps) => {
|
||||
const {
|
||||
onChange,
|
||||
schema: { title = 'Entity', description = 'An entity from the catalog' },
|
||||
|
||||
@@ -13,4 +13,4 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export type { EntityPickerUiOptions } from './EntityPicker';
|
||||
export { EntityPickerFieldSchema, type EntityPickerUiOptions } from './schema';
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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 { z } from 'zod';
|
||||
import { makeFieldSchemaFromZod } from '../utils';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const EntityPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
z.string(),
|
||||
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.',
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* The input props that can be specified under `ui:options` for the
|
||||
* `EntityPicker` field extension.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type EntityPickerUiOptions =
|
||||
typeof EntityPickerFieldSchema.uiOptionsType;
|
||||
|
||||
export type EntityPickerProps = typeof EntityPickerFieldSchema.type;
|
||||
|
||||
export const EntityPickerSchema = EntityPickerFieldSchema.schema;
|
||||
@@ -22,19 +22,9 @@ import { useApi } from '@backstage/core-plugin-api';
|
||||
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 { EntityTagsPickerProps } 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`
|
||||
@@ -42,9 +32,7 @@ export interface EntityTagsPickerUiOptions {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const EntityTagsPicker = (
|
||||
props: FieldExtensionComponentProps<string[], EntityTagsPickerUiOptions>,
|
||||
) => {
|
||||
export const EntityTagsPicker = (props: EntityTagsPickerProps) => {
|
||||
const { formData, onChange, uiSchema } = props;
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const [tagOptions, setTagOptions] = useState<string[]>([]);
|
||||
|
||||
@@ -13,4 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export type { EntityTagsPickerUiOptions } from './EntityTagsPicker';
|
||||
export {
|
||||
EntityTagsPickerFieldSchema,
|
||||
type EntityTagsPickerUiOptions,
|
||||
} from './schema';
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 { z } from 'zod';
|
||||
import { makeFieldSchemaFromZod } from '../utils';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const EntityTagsPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
z.array(z.string()),
|
||||
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'),
|
||||
}),
|
||||
);
|
||||
|
||||
export const EntityTagsPickerSchema = EntityTagsPickerFieldSchema.schema;
|
||||
|
||||
export type EntityTagsPickerProps = typeof EntityTagsPickerFieldSchema.type;
|
||||
|
||||
/**
|
||||
* The input props that can be specified under `ui:options` for the
|
||||
* `EntityTagsPicker` field extension.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type EntityTagsPickerUiOptions =
|
||||
typeof EntityTagsPickerFieldSchema.uiOptionsType;
|
||||
@@ -26,20 +26,9 @@ import Autocomplete from '@material-ui/lab/Autocomplete';
|
||||
import React, { useMemo } from 'react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
import { FieldExtensionComponentProps } from '../../../extensions';
|
||||
import { OwnedEntityPickerProps } 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`
|
||||
@@ -47,9 +36,7 @@ export interface OwnedEntityPickerUiOptions {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const OwnedEntityPicker = (
|
||||
props: FieldExtensionComponentProps<string, OwnedEntityPickerUiOptions>,
|
||||
) => {
|
||||
export const OwnedEntityPicker = (props: OwnedEntityPickerProps) => {
|
||||
const {
|
||||
onChange,
|
||||
schema: { title = 'Entity', description = 'An entity from the catalog' },
|
||||
|
||||
@@ -13,4 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export type { OwnedEntityPickerUiOptions } from './OwnedEntityPicker';
|
||||
export {
|
||||
OwnedEntityPickerFieldSchema,
|
||||
type OwnedEntityPickerUiOptions,
|
||||
} from './schema';
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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 { z } from 'zod';
|
||||
import { makeFieldSchemaFromZod } from '../utils';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const OwnedEntityPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
z.string(),
|
||||
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.',
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* The input props that can be specified under `ui:options` for the
|
||||
* `OwnedEntityPicker` field extension.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type OwnedEntityPickerUiOptions =
|
||||
typeof OwnedEntityPickerFieldSchema.uiOptionsType;
|
||||
|
||||
export type OwnedEntityPickerProps = typeof OwnedEntityPickerFieldSchema.type;
|
||||
|
||||
export const OwnedEntityPickerSchema = OwnedEntityPickerFieldSchema.schema;
|
||||
@@ -15,19 +15,9 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EntityPicker } from '../EntityPicker/EntityPicker';
|
||||
import { FieldExtensionComponentProps } from '../../../extensions';
|
||||
import { OwnerPickerProps } 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`
|
||||
@@ -35,9 +25,7 @@ export interface OwnerPickerUiOptions {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const OwnerPicker = (
|
||||
props: FieldExtensionComponentProps<string, OwnerPickerUiOptions>,
|
||||
) => {
|
||||
export const OwnerPicker = (props: OwnerPickerProps) => {
|
||||
const {
|
||||
schema: { title = 'Owner', description = 'The owner of the component' },
|
||||
uiSchema,
|
||||
|
||||
@@ -13,4 +13,4 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export type { OwnerPickerUiOptions } from './OwnerPicker';
|
||||
export { OwnerPickerFieldSchema, type OwnerPickerUiOptions } from './schema';
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 { z } from 'zod';
|
||||
import { makeFieldSchemaFromZod } from '../utils';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const OwnerPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
z.string(),
|
||||
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.',
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* The input props that can be specified under `ui:options` for the
|
||||
* `OwnerPicker` field extension.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type OwnerPickerUiOptions = typeof OwnerPickerFieldSchema.uiOptionsType;
|
||||
|
||||
export type OwnerPickerProps = typeof OwnerPickerFieldSchema.type;
|
||||
|
||||
export const OwnerPickerSchema = OwnerPickerFieldSchema.schema;
|
||||
@@ -24,36 +24,15 @@ import { GitlabRepoPicker } from './GitlabRepoPicker';
|
||||
import { AzureRepoPicker } from './AzureRepoPicker';
|
||||
import { BitbucketRepoPicker } from './BitbucketRepoPicker';
|
||||
import { GerritRepoPicker } from './GerritRepoPicker';
|
||||
import { FieldExtensionComponentProps } from '../../../extensions';
|
||||
import { RepoUrlPickerHost } from './RepoUrlPickerHost';
|
||||
import { RepoUrlPickerRepoName } from './RepoUrlPickerRepoName';
|
||||
import { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils';
|
||||
import { RepoUrlPickerProps } 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`
|
||||
@@ -61,9 +40,7 @@ export interface RepoUrlPickerUiOptions {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const RepoUrlPicker = (
|
||||
props: FieldExtensionComponentProps<string, RepoUrlPickerUiOptions>,
|
||||
) => {
|
||||
export const RepoUrlPicker = (props: RepoUrlPickerProps) => {
|
||||
const { uiSchema, onChange, rawErrors, formData } = props;
|
||||
const [state, setState] = useState<RepoUrlPickerState>(
|
||||
parseRepoPickerUrl(formData),
|
||||
|
||||
@@ -13,5 +13,8 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export type { RepoUrlPickerUiOptions } from './RepoUrlPicker';
|
||||
export {
|
||||
RepoUrlPickerFieldSchema,
|
||||
type RepoUrlPickerUiOptions,
|
||||
} from './schema';
|
||||
export { repoPickerValidation } from './validation';
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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 { z } from 'zod';
|
||||
import { makeFieldSchemaFromZod } from '../utils';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const RepoUrlPickerFieldSchema = makeFieldSchemaFromZod(
|
||||
z.string(),
|
||||
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',
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* The input props that can be specified under `ui:options` for the
|
||||
* `RepoUrlPicker` field extension.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type RepoUrlPickerUiOptions =
|
||||
typeof RepoUrlPickerFieldSchema.uiOptionsType;
|
||||
|
||||
export type RepoUrlPickerProps = typeof RepoUrlPickerFieldSchema.type;
|
||||
|
||||
// NOTE: There is a bug with this failing validation in the custom field explorer due
|
||||
// to https://github.com/rjsf-team/react-jsonschema-form/issues/675 even if
|
||||
// requestUserCredentials is not defined
|
||||
export const RepoUrlPickerSchema = RepoUrlPickerFieldSchema.schema;
|
||||
@@ -18,3 +18,4 @@ export * from './OwnerPicker';
|
||||
export * from './RepoUrlPicker';
|
||||
export * from './OwnedEntityPicker';
|
||||
export * from './EntityTagsPicker';
|
||||
export { type FieldSchema, makeFieldSchemaFromZod } from './utils';
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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';
|
||||
import {
|
||||
CustomFieldExtensionSchema,
|
||||
FieldExtensionComponentProps,
|
||||
} from '../../extensions';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* FieldSchema encapsulates a JSONSchema7 along with the
|
||||
* matching FieldExtensionComponentProps type for a field extension.
|
||||
*/
|
||||
export interface FieldSchema<TReturn, TUiOptions> {
|
||||
readonly schema: CustomFieldExtensionSchema;
|
||||
readonly type: FieldExtensionComponentProps<TReturn, TUiOptions>;
|
||||
readonly uiOptionsType: TUiOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Utility function to convert zod return and UI options schemas to a
|
||||
* CustomFieldExtensionSchema with FieldExtensionComponentProps type inference
|
||||
*/
|
||||
export function makeFieldSchemaFromZod<
|
||||
TReturnSchema extends z.ZodType,
|
||||
TUiOptionsSchema extends z.ZodType = z.ZodType<any, any, {}>,
|
||||
>(
|
||||
returnSchema: TReturnSchema,
|
||||
uiOptionsSchema?: TUiOptionsSchema,
|
||||
): FieldSchema<
|
||||
TReturnSchema extends z.ZodType<any, any, infer IReturn> ? IReturn : never,
|
||||
TUiOptionsSchema extends z.ZodType<any, any, infer IUiOptions>
|
||||
? IUiOptions
|
||||
: never
|
||||
> {
|
||||
return {
|
||||
schema: {
|
||||
returnValue: zodToJsonSchema(returnSchema) as JSONSchema7,
|
||||
uiOptions: uiOptionsSchema
|
||||
? (zodToJsonSchema(uiOptionsSchema) as JSONSchema7)
|
||||
: undefined,
|
||||
},
|
||||
type: null as any,
|
||||
uiOptionsType: null as any,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
returnValue: JSONSchema7;
|
||||
uiOptions?: 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;
|
||||
};
|
||||
|
||||
@@ -43,6 +43,7 @@ export {
|
||||
ScaffolderFieldExtensions,
|
||||
} from './extensions';
|
||||
export type {
|
||||
CustomFieldExtensionSchema,
|
||||
CustomFieldValidator,
|
||||
FieldExtensionOptions,
|
||||
FieldExtensionComponentProps,
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user