diff --git a/.changeset/brown-hotels-study.md b/.changeset/brown-hotels-study.md new file mode 100644 index 0000000000..2b9e6a86b8 --- /dev/null +++ b/.changeset/brown-hotels-study.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Move logic for constructing the template form to the backend, using a new `./parameter-schema` endpoint that returns the form schema for a given template. diff --git a/.changeset/fluffy-elephants-suffer.md b/.changeset/fluffy-elephants-suffer.md new file mode 100644 index 0000000000..cd118829ed --- /dev/null +++ b/.changeset/fluffy-elephants-suffer.md @@ -0,0 +1,7 @@ +--- +'@backstage/catalog-model': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Add version `backstage.io/v1beta2` schema for Template entities. diff --git a/.changeset/slimy-singers-return.md b/.changeset/slimy-singers-return.md new file mode 100644 index 0000000000..c5fc60aaa2 --- /dev/null +++ b/.changeset/slimy-singers-return.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Fixed file path resolution for templates with a file location diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts new file mode 100644 index 0000000000..60f3de9e59 --- /dev/null +++ b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts @@ -0,0 +1,124 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { + TemplateEntityV1beta2, + templateEntityV1beta2Validator as validator, +} from './TemplateEntityV1beta2'; + +describe('templateEntityV1beta2Validator', () => { + let entity: TemplateEntityV1beta2; + + beforeEach(() => { + entity = { + apiVersion: 'backstage.io/v1beta2', + kind: 'Template', + metadata: { + name: 'test', + }, + spec: { + type: 'website', + parameters: { + required: ['storePath', 'owner'], + properties: { + owner: { + type: 'string', + title: 'Owner', + description: 'Who is going to own this component', + }, + storePath: { + type: 'string', + title: 'Store path', + description: 'GitHub store path in org/repo format', + }, + }, + }, + steps: [ + { + id: 'fetch', + name: 'Fetch', + action: 'fetch:plan', + parameters: { + url: './template', + }, + }, + ], + output: { + fetchUrl: '{{ steps.fetch.output.targetUrl }}', + }, + }, + }; + }); + + it('happy path: accepts valid data', async () => { + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('ignores unknown apiVersion', async () => { + (entity as any).apiVersion = 'backstage.io/v1beta0'; + await expect(validator.check(entity)).resolves.toBe(false); + }); + + it('ignores unknown kind', async () => { + (entity as any).kind = 'Wizard'; + await expect(validator.check(entity)).resolves.toBe(false); + }); + + it('rejects missing type', async () => { + delete (entity as any).spec.type; + await expect(validator.check(entity)).rejects.toThrow(/type/); + }); + + it('accepts any other type', async () => { + (entity as any).spec.type = 'hallo'; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('accepts missing parameters', async () => { + delete (entity as any).spec.parameters; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('accepts missing outputs', async () => { + delete (entity as any).spec.outputs; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('rejects empty type', async () => { + (entity as any).spec.type = ''; + await expect(validator.check(entity)).rejects.toThrow(/type/); + }); + + it('rejects missing steps', async () => { + delete (entity as any).spec.steps; + await expect(validator.check(entity)).rejects.toThrow(/steps/); + }); + + it('accepts step with missing id', async () => { + delete (entity as any).spec.steps[0].id; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('accepts step with missing name', async () => { + delete (entity as any).spec.steps[0].name; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('rejects step with missing action', async () => { + delete (entity as any).spec.steps[0].action; + await expect(validator.check(entity)).rejects.toThrow(/action/); + }); +}); diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts new file mode 100644 index 0000000000..c78e7f85d5 --- /dev/null +++ b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 type { Entity, EntityMeta } from '../entity/Entity'; +import schema from '../schema/kinds/Template.v1beta2.schema.json'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; +import { ajvCompiledJsonSchemaValidator } from './util'; +import { JsonObject } from '@backstage/config'; + +const API_VERSION = ['backstage.io/v1beta2'] as const; +const KIND = 'Template' as const; + +export interface TemplateEntityV1beta2 extends Entity { + apiVersion: typeof API_VERSION[number]; + kind: typeof KIND; + metadata: EntityMeta & { + title?: string; + }; + spec: { + type: string; + parameters?: JsonObject | JsonObject[]; + steps: Array<{ + id?: string; + name?: string; + action: string; + parameters?: JsonObject; + }>; + output?: { [name: string]: string }; + }; +} + +export const templateEntityV1beta2Validator = ajvCompiledJsonSchemaValidator( + KIND, + API_VERSION, + schema, + [commonSchema, entityMetaSchema, entitySchema], +); diff --git a/packages/catalog-model/src/kinds/index.ts b/packages/catalog-model/src/kinds/index.ts index bc157c79df..4ae2db483a 100644 --- a/packages/catalog-model/src/kinds/index.ts +++ b/packages/catalog-model/src/kinds/index.ts @@ -57,6 +57,8 @@ export type { TemplateEntityV1alpha1 as TemplateEntity, TemplateEntityV1alpha1, } from './TemplateEntityV1alpha1'; +export { templateEntityV1beta2Validator } from './TemplateEntityV1beta2'; +export type { TemplateEntityV1beta2 } from './TemplateEntityV1beta2'; export { userEntityV1alpha1Validator } from './UserEntityV1alpha1'; export type { UserEntityV1alpha1 as UserEntity, diff --git a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json new file mode 100644 index 0000000000..bd9999f8bd --- /dev/null +++ b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json @@ -0,0 +1,147 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "TemplateV1beta1", + "description": "A Template describes a scaffolding task for use with the Scaffolder. It describes the required parameters as well as a series of steps that will be taken to execute the scaffolding task.", + "examples": [ + { + "apiVersion": "backstage.io/v1beta1", + "kind": "Template", + "metadata": { + "name": "react-ssr-template", + "title": "React SSR Template", + "description": "Next.js application skeleton for creating isomorphic web applications.", + "tags": ["recommended", "react"] + }, + "spec": { + "owner": "artist-relations-team", + "type": "website", + "parameters": { + "required": ["name", "description"], + "properties": { + "name": { + "title": "Name", + "type": "string", + "description": "Unique name of the component" + }, + "description": { + "title": "Description", + "type": "string", + "description": "Description of the component" + } + } + }, + "steps": [ + { + "id": "fetch", + "name": "Fetch", + "action": "fetch:plain", + "parameters": { + "url": "./template" + } + }, + { + "id": "publish", + "name": "Publish to GitHub", + "action": "publish:github", + "parameters": { + "repoUrl": "{{ parameters.repoUrl }}" + } + } + ], + "output": { + "catalogInfoUrl": "{{ steps.publish.output.catalogInfoUrl }}" + } + } + } + ], + "allOf": [ + { + "$ref": "Entity" + }, + { + "type": "object", + "required": ["spec"], + "properties": { + "apiVersion": { + "enum": ["backstage.io/v1beta2"] + }, + "kind": { + "enum": ["Template"] + }, + "metadata": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "The nice display name for the template.", + "examples": ["React SSR Template"], + "minLength": 1 + } + } + }, + "spec": { + "type": "object", + "required": ["type", "steps"], + "properties": { + "type": { + "type": "string", + "description": "The type of component. This field is optional but recommended. The software catalog accepts any type value, but an organization should take great care to establish a proper taxonomy for these. Tools including Backstage itself may read this field and behave differently depending on its value. For example, a website type component may present tooling in the Backstage interface that is specific to just websites.", + "examples": ["service", "website", "library"], + "minLength": 1 + }, + "parameters": { + "oneOf": [ + { + "type": "object", + "description": "The JSONSchema describing the inputs for the template." + }, + { + "type": "array", + "description": "A list of separate forms to collect parameters.", + "items": { + "type": "object", + "description": "The JSONSchema describing the inputs for the template." + } + } + ] + }, + "steps": { + "type": "array", + "description": "A list of steps to execute.", + "items": { + "type": "object", + "description": "A description of the step to execute.", + "required": ["action"], + "properties": { + "id": { + "type": "string", + "description": "The ID of the step, which can be used to refer to its outputs." + }, + "name": { + "type": "string", + "description": "The name of the step, which will be displayed in the UI during the scaffolding process." + }, + "action": { + "type": "string", + "description": "The name of the action to execute." + }, + "parameters": { + "type": "object", + "description": "A templated object describing the inputs to the action." + } + } + } + }, + "output": { + "type": "object", + "description": "A templated object describing the outputs of the scaffolding task.", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + ] +} diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts index ef94c54010..26a8538707 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts @@ -45,6 +45,7 @@ import { SystemEntity, systemEntityV1alpha1Validator, templateEntityV1alpha1Validator, + templateEntityV1beta2Validator, UserEntity, userEntityV1alpha1Validator, } from '@backstage/catalog-model'; @@ -59,6 +60,7 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { groupEntityV1alpha1Validator, locationEntityV1alpha1Validator, templateEntityV1alpha1Validator, + templateEntityV1beta2Validator, userEntityV1alpha1Validator, systemEntityV1alpha1Validator, domainEntityV1alpha1Validator, diff --git a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts index cea0e85148..46a1433be2 100644 --- a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts +++ b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { + TemplateEntityV1alpha1, + TemplateEntityV1beta2, +} from '@backstage/catalog-model'; import { CatalogApi } from '@backstage/catalog-client'; import { ConflictError, NotFoundError } from '@backstage/backend-common'; @@ -32,7 +35,7 @@ export class CatalogEntityClient { async findTemplate( templateName: string, options?: { token?: string }, - ): Promise { + ): Promise { const { items: templates } = (await this.catalogClient.getEntities( { filter: { @@ -41,7 +44,7 @@ export class CatalogEntityClient { }, }, options, - )) as { items: TemplateEntityV1alpha1[] }; + )) as { items: (TemplateEntityV1alpha1 | TemplateEntityV1beta2)[] }; if (templates.length !== 1) { if (templates.length > 1) { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index e717fcae1b..9d0c0863aa 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -17,7 +17,7 @@ import { PassThrough } from 'stream'; import { Logger } from 'winston'; import * as winston from 'winston'; -import { JsonValue } from '@backstage/config'; +import { JsonValue, JsonObject } from '@backstage/config'; import { TaskBroker, Task } from './types'; import fs from 'fs-extra'; import path from 'path'; @@ -57,10 +57,11 @@ export class TaskWorker { ); const templateCtx: { + parameters: JsonObject; steps: { [stepName: string]: { output: { [outputName: string]: JsonValue } }; }; - } = { steps: {} }; + } = { parameters: task.spec.values, steps: {} }; for (const step of task.spec.steps) { const metadata = { stepId: step.id }; @@ -94,22 +95,20 @@ export class TaskWorker { throw new Error(`Action '${step.action}' does not exist`); } - const parameters: { [name: string]: JsonValue } = {}; - for (const [name, maybeTemplateStr] of Object.entries( - step.parameters ?? {}, - )) { - if (typeof maybeTemplateStr === 'string') { - const value = handlebars.compile(maybeTemplateStr, { - noEscape: true, - strict: true, - data: false, - preventIndent: true, - })(templateCtx); - parameters[name] = value; - } else { - parameters[name] = maybeTemplateStr; - } - } + const parameters = JSON.parse( + JSON.stringify(step.parameters), + (_key, value) => { + if (typeof value === 'string') { + return handlebars.compile(value, { + noEscape: true, + strict: true, + data: false, + preventIndent: true, + })(templateCtx); + } + return value; + }, + ); const stepOutputs: { [name: string]: JsonValue } = {}; @@ -138,16 +137,19 @@ export class TaskWorker { } } - const output = Object.fromEntries( - Object.entries(task.spec.output).map(([name, templateStr]) => { - const value = handlebars.compile(templateStr, { - noEscape: true, - strict: true, - data: false, - preventIndent: true, - })(templateCtx); - return [name, value]; - }), + const output = JSON.parse( + JSON.stringify(task.spec.output), + (_key, value) => { + if (typeof value === 'string') { + return handlebars.compile(value, { + noEscape: true, + strict: true, + data: false, + preventIndent: true, + })(templateCtx); + } + return value; + }, ); await task.complete('completed', { output }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts index 4139948513..7f76ac33d0 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { resolve as resolvePath } from 'path'; +import { resolve as resolvePath, dirname } from 'path'; import { JsonValue } from '@backstage/config'; import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { Logger } from 'winston'; @@ -39,7 +39,7 @@ export function templateEntityToSpec( let url: string; if (protocol === 'file') { - const path = resolvePath(location, template.spec.path || '.'); + const path = resolvePath(dirname(location), template.spec.path || '.'); url = `file://${path}`; } else { @@ -86,6 +86,7 @@ export function templateEntityToSpec( }); return { + values: {}, steps, output: { remoteUrl: '{{ steps.publish.output.remoteUrl }}', diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index ae18a59e41..27a1b62d04 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -43,11 +43,12 @@ export type DbTaskEventRow = { }; export type TaskSpec = { + values: JsonObject; steps: Array<{ id: string; name: string; action: string; - parameters?: { [name: string]: JsonValue }; + parameters?: JsonObject; }>; output: { [name: string]: string }; }; diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 3d10bbd101..65b9039e27 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -45,10 +45,16 @@ import { import { registerLegacyActions } from '../scaffolder/stages/legacy'; import { getWorkingDirectory } from './helpers'; import { + InputError, NotFoundError, PluginDatabaseManager, } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; +import { + TemplateEntityV1alpha1, + TemplateEntityV1beta2, + Entity, +} from '@backstage/catalog-model'; export interface RouterOptions { preparers: PreparerBuilder; @@ -62,6 +68,21 @@ export interface RouterOptions { catalogClient: CatalogApi; } +function isAlpha1Template( + entity: TemplateEntityV1alpha1 | TemplateEntityV1beta2, +): entity is TemplateEntityV1alpha1 { + return ( + entity.apiVersion === 'backstage.io/v1alpha1' || + entity.apiVersion === 'backstage.io/v1beta1' + ); +} + +function isBeta2Template( + entity: TemplateEntityV1alpha1 | TemplateEntityV1beta2, +): entity is TemplateEntityV1beta2 { + return entity.apiVersion === 'backstage.io/v1beta2'; +} + export async function createRouter( options: RouterOptions, ): Promise { @@ -143,6 +164,11 @@ export async function createRouter( const template = await entityClient.findTemplate(templateName, { token: getBearerToken(req.headers.authorization), }); + if (!isAlpha1Template(template)) { + throw new InputError( + `This endpoint does not support templates with version ${template.apiVersion}`, + ); + } const validationResult: ValidatorResult = validate( values, @@ -231,6 +257,77 @@ export async function createRouter( // NOTE: The v2 API is unstable router + .get( + '/v2/templates/:namespace/:kind/:name/parameter-schema', + async (req, res) => { + const { namespace, kind, name } = req.params; + + if (namespace !== 'default') { + throw new InputError( + `Invalid namespace, only 'default' namespace is supported`, + ); + } + if (kind.toLowerCase() !== 'template') { + throw new InputError( + `Invalid kind, only 'Template' kind is supported`, + ); + } + + const template = await entityClient.findTemplate(name); + if (isBeta2Template(template)) { + const parameters = [template.spec.parameters ?? []].flat(); + res.json({ + title: template.metadata.title ?? template.metadata.name, + steps: parameters.map(schema => ({ + title: schema.title ?? 'Fill in template parameters', + schema, + })), + }); + } else if (isAlpha1Template(template)) { + res.json({ + title: template.metadata.title ?? template.metadata.name, + steps: [ + { + title: 'Fill in template parameters', + schema: template.spec.schema, + }, + { + title: 'Choose owner and repo', + schema: { + type: 'object', + required: ['storePath', 'owner'], + properties: { + owner: { + type: 'string', + title: 'Owner', + description: 'Who is going to own this component', + }, + storePath: { + type: 'string', + title: 'Store path', + description: + 'A full URL to the repository that should be created. e.g https://github.com/backstage/new-repo', + }, + access: { + type: 'string', + title: 'Access', + description: + 'Who should have access, in org/team or user format', + }, + }, + }, + }, + ], + }); + } else { + throw new InputError( + `Unsupported apiVersion field in schema entity, ${ + (template as Entity).apiVersion + }`, + ); + } + }, + ) .post('/v2/tasks', async (req, res) => { const templateName: string = req.body.templateName; const values: TemplaterValues = { @@ -241,16 +338,43 @@ export async function createRouter( }; const template = await entityClient.findTemplate(templateName); - const validationResult: ValidatorResult = validate( - values, - template.spec.schema, - ); + let taskSpec; + if (isAlpha1Template(template)) { + const result = validate(values, template.spec.schema); - if (!validationResult.valid) { - res.status(400).json({ errors: validationResult.errors }); - return; + if (!result.valid) { + res.status(400).json({ errors: result.errors }); + return; + } + + taskSpec = templateEntityToSpec(template, values); + } else if (isBeta2Template(template)) { + for (const parameters of [template.spec.parameters ?? []].flat()) { + const result = validate(values, parameters); + + if (!result.valid) { + res.status(400).json({ errors: result.errors }); + return; + } + } + + taskSpec = { + values, + steps: template.spec.steps.map((step, index) => ({ + ...step, + id: step.id ?? `step-${index + 1}`, + name: step.name ?? step.action, + })), + output: template.spec.output ?? {}, + }; + } else { + throw new InputError( + `Unsupported apiVersion field in schema entity, ${ + (template as Entity).apiVersion + }`, + ); } - const taskSpec = templateEntityToSpec(template, values); + const result = await taskBroker.dispatch(taskSpec); res.status(201).json({ id: result.taskId }); diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 20fca5b3f2..ac165e021a 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { EntityName } from '@backstage/catalog-model'; +import { JsonObject } from '@backstage/config'; import { createApiRef, DiscoveryApi, @@ -28,6 +30,14 @@ export const scaffolderApiRef = createApiRef({ description: 'Used to make requests towards the scaffolder backend', }); +type TemplateParameterSchema = { + title: string; + steps: Array<{ + title: string; + schema: JsonObject; + }>; +}; + export type LogEvent = { type: 'log' | 'completion'; body: { @@ -41,6 +51,10 @@ export type LogEvent = { }; export interface ScaffolderApi { + getTemplateParameterSchema( + templateName: EntityName, + ): Promise; + /** * Executes the scaffolding of a component, given a template and its * parameter values. @@ -72,6 +86,34 @@ export class ScaffolderClient implements ScaffolderApi { this.identityApi = options.identityApi; } + async getTemplateParameterSchema( + templateName: EntityName, + ): Promise { + const { namespace, kind, name } = templateName; + + const token = await this.identityApi.getIdToken(); + const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); + const templatePath = [namespace, kind, name] + .map(s => encodeURIComponent(s)) + .join('/'); + const url = `${baseUrl}/v2/templates/${templatePath}/parameter-schema`; + + const response = await fetch(url, { + headers: { + ...(token && { Authorization: `Bearer ${token}` }), + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch template parameter schema, ${await response.text()}`, + ); + } + + const schema: TemplateParameterSchema = await response.json(); + return schema; + } + /** * Executes the scaffolding of a component, given a template and its * parameter values. diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx index 27c5b4f18a..eb51e86d59 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx +++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { JSONSchema } from '@backstage/catalog-model'; +import { JsonObject } from '@backstage/config'; import { Content, StructuredMetadataTable } from '@backstage/core'; import { Box, @@ -28,16 +28,17 @@ import { import { FormProps, IChangeEvent, withTheme } from '@rjsf/core'; import { Theme as MuiTheme } from '@rjsf/material-ui'; import React, { useState } from 'react'; +import { transformSchemaToProps } from './schema'; const Form = withTheme(MuiTheme); type Step = { - schema: JSONSchema; - label: string; + schema: JsonObject; + title: string; } & Partial, 'schema'>>; type Props = { /** - * Steps for the form, each contains label and form schema + * Steps for the form, each contains title and form schema */ steps: Step[]; formData: Record; @@ -66,20 +67,21 @@ export const MultistepJsonForm = ({ return ( <> - {steps.map(({ label, schema, ...formProps }) => ( - - {label} - + {steps.map(({ title, schema, ...formProps }) => ( + + + {title} + +
['schema']} onSubmit={e => { if (e.errors.length === 0) handleNext(); }} {...formProps} + {...transformSchemaToProps(schema)} >