Merge remote-tracking branch 'origin/master' into scaffolder-result-content
# Conflicts: # plugins/scaffolder/api-report.md # plugins/scaffolder/src/plugin.tsx
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
|
||||
@@ -407,3 +407,4 @@ zod
|
||||
Zolotusky
|
||||
zoomable
|
||||
zsh
|
||||
Lainfiesta
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
title: The Linux Foundation launches its Introduction to Backstage course
|
||||
description: The new Linux Foundation course is available for free on edX and is designed to help individuals understand how to map Backstage to their organization needs
|
||||
author: Jorge Lainfiesta, Roadie
|
||||
authorURL: https://www.linkedin.com/in/jrlainfiesta/
|
||||
---
|
||||
|
||||
Backstage continues to grow in [popularity and maturity](https://roadie.io/blog/backstage-consolidating-its-role-in-the-cloud-native-ecosystem/), with industry leaders not only adopting the framework but actively participating in the community through contributions and commercial offerings. One of the most recent contributions comes from the Linux Foundation, [launching an introductory course](https://training.linuxfoundation.org/blog/23107/) aimed at DevOps engineers or professionals working on Developer Productivity or Developer Experience. The course is available for free on edX, with a paid option for the certified track.
|
||||
|
||||
[](https://www.edx.org/course/introduction-to-backstage-developer-portals-made-easy)
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
The course, [Introduction to Backstage: Developer Portals Made Easy (LFS142x)](https://www.edx.org/course/introduction-to-backstage-developer-portals-made-easy), starts discussing the benefits of adopting a Developer Portal. Then, it dives into what is Backstage, its main features (Catalog, Scaffolder, TechDocs), and how to map its capabilities to your organization. At last, it provides tips on how to connect with the community to ease up your adoption journey.
|
||||
|
||||
The course author, [Jorge Lainfiesta](https://www.linkedin.com/in/jrlainfiesta/), is a Technical Marketing Manager at [Roadie](https://roadie.io). Jorge has a background in software engineering (PayPal) and digital communication (UCLA). He’s been working around Backstage since it was open sourced by Spotify and co-hosts community initiatives like the Backstage Users Unconference.
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
+317
-317
@@ -39,72 +39,72 @@ import { UiSchema as UiSchema_2 } from '@rjsf/utils';
|
||||
|
||||
// @alpha
|
||||
export function createNextScaffolderFieldExtension<
|
||||
TReturnValue = unknown,
|
||||
TInputProps extends UIOptionsType = {},
|
||||
>(
|
||||
options: NextFieldExtensionOptions<TReturnValue, TInputProps>,
|
||||
TReturnValue = unknown,
|
||||
TInputProps extends UIOptionsType = {},
|
||||
>(
|
||||
options: NextFieldExtensionOptions<TReturnValue, TInputProps>,
|
||||
): Extension<FieldExtensionComponent<TReturnValue, TInputProps>>;
|
||||
|
||||
// @public
|
||||
export function createScaffolderFieldExtension<
|
||||
TReturnValue = unknown,
|
||||
TInputProps = unknown,
|
||||
>(
|
||||
options: FieldExtensionOptions<TReturnValue, TInputProps>,
|
||||
TReturnValue = unknown,
|
||||
TInputProps = unknown,
|
||||
>(
|
||||
options: FieldExtensionOptions<TReturnValue, TInputProps>,
|
||||
): Extension<FieldExtensionComponent<TReturnValue, TInputProps>>;
|
||||
|
||||
// @public
|
||||
export function createScaffolderLayout<TInputProps = unknown>(
|
||||
options: LayoutOptions,
|
||||
options: LayoutOptions,
|
||||
): Extension<LayoutComponent<TInputProps>>;
|
||||
|
||||
// @public
|
||||
export type CustomFieldValidator<TFieldReturnValue> = (
|
||||
data: TFieldReturnValue,
|
||||
field: FieldValidation,
|
||||
context: {
|
||||
apiHolder: ApiHolder;
|
||||
},
|
||||
data: TFieldReturnValue,
|
||||
field: FieldValidation,
|
||||
context: {
|
||||
apiHolder: ApiHolder;
|
||||
},
|
||||
) => void | Promise<void>;
|
||||
|
||||
// @public
|
||||
export const EntityNamePickerFieldExtension: FieldExtensionComponent<
|
||||
string,
|
||||
{}
|
||||
>;
|
||||
string,
|
||||
{}
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const EntityPickerFieldExtension: FieldExtensionComponent<
|
||||
string,
|
||||
EntityPickerUiOptions
|
||||
>;
|
||||
string,
|
||||
EntityPickerUiOptions
|
||||
>;
|
||||
|
||||
// @public
|
||||
export interface EntityPickerUiOptions {
|
||||
// (undocumented)
|
||||
allowArbitraryValues?: boolean;
|
||||
// (undocumented)
|
||||
allowedKinds?: string[];
|
||||
// (undocumented)
|
||||
defaultKind?: string;
|
||||
// (undocumented)
|
||||
defaultNamespace?: string | false;
|
||||
// (undocumented)
|
||||
allowArbitraryValues?: boolean;
|
||||
// (undocumented)
|
||||
allowedKinds?: string[];
|
||||
// (undocumented)
|
||||
defaultKind?: string;
|
||||
// (undocumented)
|
||||
defaultNamespace?: string | false;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const EntityTagsPickerFieldExtension: FieldExtensionComponent<
|
||||
string[],
|
||||
EntityTagsPickerUiOptions
|
||||
>;
|
||||
string[],
|
||||
EntityTagsPickerUiOptions
|
||||
>;
|
||||
|
||||
// @public
|
||||
export interface EntityTagsPickerUiOptions {
|
||||
// (undocumented)
|
||||
helperText?: string;
|
||||
// (undocumented)
|
||||
kinds?: string[];
|
||||
// (undocumented)
|
||||
showCounts?: boolean;
|
||||
// (undocumented)
|
||||
helperText?: string;
|
||||
// (undocumented)
|
||||
kinds?: string[];
|
||||
// (undocumented)
|
||||
showCounts?: boolean;
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -112,25 +112,25 @@ export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null;
|
||||
|
||||
// @public
|
||||
export interface FieldExtensionComponentProps<
|
||||
TFieldReturnValue,
|
||||
TUiOptions extends {} = {},
|
||||
> extends FieldProps<TFieldReturnValue> {
|
||||
// (undocumented)
|
||||
uiSchema: FieldProps['uiSchema'] & {
|
||||
'ui:options'?: TUiOptions;
|
||||
};
|
||||
TFieldReturnValue,
|
||||
TUiOptions extends {} = {},
|
||||
> extends FieldProps<TFieldReturnValue> {
|
||||
// (undocumented)
|
||||
uiSchema: FieldProps['uiSchema'] & {
|
||||
'ui:options'?: TUiOptions;
|
||||
};
|
||||
}
|
||||
|
||||
// @public
|
||||
export type FieldExtensionOptions<
|
||||
TFieldReturnValue = unknown,
|
||||
TInputProps = unknown,
|
||||
> = {
|
||||
name: string;
|
||||
component: (
|
||||
props: FieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
|
||||
) => JSX.Element | null;
|
||||
validation?: CustomFieldValidator<TFieldReturnValue>;
|
||||
TFieldReturnValue = unknown,
|
||||
TInputProps = unknown,
|
||||
> = {
|
||||
name: string;
|
||||
component: (
|
||||
props: FieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
|
||||
) => JSX.Element | null;
|
||||
validation?: CustomFieldValidator<TFieldReturnValue>;
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -138,10 +138,10 @@ export type LayoutComponent<_TInputProps> = () => null;
|
||||
|
||||
// @public
|
||||
export interface LayoutOptions<P = any> {
|
||||
// (undocumented)
|
||||
component: LayoutTemplate<P>;
|
||||
// (undocumented)
|
||||
name: string;
|
||||
// (undocumented)
|
||||
component: LayoutTemplate<P>;
|
||||
// (undocumented)
|
||||
name: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -149,57 +149,57 @@ export type LayoutTemplate<T = any> = FormProps<T>['ObjectFieldTemplate'];
|
||||
|
||||
// @public
|
||||
export type ListActionsResponse = Array<{
|
||||
id: string;
|
||||
description?: string;
|
||||
schema?: {
|
||||
input?: JSONSchema7;
|
||||
output?: JSONSchema7;
|
||||
};
|
||||
id: string;
|
||||
description?: string;
|
||||
schema?: {
|
||||
input?: JSONSchema7;
|
||||
output?: JSONSchema7;
|
||||
};
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export type LogEvent = {
|
||||
type: 'log' | 'completion';
|
||||
body: {
|
||||
message: string;
|
||||
stepId?: string;
|
||||
status?: ScaffolderTaskStatus;
|
||||
};
|
||||
createdAt: string;
|
||||
id: string;
|
||||
taskId: string;
|
||||
type: 'log' | 'completion';
|
||||
body: {
|
||||
message: string;
|
||||
stepId?: string;
|
||||
status?: ScaffolderTaskStatus;
|
||||
};
|
||||
createdAt: string;
|
||||
id: string;
|
||||
taskId: string;
|
||||
};
|
||||
|
||||
// @alpha
|
||||
export type NextCustomFieldValidator<TFieldReturnValue> = (
|
||||
data: TFieldReturnValue,
|
||||
field: FieldValidation_2,
|
||||
context: {
|
||||
apiHolder: ApiHolder;
|
||||
},
|
||||
data: TFieldReturnValue,
|
||||
field: FieldValidation_2,
|
||||
context: {
|
||||
apiHolder: ApiHolder;
|
||||
},
|
||||
) => void | Promise<void>;
|
||||
|
||||
// @alpha
|
||||
export interface NextFieldExtensionComponentProps<
|
||||
TFieldReturnValue,
|
||||
TUiOptions = {},
|
||||
> extends PropsWithChildren<FieldProps_2<TFieldReturnValue>> {
|
||||
// (undocumented)
|
||||
uiSchema?: UiSchema_2<TFieldReturnValue> & {
|
||||
'ui:options'?: TUiOptions & UIOptionsType;
|
||||
};
|
||||
TFieldReturnValue,
|
||||
TUiOptions = {},
|
||||
> extends PropsWithChildren<FieldProps_2<TFieldReturnValue>> {
|
||||
// (undocumented)
|
||||
uiSchema?: UiSchema_2<TFieldReturnValue> & {
|
||||
'ui:options'?: TUiOptions & UIOptionsType;
|
||||
};
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export type NextFieldExtensionOptions<
|
||||
TFieldReturnValue = unknown,
|
||||
TInputProps = unknown,
|
||||
> = {
|
||||
name: string;
|
||||
component: (
|
||||
props: NextFieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
|
||||
) => JSX.Element | null;
|
||||
validation?: NextCustomFieldValidator<TFieldReturnValue>;
|
||||
TFieldReturnValue = unknown,
|
||||
TInputProps = unknown,
|
||||
> = {
|
||||
name: string;
|
||||
component: (
|
||||
props: NextFieldExtensionComponentProps<TFieldReturnValue, TInputProps>,
|
||||
) => JSX.Element | null;
|
||||
validation?: NextCustomFieldValidator<TFieldReturnValue>;
|
||||
};
|
||||
|
||||
// @alpha (undocumented)
|
||||
@@ -207,109 +207,109 @@ export const nextRouteRef: RouteRef<undefined>;
|
||||
|
||||
// @alpha
|
||||
export type NextRouterProps = {
|
||||
components?: {
|
||||
TemplateCardComponent?: React_2.ComponentType<{
|
||||
template: TemplateEntityV1beta3;
|
||||
}>;
|
||||
TaskPageComponent?: React_2.ComponentType<{}>;
|
||||
};
|
||||
groups?: TemplateGroupFilter[];
|
||||
components?: {
|
||||
TemplateCardComponent?: React_2.ComponentType<{
|
||||
template: TemplateEntityV1beta3;
|
||||
}>;
|
||||
TaskPageComponent?: React_2.ComponentType<{}>;
|
||||
};
|
||||
groups?: TemplateGroupFilter[];
|
||||
};
|
||||
|
||||
// @alpha
|
||||
export const NextScaffolderPage: (
|
||||
props: PropsWithChildren<NextRouterProps>,
|
||||
props: PropsWithChildren<NextRouterProps>,
|
||||
) => JSX.Element;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const nextSelectedTemplateRouteRef: SubRouteRef<
|
||||
PathParams<'/templates/:namespace/:templateName'>
|
||||
>;
|
||||
PathParams<'/templates/:namespace/:templateName'>
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const OwnedEntityPickerFieldExtension: FieldExtensionComponent<
|
||||
string,
|
||||
OwnedEntityPickerUiOptions
|
||||
>;
|
||||
string,
|
||||
OwnedEntityPickerUiOptions
|
||||
>;
|
||||
|
||||
// @public
|
||||
export interface OwnedEntityPickerUiOptions {
|
||||
// (undocumented)
|
||||
allowArbitraryValues?: boolean;
|
||||
// (undocumented)
|
||||
allowedKinds?: string[];
|
||||
// (undocumented)
|
||||
defaultKind?: string;
|
||||
// (undocumented)
|
||||
defaultNamespace?: string | false;
|
||||
// (undocumented)
|
||||
allowArbitraryValues?: boolean;
|
||||
// (undocumented)
|
||||
allowedKinds?: string[];
|
||||
// (undocumented)
|
||||
defaultKind?: string;
|
||||
// (undocumented)
|
||||
defaultNamespace?: string | false;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const OwnerPickerFieldExtension: FieldExtensionComponent<
|
||||
string,
|
||||
OwnerPickerUiOptions
|
||||
>;
|
||||
string,
|
||||
OwnerPickerUiOptions
|
||||
>;
|
||||
|
||||
// @public
|
||||
export interface OwnerPickerUiOptions {
|
||||
// (undocumented)
|
||||
allowArbitraryValues?: boolean;
|
||||
// (undocumented)
|
||||
allowedKinds?: string[];
|
||||
// (undocumented)
|
||||
defaultNamespace?: string | false;
|
||||
// (undocumented)
|
||||
allowArbitraryValues?: boolean;
|
||||
// (undocumented)
|
||||
allowedKinds?: string[];
|
||||
// (undocumented)
|
||||
defaultNamespace?: string | false;
|
||||
}
|
||||
|
||||
// @public
|
||||
export const repoPickerValidation: (
|
||||
value: string,
|
||||
validation: FieldValidation,
|
||||
context: {
|
||||
apiHolder: ApiHolder;
|
||||
},
|
||||
value: string,
|
||||
validation: FieldValidation,
|
||||
context: {
|
||||
apiHolder: ApiHolder;
|
||||
},
|
||||
) => void;
|
||||
|
||||
// @public
|
||||
export const RepoUrlPickerFieldExtension: FieldExtensionComponent<
|
||||
string,
|
||||
RepoUrlPickerUiOptions
|
||||
>;
|
||||
string,
|
||||
RepoUrlPickerUiOptions
|
||||
>;
|
||||
|
||||
// @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[];
|
||||
// (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[];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// @public
|
||||
export type ReviewStepProps = {
|
||||
disableButtons: boolean;
|
||||
formData: JsonObject;
|
||||
handleBack: () => void;
|
||||
handleReset: () => void;
|
||||
handleCreate: () => void;
|
||||
steps: {
|
||||
uiSchema: UiSchema;
|
||||
mergedSchema: JsonObject;
|
||||
schema: JsonObject;
|
||||
}[];
|
||||
disableButtons: boolean;
|
||||
formData: JsonObject;
|
||||
handleBack: () => void;
|
||||
handleReset: () => void;
|
||||
handleCreate: () => void;
|
||||
steps: {
|
||||
uiSchema: UiSchema;
|
||||
mergedSchema: JsonObject;
|
||||
schema: JsonObject;
|
||||
}[];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -317,55 +317,55 @@ export const rootRouteRef: RouteRef<undefined>;
|
||||
|
||||
// @public
|
||||
export type RouterProps = {
|
||||
components?: {
|
||||
ReviewStepComponent?: ComponentType<ReviewStepProps>;
|
||||
TemplateCardComponent?:
|
||||
| ComponentType<{
|
||||
template: TemplateEntityV1beta3;
|
||||
components?: {
|
||||
ReviewStepComponent?: ComponentType<ReviewStepProps>;
|
||||
TemplateCardComponent?:
|
||||
| ComponentType<{
|
||||
template: TemplateEntityV1beta3;
|
||||
}>
|
||||
| undefined;
|
||||
TaskPageComponent?: ComponentType<{}>;
|
||||
};
|
||||
groups?: Array<{
|
||||
title?: React_2.ReactNode;
|
||||
filter: (entity: Entity) => boolean;
|
||||
}>;
|
||||
defaultPreviewTemplate?: string;
|
||||
headerOptions?: {
|
||||
pageTitleOverride?: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
};
|
||||
contextMenu?: {
|
||||
editor?: boolean;
|
||||
actions?: boolean;
|
||||
};
|
||||
| undefined;
|
||||
TaskPageComponent?: ComponentType<{}>;
|
||||
};
|
||||
groups?: Array<{
|
||||
title?: React_2.ReactNode;
|
||||
filter: (entity: Entity) => boolean;
|
||||
}>;
|
||||
defaultPreviewTemplate?: string;
|
||||
headerOptions?: {
|
||||
pageTitleOverride?: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
};
|
||||
contextMenu?: {
|
||||
editor?: boolean;
|
||||
actions?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface ScaffolderApi {
|
||||
// (undocumented)
|
||||
dryRun?(options: ScaffolderDryRunOptions): Promise<ScaffolderDryRunResponse>;
|
||||
// (undocumented)
|
||||
getIntegrationsList(
|
||||
options: ScaffolderGetIntegrationsListOptions,
|
||||
): Promise<ScaffolderGetIntegrationsListResponse>;
|
||||
// (undocumented)
|
||||
getTask(taskId: string): Promise<ScaffolderTask>;
|
||||
// (undocumented)
|
||||
getTemplateParameterSchema(
|
||||
templateRef: string,
|
||||
): Promise<TemplateParameterSchema>;
|
||||
listActions(): Promise<ListActionsResponse>;
|
||||
// (undocumented)
|
||||
listTasks?(options: { filterByOwnership: 'owned' | 'all' }): Promise<{
|
||||
tasks: ScaffolderTask[];
|
||||
}>;
|
||||
scaffold(
|
||||
options: ScaffolderScaffoldOptions,
|
||||
): Promise<ScaffolderScaffoldResponse>;
|
||||
// (undocumented)
|
||||
streamLogs(options: ScaffolderStreamLogsOptions): Observable<LogEvent>;
|
||||
// (undocumented)
|
||||
dryRun?(options: ScaffolderDryRunOptions): Promise<ScaffolderDryRunResponse>;
|
||||
// (undocumented)
|
||||
getIntegrationsList(
|
||||
options: ScaffolderGetIntegrationsListOptions,
|
||||
): Promise<ScaffolderGetIntegrationsListResponse>;
|
||||
// (undocumented)
|
||||
getTask(taskId: string): Promise<ScaffolderTask>;
|
||||
// (undocumented)
|
||||
getTemplateParameterSchema(
|
||||
templateRef: string,
|
||||
): Promise<TemplateParameterSchema>;
|
||||
listActions(): Promise<ListActionsResponse>;
|
||||
// (undocumented)
|
||||
listTasks?(options: { filterByOwnership: 'owned' | 'all' }): Promise<{
|
||||
tasks: ScaffolderTask[];
|
||||
}>;
|
||||
scaffold(
|
||||
options: ScaffolderScaffoldOptions,
|
||||
): Promise<ScaffolderScaffoldResponse>;
|
||||
// (undocumented)
|
||||
streamLogs(options: ScaffolderStreamLogsOptions): Observable<LogEvent>;
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -373,67 +373,67 @@ export const scaffolderApiRef: ApiRef<ScaffolderApi>;
|
||||
|
||||
// @public
|
||||
export class ScaffolderClient implements ScaffolderApi {
|
||||
constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
fetchApi: FetchApi;
|
||||
identityApi?: IdentityApi;
|
||||
scmIntegrationsApi: ScmIntegrationRegistry;
|
||||
useLongPollingLogs?: boolean;
|
||||
});
|
||||
// (undocumented)
|
||||
dryRun(options: ScaffolderDryRunOptions): Promise<ScaffolderDryRunResponse>;
|
||||
// (undocumented)
|
||||
getIntegrationsList(
|
||||
options: ScaffolderGetIntegrationsListOptions,
|
||||
): Promise<ScaffolderGetIntegrationsListResponse>;
|
||||
// (undocumented)
|
||||
getTask(taskId: string): Promise<ScaffolderTask>;
|
||||
// (undocumented)
|
||||
getTemplateParameterSchema(
|
||||
templateRef: string,
|
||||
): Promise<TemplateParameterSchema>;
|
||||
// (undocumented)
|
||||
listActions(): Promise<ListActionsResponse>;
|
||||
// (undocumented)
|
||||
listTasks(options: { filterByOwnership: 'owned' | 'all' }): Promise<{
|
||||
tasks: ScaffolderTask[];
|
||||
}>;
|
||||
scaffold(
|
||||
options: ScaffolderScaffoldOptions,
|
||||
): Promise<ScaffolderScaffoldResponse>;
|
||||
// (undocumented)
|
||||
streamLogs(options: ScaffolderStreamLogsOptions): Observable<LogEvent>;
|
||||
constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
fetchApi: FetchApi;
|
||||
identityApi?: IdentityApi;
|
||||
scmIntegrationsApi: ScmIntegrationRegistry;
|
||||
useLongPollingLogs?: boolean;
|
||||
});
|
||||
// (undocumented)
|
||||
dryRun(options: ScaffolderDryRunOptions): Promise<ScaffolderDryRunResponse>;
|
||||
// (undocumented)
|
||||
getIntegrationsList(
|
||||
options: ScaffolderGetIntegrationsListOptions,
|
||||
): Promise<ScaffolderGetIntegrationsListResponse>;
|
||||
// (undocumented)
|
||||
getTask(taskId: string): Promise<ScaffolderTask>;
|
||||
// (undocumented)
|
||||
getTemplateParameterSchema(
|
||||
templateRef: string,
|
||||
): Promise<TemplateParameterSchema>;
|
||||
// (undocumented)
|
||||
listActions(): Promise<ListActionsResponse>;
|
||||
// (undocumented)
|
||||
listTasks(options: { filterByOwnership: 'owned' | 'all' }): Promise<{
|
||||
tasks: ScaffolderTask[];
|
||||
}>;
|
||||
scaffold(
|
||||
options: ScaffolderScaffoldOptions,
|
||||
): Promise<ScaffolderScaffoldResponse>;
|
||||
// (undocumented)
|
||||
streamLogs(options: ScaffolderStreamLogsOptions): Observable<LogEvent>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ScaffolderDryRunOptions {
|
||||
// (undocumented)
|
||||
directoryContents: {
|
||||
path: string;
|
||||
base64Content: string;
|
||||
}[];
|
||||
// (undocumented)
|
||||
secrets?: Record<string, string>;
|
||||
// (undocumented)
|
||||
template: JsonValue;
|
||||
// (undocumented)
|
||||
values: JsonObject;
|
||||
// (undocumented)
|
||||
directoryContents: {
|
||||
path: string;
|
||||
base64Content: string;
|
||||
}[];
|
||||
// (undocumented)
|
||||
secrets?: Record<string, string>;
|
||||
// (undocumented)
|
||||
template: JsonValue;
|
||||
// (undocumented)
|
||||
values: JsonObject;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ScaffolderDryRunResponse {
|
||||
// (undocumented)
|
||||
directoryContents: Array<{
|
||||
path: string;
|
||||
base64Content: string;
|
||||
executable: boolean;
|
||||
}>;
|
||||
// (undocumented)
|
||||
log: Array<Pick<LogEvent, 'body'>>;
|
||||
// (undocumented)
|
||||
output: ScaffolderTaskOutput;
|
||||
// (undocumented)
|
||||
steps: TaskStep[];
|
||||
// (undocumented)
|
||||
directoryContents: Array<{
|
||||
path: string;
|
||||
base64Content: string;
|
||||
executable: boolean;
|
||||
}>;
|
||||
// (undocumented)
|
||||
log: Array<Pick<LogEvent, 'body'>>;
|
||||
// (undocumented)
|
||||
output: ScaffolderTaskOutput;
|
||||
// (undocumented)
|
||||
steps: TaskStep[];
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -441,18 +441,18 @@ export const ScaffolderFieldExtensions: React_2.ComponentType;
|
||||
|
||||
// @public
|
||||
export interface ScaffolderGetIntegrationsListOptions {
|
||||
// (undocumented)
|
||||
allowedHosts: string[];
|
||||
// (undocumented)
|
||||
allowedHosts: string[];
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface ScaffolderGetIntegrationsListResponse {
|
||||
// (undocumented)
|
||||
integrations: {
|
||||
type: string;
|
||||
title: string;
|
||||
host: string;
|
||||
}[];
|
||||
// (undocumented)
|
||||
integrations: {
|
||||
type: string;
|
||||
title: string;
|
||||
host: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -460,10 +460,10 @@ export const ScaffolderLayouts: React.ComponentType;
|
||||
|
||||
// @public (undocumented)
|
||||
export type ScaffolderOutputLink = {
|
||||
title?: string;
|
||||
icon?: string;
|
||||
url?: string;
|
||||
entityRef?: string;
|
||||
title?: string;
|
||||
icon?: string;
|
||||
url?: string;
|
||||
entityRef?: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -471,105 +471,105 @@ export const ScaffolderPage: (props: RouterProps) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export const scaffolderPlugin: BackstagePlugin<
|
||||
{
|
||||
root: RouteRef<undefined>;
|
||||
},
|
||||
{
|
||||
registerComponent: ExternalRouteRef<undefined, true>;
|
||||
viewTechDoc: ExternalRouteRef<
|
||||
{
|
||||
name: string;
|
||||
kind: string;
|
||||
namespace: string;
|
||||
},
|
||||
true
|
||||
{
|
||||
root: RouteRef<undefined>;
|
||||
},
|
||||
{
|
||||
registerComponent: ExternalRouteRef<undefined, true>;
|
||||
viewTechDoc: ExternalRouteRef<
|
||||
{
|
||||
name: string;
|
||||
kind: string;
|
||||
namespace: string;
|
||||
},
|
||||
true
|
||||
>;
|
||||
},
|
||||
{}
|
||||
>;
|
||||
},
|
||||
{}
|
||||
>;
|
||||
|
||||
// @public
|
||||
export interface ScaffolderScaffoldOptions {
|
||||
// (undocumented)
|
||||
secrets?: Record<string, string>;
|
||||
// (undocumented)
|
||||
templateRef: string;
|
||||
// (undocumented)
|
||||
values: Record<string, JsonValue>;
|
||||
// (undocumented)
|
||||
secrets?: Record<string, string>;
|
||||
// (undocumented)
|
||||
templateRef: string;
|
||||
// (undocumented)
|
||||
values: Record<string, JsonValue>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface ScaffolderScaffoldResponse {
|
||||
// (undocumented)
|
||||
taskId: string;
|
||||
// (undocumented)
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface ScaffolderStreamLogsOptions {
|
||||
// (undocumented)
|
||||
after?: number;
|
||||
// (undocumented)
|
||||
taskId: string;
|
||||
// (undocumented)
|
||||
after?: number;
|
||||
// (undocumented)
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type ScaffolderTask = {
|
||||
id: string;
|
||||
spec: TaskSpec;
|
||||
status: 'failed' | 'completed' | 'processing' | 'open' | 'cancelled';
|
||||
lastHeartbeatAt: string;
|
||||
createdAt: string;
|
||||
id: string;
|
||||
spec: TaskSpec;
|
||||
status: 'failed' | 'completed' | 'processing' | 'open' | 'cancelled';
|
||||
lastHeartbeatAt: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type ScaffolderTaskOutput = {
|
||||
links?: ScaffolderOutputLink[];
|
||||
links?: ScaffolderOutputLink[];
|
||||
} & {
|
||||
[key: string]: unknown;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ScaffolderTaskStatus =
|
||||
| 'open'
|
||||
| 'processing'
|
||||
| 'failed'
|
||||
| 'completed'
|
||||
| 'skipped';
|
||||
| 'open'
|
||||
| 'processing'
|
||||
| 'failed'
|
||||
| 'completed'
|
||||
| 'skipped';
|
||||
|
||||
// @public
|
||||
export interface ScaffolderUseTemplateSecrets {
|
||||
// (undocumented)
|
||||
setSecrets: (input: Record<string, string>) => void;
|
||||
// (undocumented)
|
||||
setSecrets: (input: Record<string, string>) => void;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const selectedTemplateRouteRef: SubRouteRef<
|
||||
PathParams<'/templates/:namespace/:templateName'>
|
||||
>;
|
||||
PathParams<'/templates/:namespace/:templateName'>
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const TaskPage: ({ loadingText }: TaskPageProps) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type TaskPageProps = {
|
||||
loadingText?: string;
|
||||
loadingText?: string;
|
||||
};
|
||||
|
||||
// @alpha (undocumented)
|
||||
export type TemplateGroupFilter = {
|
||||
title?: React_2.ReactNode;
|
||||
filter: (entity: Entity) => boolean;
|
||||
title?: React_2.ReactNode;
|
||||
filter: (entity: Entity) => boolean;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type TemplateParameterSchema = {
|
||||
title: string;
|
||||
description?: string;
|
||||
steps: Array<{
|
||||
title: string;
|
||||
description?: string;
|
||||
schema: JsonObject;
|
||||
}>;
|
||||
steps: Array<{
|
||||
title: string;
|
||||
description?: string;
|
||||
schema: JsonObject;
|
||||
}>;
|
||||
};
|
||||
|
||||
// @public
|
||||
|
||||
@@ -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 { repoPickerValidation } from './components';
|
||||
import { RepoUrlPicker } from './components/fields/RepoUrlPicker/RepoUrlPicker';
|
||||
import {
|
||||
EntityNamePicker,
|
||||
EntityNamePickerSchema,
|
||||
} from './components/fields/EntityNamePicker/EntityNamePicker';
|
||||
import {
|
||||
OwnerPicker,
|
||||
OwnerPickerSchema,
|
||||
} from './components/fields/OwnerPicker/OwnerPicker';
|
||||
import { repoPickerValidation } from './components/fields/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