From d781b331a8314c779e3ffcee746dd4d8f8ab0e01 Mon Sep 17 00:00:00 2001 From: Matt Benson Date: Fri, 16 May 2025 12:59:08 -0500 Subject: [PATCH] render details for composite property schemas Signed-off-by: Matt Benson --- .changeset/eight-planets-see.md | 5 + .../RenderSchema/RenderSchema.test.tsx | 31 ++- .../components/RenderSchema/RenderSchema.tsx | 197 ++++++++++-------- 3 files changed, 141 insertions(+), 92 deletions(-) create mode 100644 .changeset/eight-planets-see.md diff --git a/.changeset/eight-planets-see.md b/.changeset/eight-planets-see.md new file mode 100644 index 0000000000..20adcac7ef --- /dev/null +++ b/.changeset/eight-planets-see.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +render details for composite property schemas diff --git a/plugins/scaffolder/src/components/RenderSchema/RenderSchema.test.tsx b/plugins/scaffolder/src/components/RenderSchema/RenderSchema.test.tsx index 0f1150591d..ec49f414f9 100644 --- a/plugins/scaffolder/src/components/RenderSchema/RenderSchema.test.tsx +++ b/plugins/scaffolder/src/components/RenderSchema/RenderSchema.test.tsx @@ -475,7 +475,7 @@ describe('JSON schema UI rendering', () => { const tr = getByTestId(`properties-row_test.${k}`); expect(tr).toBeInTheDocument(); - const sub = getByTestId(`properties-row_test_sub${i}.${k}`); + const sub = getByTestId(`properties-row_test_oneOf${i}.${k}`); expect(sub).toBeInTheDocument(); expect( @@ -485,7 +485,7 @@ describe('JSON schema UI rendering', () => { ).toBe(true); expect( - getByTestId(`properties-row_test_sub${i}.${k}Flag`), + getByTestId(`properties-row_test_oneOf${i}.${k}Flag`), ).toBeInTheDocument(); } }); @@ -528,10 +528,35 @@ describe('JSON schema UI rendering', () => { for (const i of subs.keys()) { for (const k of Object.keys(subs[i].properties!)) { expect( - rendered.getByTestId(`properties-row_test_sub${i}.${k}`), + rendered.getByTestId(`properties-row_test_oneOf${i}.${k}`), ).toBeInTheDocument(); } } }); + it('property alternatives', async () => { + const schema: JSONSchema7 = { + properties: { + bs: { + anyOf: [ + { + type: 'boolean', + }, + { + type: 'string', + }, + ], + }, + }, + }; + const rendered = await renderInTestApp( + , + ); + expect( + rendered.getByTestId(`root-row_test.bs_anyOf0`), + ).toBeInTheDocument(); + expect( + rendered.getByTestId(`root-row_test.bs_anyOf1`), + ).toBeInTheDocument(); + }); }); }); diff --git a/plugins/scaffolder/src/components/RenderSchema/RenderSchema.tsx b/plugins/scaffolder/src/components/RenderSchema/RenderSchema.tsx index 8f967f8ee2..fc926e6c44 100644 --- a/plugins/scaffolder/src/components/RenderSchema/RenderSchema.tsx +++ b/plugins/scaffolder/src/components/RenderSchema/RenderSchema.tsx @@ -41,37 +41,44 @@ import { JSONSchema7Definition, JSONSchema7Type, } from 'json-schema'; -import { FC, JSX, cloneElement, Fragment } from 'react'; +import { FC, JSX, cloneElement, Fragment, ReactElement } from 'react'; import { scaffolderTranslationRef } from '../../translation'; import { SchemaRenderContext, SchemaRenderStrategy } from './types'; import { TranslationMessages } from '../TemplatingExtensionsPage/types'; -const getTypes = (properties: JSONSchema7) => { - if (!properties.type) { +const compositeSchemaProperties = ['allOf', 'anyOf', 'not', 'oneOf'] as const; + +type subSchemasType = { + [K in (typeof compositeSchemaProperties)[number]]?: JSONSchema7Definition[]; +}; + +const getTypes = (schema: JSONSchema7) => { + if (!schema.type) { + if ( + Object.getOwnPropertyNames(schema).some(p => + compositeSchemaProperties.includes(p as any), + ) + ) { + return undefined; + } return ['unknown']; } - if (properties.type !== 'array') { - return [properties.type].flat(); + if (schema.type !== 'array') { + return [schema.type].flat(); } return [ - `${properties.type}(${ - (properties.items as JSONSchema7 | undefined)?.type ?? 'unknown' + `${schema.type}(${ + (schema.items as JSONSchema7 | undefined)?.type ?? 'unknown' })`, ]; }; -const getSubschemas = ( - schema: JSONSchema7Definition, -): Record => { +const getSubschemas = (schema: JSONSchema7Definition): subSchemasType => { if (typeof schema === 'boolean') { return {}; } const base: Omit = {}; - const compositeSchemaProperties = ['allOf', 'anyOf', 'not', 'oneOf'] as const; - type subSchemasType = { - [K in (typeof compositeSchemaProperties)[number]]?: JSONSchema7Definition[]; - }; const subschemas: subSchemasType = {}; for (const [key, value] of Object.entries(schema) as [ @@ -224,7 +231,10 @@ const inspectSchema = ( return { canSubschema: false, hasEnum: false }; } return { - canSubschema: getTypes(schema).some(t => t.includes('object')), + canSubschema: + Object.getOwnPropertyNames(schema).some(p => + compositeSchemaProperties.includes(p as any), + ) || getTypes(schema)!.some(t => t.includes('object')), hasEnum: !!enumFrom(schema), }; }; @@ -242,8 +252,8 @@ const typeColumn = { const info = inspectSchema(element.schema); return ( <> - {types.map((type, index) => - type.includes('object') || (info.hasEnum && index === 0) ? ( + {types?.map((type, index) => + info.canSubschema || (info.hasEnum && index === 0) ? ( { if (typeof schema === 'object') { - const subschemas = - strategy === 'root' || !context.parent ? getSubschemas(schema) : {}; + const subschemas = getSubschemas(schema); let columns: Column[] | undefined; let elements: SchemaRenderElement[] | undefined; if (strategy === 'root') { @@ -393,90 +402,100 @@ export const RenderSchema = ({ {elements.map(el => { const id = generateId(el, context); const info = inspectSchema(el.schema); - return ( - - - {columns!.map(col => ( - + {columns!.map(col => ( + + {col.render(el, context)} + + ))} + , + ]; + if ( + typeof el.schema !== 'boolean' && + (info.canSubschema || info.hasEnum) + ) { + let details: ReactElement = ( + + {info.canSubschema && ( + - {col.render(el, context)} - - ))} - - {typeof el.schema !== 'boolean' && - (info.canSubschema || info.hasEnum) && ( - - - - - {info.canSubschema && ( - - )} - {info.hasEnum && ( - <> - {cloneElement( - context.headings[0], - {}, - 'Valid values:', - )} - - - )} - - - - + /> )} - - ); + {info.hasEnum && ( + <> + {cloneElement( + context.headings[0], + {}, + 'Valid values:', + )} + + + )} + + ); + if (getTypes(el.schema)) { + details = ( + + {details} + + ); + } + rows.push( + + + {details} + + , + ); + } + return {rows}; })} )} - {Object.keys(subschemas).map(sk => ( + {(Object.keys(subschemas) as Array).map(sk => ( {cloneElement(context.headings[0], {}, sk)} - {subschemas[sk].map((sub, index) => ( + {subschemas[sk]!.map((sub, index) => (