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:
bnechyporenko
2022-11-17 10:33:53 +01:00
36 changed files with 1188 additions and 431 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': minor
---
Implement Custom Field Explorer to view and play around with available installed custom field extensions
+1
View File
@@ -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):
![Custom Field Explorer](../../assets/software-templates/custom-field-explorer.png)
In order to make your new custom field extension available in the explorer you will have to define a
JSON schema that describes the input/output types on your field like in the following example:
```tsx
//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx
export const MyCustomExtensionWithOptionsSchema = {
uiOptions: {
type: 'object',
properties: {
focused: {
description: 'Whether to focus this field',
type: 'boolean',
},
},
},
returnValue: { type: 'string' },
};
export const MyCustomExtensionWithOptions = ({
onChange,
rawErrors,
required,
formData,
}: 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.
[![Introduction to Backstage: Developer Portals Made Easy (LFS142x): Enroll!](assets/22-11-15/Introduction-to-Backstage-Developer-Portals-Made-Easy-2-768x432.png)](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). Hes been working around Backstage since it was open sourced by Spotify and co-hosts community initiatives like the Backstage Users Unconference.
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

+317 -317
View File
@@ -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
+3 -1
View File
@@ -74,7 +74,9 @@
"react-use": "^17.2.4",
"use-immer": "^0.7.0",
"yaml": "^2.0.0",
"zen-observable": "^0.8.15"
"zen-observable": "^0.8.15",
"zod": "^3.11.6",
"zod-to-json-schema": "^3.18.1"
},
"peerDependencies": {
"@types/react": "^16.13.1 || ^17.0.0",
@@ -0,0 +1,201 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { StreamLanguage } from '@codemirror/language';
import { yaml as yamlSupport } from '@codemirror/legacy-modes/mode/yaml';
import {
Button,
Card,
CardContent,
CardHeader,
FormControl,
IconButton,
InputLabel,
makeStyles,
MenuItem,
Select,
} from '@material-ui/core';
import CloseIcon from '@material-ui/icons/Close';
import { withTheme } from '@rjsf/core';
import { Theme as MuiTheme } from '@rjsf/material-ui';
import CodeMirror from '@uiw/react-codemirror';
import React, { useCallback, useMemo, useState } from 'react';
import yaml from 'yaml';
import { FieldExtensionOptions } from '../../extensions';
import * as fieldOverrides from '../MultistepJsonForm/FieldOverrides';
import { TemplateEditorForm } from './TemplateEditorForm';
const Form = withTheme(MuiTheme);
const useStyles = makeStyles(theme => ({
root: {
gridArea: 'pageContent',
display: 'grid',
gridTemplateAreas: `
"controls controls"
"fieldForm preview"
`,
gridTemplateRows: 'auto 1fr',
gridTemplateColumns: '1fr 1fr',
},
controls: {
gridArea: 'controls',
display: 'flex',
flexFlow: 'row nowrap',
alignItems: 'center',
margin: theme.spacing(1),
},
fieldForm: {
gridArea: 'fieldForm',
},
preview: {
gridArea: 'preview',
},
}));
export const CustomFieldExplorer = ({
customFieldExtensions = [],
onClose,
}: {
customFieldExtensions?: FieldExtensionOptions<any, any>[];
onClose?: () => void;
}) => {
const classes = useStyles();
const fieldOptions = customFieldExtensions.filter(field => !!field.schema);
const [selectedField, setSelectedField] = useState(fieldOptions[0]);
const [fieldFormState, setFieldFormState] = useState({});
const [formState, setFormState] = useState({});
const [refreshKey, setRefreshKey] = useState(Date.now());
const sampleFieldTemplate = useMemo(
() =>
yaml.stringify({
parameters: [
{
title: `${selectedField.name} Example`,
properties: {
[selectedField.name]: {
type: selectedField.schema?.returnValue?.type,
'ui:field': selectedField.name,
'ui:options': fieldFormState,
},
},
},
],
}),
[fieldFormState, selectedField],
);
const fieldComponents = useMemo(() => {
return Object.fromEntries(
customFieldExtensions.map(({ name, component }) => [name, component]),
);
}, [customFieldExtensions]);
const handleSelectionChange = useCallback(
selection => {
setSelectedField(selection);
setFieldFormState({});
setFormState({});
},
[setFieldFormState, setFormState, setSelectedField],
);
const handleFieldConfigChange = useCallback(
state => {
setFieldFormState(state);
setFormState({});
// Force TemplateEditorForm to re-render since some fields
// may not be responsive to ui:option changes
setRefreshKey(Date.now());
},
[setFieldFormState, setRefreshKey],
);
return (
<main className={classes.root}>
<div className={classes.controls}>
<FormControl variant="outlined" size="small" fullWidth>
<InputLabel id="select-field-label">
Choose Custom Field Extension
</InputLabel>
<Select
value={selectedField}
label="Choose Custom Field Extension"
labelId="select-field-label"
onChange={e => handleSelectionChange(e.target.value)}
>
{fieldOptions.map((option, idx) => (
<MenuItem key={idx} value={option as any}>
{option.name}
</MenuItem>
))}
</Select>
</FormControl>
<IconButton size="medium" onClick={onClose}>
<CloseIcon />
</IconButton>
</div>
<div className={classes.fieldForm}>
<Card>
<CardHeader title="Field Options" />
<CardContent>
<Form
showErrorList={false}
fields={{ ...fieldOverrides, ...fieldComponents }}
noHtml5Validate
formData={fieldFormState}
formContext={{ fieldFormState }}
onSubmit={e => handleFieldConfigChange(e.formData)}
schema={selectedField.schema?.uiOptions || {}}
>
<Button
variant="contained"
color="primary"
type="submit"
disabled={!selectedField.schema?.uiOptions}
>
Apply
</Button>
</Form>
</CardContent>
</Card>
</div>
<div className={classes.preview}>
<Card>
<CardHeader title="Example Template Spec" />
<CardContent>
<CodeMirror
readOnly
theme="dark"
height="100%"
extensions={[StreamLanguage.define(yamlSupport)]}
value={sampleFieldTemplate}
/>
</CardContent>
</Card>
<TemplateEditorForm
key={refreshKey}
content={sampleFieldTemplate}
contentIsSpec
fieldExtensions={customFieldExtensions}
data={formState}
onUpdate={setFormState}
setErrorText={() => null}
/>
</div>
</main>
);
};
@@ -44,7 +44,7 @@ const useStyles = makeStyles(theme => ({
interface EditorIntroProps {
style?: JSX.IntrinsicElements['div']['style'];
onSelect?: (option: 'local' | 'form') => void;
onSelect?: (option: 'local' | 'form' | 'field-explorer') => void;
}
export function TemplateEditorIntro(props: EditorIntroProps) {
@@ -104,6 +104,22 @@ export function TemplateEditorIntro(props: EditorIntroProps) {
</Card>
);
const cardFieldExplorer = (
<Card className={classes.card} elevation={4}>
<CardActionArea onClick={() => props.onSelect?.('field-explorer')}>
<CardContent>
<Typography variant="h5" gutterBottom>
Custom Field Explorer
</Typography>
<Typography variant="body1">
View and play around with available installed custom field
extensions.
</Typography>
</CardContent>
</CardActionArea>
</Card>
);
return (
<div style={props.style}>
<Typography variant="h6" className={classes.introText}>
@@ -121,6 +137,7 @@ export function TemplateEditorIntro(props: EditorIntroProps) {
{supportsLoad && cardLoadLocal}
{cardFormEditor}
{!supportsLoad && cardLoadLocal}
{cardFieldExplorer}
</div>
</div>
);
@@ -19,6 +19,7 @@ import {
TemplateDirectoryAccess,
WebFileSystemAccess,
} from '../../lib/filesystem';
import { CustomFieldExplorer } from './CustomFieldExplorer';
import { TemplateEditorIntro } from './TemplateEditorIntro';
import { TemplateEditor } from './TemplateEditor';
import { TemplateFormPreviewer } from './TemplateFormPreviewer';
@@ -32,6 +33,9 @@ type Selection =
}
| {
type: 'form';
}
| {
type: 'field-explorer';
};
interface TemplateEditorPageProps {
@@ -62,6 +66,13 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) {
layouts={props.layouts}
/>
);
} else if (selection?.type === 'field-explorer') {
content = (
<CustomFieldExplorer
customFieldExtensions={props.customFieldExtensions}
onClose={() => setSelection(undefined)}
/>
);
} else {
content = (
<Content>
@@ -73,6 +84,8 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) {
.catch(() => {});
} else if (option === 'form') {
setSelection({ type: 'form' });
} else if (option === 'field-explorer') {
setSelection({ type: 'field-explorer' });
}
}}
/>
@@ -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,
};
}
+30 -6
View File
@@ -13,40 +13,64 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EntityPicker } from '../components/fields/EntityPicker/EntityPicker';
import { EntityNamePicker } from '../components/fields/EntityNamePicker/EntityNamePicker';
import {
EntityPicker,
EntityPickerSchema,
} from '../components/fields/EntityPicker/EntityPicker';
import {
EntityNamePicker,
EntityNamePickerSchema,
} from '../components/fields/EntityNamePicker/EntityNamePicker';
import { entityNamePickerValidation } from '../components/fields/EntityNamePicker/validation';
import { EntityTagsPicker } from '../components/fields/EntityTagsPicker/EntityTagsPicker';
import { OwnerPicker } from '../components/fields/OwnerPicker/OwnerPicker';
import { RepoUrlPicker } from '../components/fields/RepoUrlPicker/RepoUrlPicker';
import {
EntityTagsPicker,
EntityTagsPickerSchema,
} from '../components/fields/EntityTagsPicker/EntityTagsPicker';
import {
OwnerPicker,
OwnerPickerSchema,
} from '../components/fields/OwnerPicker/OwnerPicker';
import {
RepoUrlPicker,
RepoUrlPickerSchema,
} from '../components/fields/RepoUrlPicker/RepoUrlPicker';
import { repoPickerValidation } from '../components/fields/RepoUrlPicker/validation';
import { OwnedEntityPicker } from '../components/fields/OwnedEntityPicker/OwnedEntityPicker';
import {
OwnedEntityPicker,
OwnedEntityPickerSchema,
} from '../components/fields/OwnedEntityPicker/OwnedEntityPicker';
export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [
{
component: EntityPicker,
name: 'EntityPicker',
schema: EntityPickerSchema,
},
{
component: EntityNamePicker,
name: 'EntityNamePicker',
validation: entityNamePickerValidation,
schema: EntityNamePickerSchema,
},
{
component: EntityTagsPicker,
name: 'EntityTagsPicker',
schema: EntityTagsPickerSchema,
},
{
component: RepoUrlPicker,
name: 'RepoUrlPicker',
validation: repoPickerValidation,
schema: RepoUrlPickerSchema,
},
{
component: OwnerPicker,
name: 'OwnerPicker',
schema: OwnerPickerSchema,
},
{
component: OwnedEntityPicker,
name: 'OwnedEntityPicker',
schema: OwnedEntityPickerSchema,
},
];
@@ -16,6 +16,7 @@
import React from 'react';
import {
CustomFieldExtensionSchema,
CustomFieldValidator,
FieldExtensionOptions,
FieldExtensionComponentProps,
@@ -103,6 +104,7 @@ attachComponentData(
);
export type {
CustomFieldExtensionSchema,
CustomFieldValidator,
FieldExtensionOptions,
FieldExtensionComponentProps,
+14 -2
View File
@@ -15,14 +15,14 @@
*/
import { ApiHolder } from '@backstage/core-plugin-api';
import { FieldValidation, FieldProps } from '@rjsf/core';
import { PropsWithChildren } from 'react';
import {
UIOptionsType,
FieldProps as FieldPropsV5,
UiSchema as UiSchemaV5,
FieldValidation as FieldValidationV5,
} from '@rjsf/utils';
import { PropsWithChildren } from 'react';
import { JSONSchema7 } from 'json-schema';
/**
* Field validation type for Custom Field Extensions.
@@ -35,6 +35,16 @@ export type CustomFieldValidator<TFieldReturnValue> = (
context: { apiHolder: ApiHolder },
) => void | Promise<void>;
/**
* Type for the Custom Field Extension schema.
*
* @public
*/
export type CustomFieldExtensionSchema = {
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;
};
+1
View File
@@ -43,6 +43,7 @@ export {
ScaffolderFieldExtensions,
} from './extensions';
export type {
CustomFieldExtensionSchema,
CustomFieldValidator,
FieldExtensionOptions,
FieldExtensionComponentProps,
+31 -7
View File
@@ -16,12 +16,24 @@
import { scmIntegrationsApiRef } from '@backstage/integration-react';
import { scaffolderApiRef, ScaffolderClient } from './api';
import { EntityPicker } from './components/fields/EntityPicker/EntityPicker';
import {
EntityPicker,
EntityPickerSchema,
} from './components/fields/EntityPicker/EntityPicker';
import { entityNamePickerValidation } from './components/fields/EntityNamePicker';
import { EntityNamePicker } from './components/fields/EntityNamePicker/EntityNamePicker';
import { OwnerPicker } from './components/fields/OwnerPicker/OwnerPicker';
import { 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,
}),
);
+2
View File
@@ -6646,6 +6646,8 @@ __metadata:
use-immer: ^0.7.0
yaml: ^2.0.0
zen-observable: ^0.8.15
zod: ^3.11.6
zod-to-json-schema: ^3.18.1
peerDependencies:
"@types/react": ^16.13.1 || ^17.0.0
react: ^16.13.1 || ^17.0.0