From ccaa5d7d43bef0ab88ca9623b2e8decb4a216f85 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 19 Feb 2021 16:03:39 +0100 Subject: [PATCH] scaffolder: implement uiSchema support in template parameter schema Co-authored-by: Johan Haals --- .../MultistepJsonForm/MultistepJsonForm.tsx | 61 ++++++++-------- .../MultistepJsonForm/schema.test.ts | 71 +++++++++++++++++++ .../components/MultistepJsonForm/schema.ts | 71 +++++++++++++++++++ 3 files changed, 172 insertions(+), 31 deletions(-) create mode 100644 plugins/scaffolder/src/components/MultistepJsonForm/schema.test.ts create mode 100644 plugins/scaffolder/src/components/MultistepJsonForm/schema.ts diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx index 17acc1e8dd..71084a1eda 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,10 +28,11 @@ 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; + schema: JsonObject; title: string; } & Partial, 'schema'>>; @@ -66,35 +67,33 @@ export const MultistepJsonForm = ({ return ( <> - {steps.map( - ({ title, schema: { title: _, ...schema }, ...formProps }) => ( - - - {title} - - -
['schema']} - onSubmit={e => { - if (e.errors.length === 0) handleNext(); - }} - {...formProps} - > - - -
-
-
- ), - )} + {steps.map(({ title, schema, ...formProps }) => ( + + + {title} + + +
{ + if (e.errors.length === 0) handleNext(); + }} + {...formProps} + {...transformSchemaToProps(schema)} + > + + +
+
+
+ ))}
{activeStep === steps.length && ( diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/schema.test.ts b/plugins/scaffolder/src/components/MultistepJsonForm/schema.test.ts new file mode 100644 index 0000000000..e02e5be01c --- /dev/null +++ b/plugins/scaffolder/src/components/MultistepJsonForm/schema.test.ts @@ -0,0 +1,71 @@ +/* + * 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 { transformSchemaToProps } from './schema'; + +describe('transformSchemaToProps', () => { + it('transforms deep schema', () => { + const inputSchema = { + type: 'object', + properties: { + field1: { + type: 'string', + 'ui:derp': 'herp', + }, + field2: { + type: 'object', + properties: { + fieldX: { + type: 'string', + 'ui:derp': 'xerp', + }, + }, + }, + }, + }; + const expectedSchema = { + type: 'object', + properties: { + field1: { + type: 'string', + }, + field2: { + type: 'object', + properties: { + fieldX: { + type: 'string', + }, + }, + }, + }, + }; + const expectedUiSchema = { + field1: { + 'ui:derp': 'herp', + }, + field2: { + fieldX: { + 'ui:derp': 'xerp', + }, + }, + }; + + expect(transformSchemaToProps(inputSchema)).toEqual({ + schema: expectedSchema, + uiSchema: expectedUiSchema, + }); + }); +}); diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts b/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts new file mode 100644 index 0000000000..7de3994a8e --- /dev/null +++ b/plugins/scaffolder/src/components/MultistepJsonForm/schema.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2021 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 { JsonObject } from '@backstage/config'; +import { FormProps } from '@rjsf/core'; + +function isObject(value: unknown): value is JsonObject { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function extractUiSchema(schema: JsonObject, uiSchema: JsonObject) { + const { properties } = schema; + if (!isObject(properties)) { + return; + } + for (const propName in properties) { + if (!properties.hasOwnProperty(propName)) { + continue; + } + const schemaNode = properties[propName]; + if (!isObject(schemaNode)) { + continue; + } + + if (schemaNode.type === 'object') { + const innerUiSchema = {}; + uiSchema[propName] = innerUiSchema; + extractUiSchema(schemaNode, innerUiSchema); + } else { + for (const innerKey in schemaNode) { + if (!schemaNode.hasOwnProperty(innerKey)) { + continue; + } + const innerValue = schemaNode[innerKey]; + if (innerKey.startsWith('ui:')) { + const innerUiSchema = uiSchema[propName] || {}; + if (!isObject(innerUiSchema)) { + throw new TypeError('Unexpected non-object in uiSchema'); + } + uiSchema[propName] = innerUiSchema; + + innerUiSchema[innerKey] = innerValue; + delete schemaNode[innerKey]; + } + } + } + } +} + +export function transformSchemaToProps( + inputSchema: JsonObject, +): { schema: FormProps['schema']; uiSchema: FormProps['uiSchema'] } { + const schema = JSON.parse(JSON.stringify(inputSchema)); + delete schema.title; // Rendered separately + const uiSchema = {}; + extractUiSchema(schema, uiSchema); + return { schema, uiSchema }; +}